From 08d9b5a0f5416d74f2f7796554cd440de93bdd11 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 16 May 2024 14:27:35 +0200 Subject: [PATCH 01/18] Add template for ReportListItem --- src/CONST.ts | 1 + .../SelectionList/ReportListItem.tsx | 21 ++++++++++++ src/components/SelectionList/types.ts | 14 ++++++-- src/libs/SearchUtils.ts | 26 ++++++++++++-- src/types/onyx/SearchResults.ts | 34 ++++++++++++++++--- 5 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 src/components/SelectionList/ReportListItem.tsx diff --git a/src/CONST.ts b/src/CONST.ts index a983a18e3d6a..8c6d4dffa668 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4771,6 +4771,7 @@ const CONST = { SEARCH_DATA_TYPES: { TRANSACTION: 'transaction', + REPORT: 'report', }, REFERRER: { diff --git a/src/components/SelectionList/ReportListItem.tsx b/src/components/SelectionList/ReportListItem.tsx new file mode 100644 index 000000000000..c52aa159b289 --- /dev/null +++ b/src/components/SelectionList/ReportListItem.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import type {ListItem, ReportListItemProps} from './types'; + +function ReportListItem({ + item, + isFocused, + showTooltip, + isDisabled, + canSelectMultiple, + onSelectRow, + onDismissError, + shouldPreventDefaultFocusOnSelectRow, + onFocus, + shouldSyncFocus, +}: ReportListItemProps) { + return null; +} + +ReportListItem.displayName = 'ReportListItem'; + +export default ReportListItem; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 79e47e4aa4d7..13ed04937fef 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -4,12 +4,13 @@ import type {MaybePhraseKey} from '@libs/Localize'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; import type CONST from '@src/CONST'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import type {SearchAccountDetails, SearchTransaction} from '@src/types/onyx/SearchResults'; +import type {SearchAccountDetails, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type IconAsset from '@src/types/utils/IconAsset'; import type InviteMemberListItem from './InviteMemberListItem'; import type RadioListItem from './RadioListItem'; +import type ReportListItem from './ReportListItem'; import type TableListItem from './TableListItem'; import type TransactionListItem from './TransactionListItem'; import type UserListItem from './UserListItem'; @@ -148,6 +149,11 @@ type TransactionListItemType = ListItem & shouldShowTax: boolean; }; +type ReportListItemType = ListItem & + SearchReport & { + transactions: SearchTransaction[]; + }; + type ListItemProps = CommonListItemProps & { /** The section list item */ item: TItem; @@ -206,7 +212,9 @@ type TableListItemProps = ListItemProps; type TransactionListItemProps = ListItemProps; -type ValidListItem = typeof RadioListItem | typeof UserListItem | typeof TableListItem | typeof InviteMemberListItem | typeof TransactionListItem; +type ReportListItemProps = ListItemProps; + +type ValidListItem = typeof RadioListItem | typeof UserListItem | typeof TableListItem | typeof InviteMemberListItem | typeof TransactionListItem | typeof ReportListItem; type Section = { /** Title of the section */ @@ -412,4 +420,6 @@ export type { TransactionListItemType, UserListItemProps, ValidListItem, + ReportListItemProps, + ReportListItemType, }; diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index fb07990237f9..d6b03aab6aae 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,6 +1,7 @@ import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils'; +import ReportListItem from '@components/SelectionList/ReportListItem'; import TransactionListItem from '@components/SelectionList/TransactionListItem'; -import type {TransactionListItemType} from '@components/SelectionList/types'; +import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -54,11 +55,32 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): Transac }); } +function getReportSections(data: OnyxTypes.SearchResults['data']): ReportListItemType[] { + const reportIDToTransactions: Record = {}; + for (const key in data) { + if (key.startsWith('transactions_')) { + const transaction = {...data[key]}; + const reportKey = `report_${transaction.reportID}`; + if (reportIDToTransactions?.[reportKey].transactions) { + reportIDToTransactions[reportKey].transactions.push(transaction); + } else { + reportIDToTransactions[reportKey].transactions = [transaction]; + } + } + } + + return Object.values(reportIDToTransactions); +} + const searchTypeToItemMap: SearchTypeToItemMap = { [CONST.SEARCH_DATA_TYPES.TRANSACTION]: { listItem: TransactionListItem, getSections: getTransactionsSections, }, + [CONST.SEARCH_DATA_TYPES.REPORT]: { + listItem: ReportListItem, + getSections: getReportSections, + }, }; function getListItem(type: K): SearchTypeToItemMap[K]['listItem'] { @@ -74,4 +96,4 @@ function getQueryHash(query: string, policyID?: string): number { return UserUtils.hashText(textToHash, 2 ** 32); } -export {getListItem, getQueryHash, getSections, getShouldShowColumn, getShouldShowMerchant, getSearchType}; +export {getListItem, getQueryHash, getSearchType, getSections, getShouldShowColumn, getShouldShowMerchant}; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index a4cf9c2870d0..662b9c8a53ff 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -1,12 +1,21 @@ import type {ValueOf} from 'type-fest'; +import type ReportListItem from '@components/SelectionList/ReportListItem'; import type TransactionListItem from '@components/SelectionList/TransactionListItem'; import type CONST from '@src/CONST'; type SearchDataTypes = ValueOf; -type ListItemType = T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION ? typeof TransactionListItem : never; +type ListItemType = T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION + ? typeof TransactionListItem + : T extends typeof CONST.SEARCH_DATA_TYPES.REPORT + ? typeof ReportListItem + : never; -type SectionsType = T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION ? SearchTransaction[] : never; +type SectionsType = T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION + ? SearchTransaction[] + : T extends typeof CONST.SEARCH_DATA_TYPES.REPORT + ? SearchReport[] + : never; type SearchTypeToItemMap = { [K in SearchDataTypes]: { @@ -34,6 +43,23 @@ type SearchPolicyDetails = { name: string; }; +type SearchReport = { + /** The ID of the report */ + reportID: string; + + /** The name of the report */ + reportName: string; + + /** The report total amount */ + total: number; + + /** The report currency */ + currency: string; + + /** The action that can be performed for the report */ + action: string; +}; + type SearchTransaction = { /** The ID of the transaction */ transactionID: string; @@ -122,9 +148,9 @@ type SearchQuery = ValueOf; type SearchResults = { search: SearchResultsInfo; - data: Record> & Record; + data: Record> & Record & Record; }; export default SearchResults; -export type {SearchQuery, SearchTransaction, SearchTransactionType, SearchPersonalDetails, SearchPolicyDetails, SearchAccountDetails, SearchDataTypes, SearchTypeToItemMap}; +export type {SearchQuery, SearchTransaction, SearchTransactionType, SearchPersonalDetails, SearchPolicyDetails, SearchAccountDetails, SearchDataTypes, SearchTypeToItemMap, SearchReport}; From 6b6916629cb63ae29f595eb45b3f7bf0ed776ee9 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 16 May 2024 16:48:32 +0200 Subject: [PATCH 02/18] Add template of getReportSections --- src/components/Search.tsx | 19 +++++++- .../SelectionList/ReportListItem.tsx | 22 ++++++++- src/components/SelectionList/types.ts | 2 +- src/libs/SearchUtils.ts | 47 +++++++++++++++---- src/types/onyx/SearchResults.ts | 10 ++-- 5 files changed, 83 insertions(+), 17 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index a8e469da7d99..9e1cb4fca5cd 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -66,7 +66,20 @@ function Search({query, policyIDs}: SearchProps) { } const ListItem = SearchUtils.getListItem(type); - const data = SearchUtils.getSections(searchResults?.data ?? {}, type); + + const data = SearchUtils.getSections( + { + ...searchResults?.data, + report_5985708612548179: { + reportID: 5985708612548179, + reportName: 'name', + total: 1000, + currency: 'USD', + action: 'pay', + }, + } ?? {}, + type, + ); return ( { + if (!item?.transactionThreadReportID) { + return; + } + openReport(item.transactionThreadReportID); }} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} diff --git a/src/components/SelectionList/ReportListItem.tsx b/src/components/SelectionList/ReportListItem.tsx index c52aa159b289..256b696c86a9 100644 --- a/src/components/SelectionList/ReportListItem.tsx +++ b/src/components/SelectionList/ReportListItem.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import type {ListItem, ReportListItemProps} from './types'; +import TransactionListItem from './TransactionListItem'; +import type {ListItem, ReportListItemProps, ReportListItemType} from './types'; function ReportListItem({ item, @@ -13,6 +14,25 @@ function ReportListItem({ onFocus, shouldSyncFocus, }: ReportListItemProps) { + const reportItem = item as unknown as ReportListItemType; + + if (reportItem.transactions.length === 1) { + return ( + + ); + } + return null; } diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 13ed04937fef..020d63d4e3d7 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -151,7 +151,7 @@ type TransactionListItemType = ListItem & type ReportListItemType = ListItem & SearchReport & { - transactions: SearchTransaction[]; + transactions: TransactionListItemType[]; }; type ListItemProps = CommonListItemProps & { diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index d6b03aab6aae..edb9a031670f 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -8,13 +8,19 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {SearchDataTypes, SearchTypeToItemMap} from '@src/types/onyx/SearchResults'; import * as UserUtils from './UserUtils'; +function isSearchDataType(type: string): type is SearchDataTypes { + const searchDataTypes: string[] = Object.values(CONST.SEARCH_DATA_TYPES); + return searchDataTypes.includes(type); +} + function getSearchType(search: OnyxTypes.SearchResults['search']): SearchDataTypes | undefined { - switch (search.type) { - case CONST.SEARCH_DATA_TYPES.TRANSACTION: - return CONST.SEARCH_DATA_TYPES.TRANSACTION; - default: - return undefined; + if (!isSearchDataType(search.type)) { + return undefined; } + + // @TODO: It's a temporary setting for testing purposes. Uncomment the comment below when ReportListItem is ready. + // return search.type; + return 'report'; } function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean { @@ -56,16 +62,39 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): Transac } function getReportSections(data: OnyxTypes.SearchResults['data']): ReportListItemType[] { + const shouldShowMerchant = getShouldShowMerchant(data); + const shouldShowCategory = getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.CATEGORY); + const shouldShowTag = getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAG); + const shouldShowTax = getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT); + const reportIDToTransactions: Record = {}; for (const key in data) { if (key.startsWith('transactions_')) { - const transaction = {...data[key]}; - const reportKey = `report_${transaction.reportID}`; - if (reportIDToTransactions?.[reportKey].transactions) { + const value = {...data[key]}; + const isExpenseReport = value.reportType === CONST.REPORT.TYPE.EXPENSE; + const reportKey = `report_${value.reportID}`; + const transaction = { + ...value, + from: data.personalDetailsList?.[value.accountID], + to: isExpenseReport ? data[`${ONYXKEYS.COLLECTION.POLICY}${value.policyID}`] : data.personalDetailsList?.[value.managerID], + shouldShowMerchant, + shouldShowCategory, + shouldShowTag, + shouldShowTax, + keyForList: value.transactionID, + }; + if (reportIDToTransactions?.[reportKey]?.transactions) { reportIDToTransactions[reportKey].transactions.push(transaction); } else { - reportIDToTransactions[reportKey].transactions = [transaction]; + reportIDToTransactions[reportKey] = {transactions: [transaction]}; } + } else if (key.startsWith('report_')) { + const value = {...data[key]}; + const reportKey = `report_${value.reportID}`; + reportIDToTransactions[reportKey] = { + ...value, + transactions: reportIDToTransactions[reportKey]?.transactions ?? [], + }; } } diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 662b9c8a53ff..e686e7d02e4f 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -45,19 +45,19 @@ type SearchPolicyDetails = { type SearchReport = { /** The ID of the report */ - reportID: string; + reportID?: string; /** The name of the report */ - reportName: string; + reportName?: string; /** The report total amount */ - total: number; + total?: number; /** The report currency */ - currency: string; + currency?: string; /** The action that can be performed for the report */ - action: string; + action?: string; }; type SearchTransaction = { From 3adf6c6777bae9f3022ea828e191b33c7cd63eff Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 17 May 2024 12:57:36 +0200 Subject: [PATCH 03/18] Fix columns alignment --- src/components/Search.tsx | 19 +++++++- .../SelectionList/SearchTableHeader.tsx | 2 +- .../SelectionList/TransactionListItem.tsx | 46 ++++++++++++++----- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 9e1cb4fca5cd..380ba42d4f63 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React, {useEffect, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -11,6 +11,7 @@ import EmptySearchView from '@pages/Search/EmptySearchView'; import useCustomBackHandler from '@pages/Search/useCustomBackHandler'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import SelectionList from './SelectionList'; @@ -23,6 +24,7 @@ type SearchProps = { }; function Search({query, policyIDs}: SearchProps) { + const [selectedItems, setSelectedItems] = useState>([]); const {isOffline} = useNetwork(); const styles = useThemeStyles(); useCustomBackHandler(); @@ -65,6 +67,10 @@ function Search({query, policyIDs}: SearchProps) { return null; } + const toggleListItem = (listItem: SearchTransaction | SearchReport) => { + console.log(listItem); + }; + const ListItem = SearchUtils.getListItem(type); const data = SearchUtils.getSections( @@ -81,8 +87,19 @@ function Search({query, policyIDs}: SearchProps) { type, ); + const toggleAllItems = () => { + if (selectedItems.length === data.length) { + setSelectedItems([]); + } else { + setSelectedItems([...data]); + } + }; + return ( } ListItem={ListItem} sections={[{data, isDisabled: false}]} diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index ec0267d20c04..1226e3cdebb8 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -30,7 +30,7 @@ function SearchTableHeader({data}: SearchTableHeaderProps) { } return ( - + ({ hoverStyle={item.isSelected && styles.activeComponentBG} > {() => ( - - {dateCell} - {merchantCell} - {userCell(transactionItem.from)} - {userCell(transactionItem.to)} - {transactionItem.shouldShowCategory && {categoryCell}} - {transactionItem.shouldShowTag && {tagCell}} - {transactionItem.shouldShowTax && {taxCell}} - {totalCell} - {typeCell} - {actionCell} + + {canSelectMultiple && ( + {}} + style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled]} + > + + {item.isSelected && ( + + )} + + + )} + + {dateCell} + {merchantCell} + {userCell(transactionItem.from)} + {userCell(transactionItem.to)} + {transactionItem.shouldShowCategory && {categoryCell}} + {transactionItem.shouldShowTag && {tagCell}} + {transactionItem.shouldShowTax && {taxCell}} + {totalCell} + {typeCell} + {actionCell} + )} From 15c67fedd05f4192c3acf6fdf6aca0b2c059983b Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 17 May 2024 15:16:04 +0200 Subject: [PATCH 04/18] Add header for ReportListItem --- src/components/Search.tsx | 6 +- .../SelectionList/ReportListItem.tsx | 117 ++++++++++++++++++ 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index c2d58730eefc..c72de0312dac 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -86,9 +86,9 @@ function Search({query, policyIDs}: SearchProps) { const data = SearchUtils.getSections( { ...searchResults?.data, - report_5985708612548179: { - reportID: 5985708612548179, - reportName: 'name', + report_0: { + reportID: 0, + reportName: 'Alice’s Apples owes $110.00', total: 1000, currency: 'USD', action: 'pay', diff --git a/src/components/SelectionList/ReportListItem.tsx b/src/components/SelectionList/ReportListItem.tsx index 256b696c86a9..d31aeb5805af 100644 --- a/src/components/SelectionList/ReportListItem.tsx +++ b/src/components/SelectionList/ReportListItem.tsx @@ -1,4 +1,18 @@ import React from 'react'; +import {useWindowDimensions, View} from 'react-native'; +import Button from '@components/Button'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import {PressableWithFeedback} from '@components/Pressable'; +import Text from '@components/Text'; +import TextWithTooltip from '@components/TextWithTooltip'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import CONST from '@src/CONST'; +import BaseListItem from './BaseListItem'; import TransactionListItem from './TransactionListItem'; import type {ListItem, ReportListItemProps, ReportListItemType} from './types'; @@ -16,6 +30,34 @@ function ReportListItem({ }: ReportListItemProps) { const reportItem = item as unknown as ReportListItemType; + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const theme = useTheme(); + const {isLargeScreenWidth} = useWindowDimensions(); + const StyleUtils = useStyleUtils(); + + const listItemPressableStyle = [styles.selectionListPressableItemWrapper, styles.pv3, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive]; + + const totalCell = ( + + ); + + const actionCell = ( +