Skip to content
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

[No QA][Search v1] Create ReportListItem #42410

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4772,6 +4772,7 @@ const CONST = {

SEARCH_DATA_TYPES: {
TRANSACTION: 'transaction',
REPORT: 'report',
},

REFERRER: {
Expand Down
56 changes: 53 additions & 3 deletions src/components/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
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';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as SearchActions from '@libs/actions/Search';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
Expand All @@ -12,20 +13,29 @@ import useCustomBackHandler from '@pages/Search/useCustomBackHandler';
import CONST from '@src/CONST';
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';
import SearchTableHeader from './SelectionList/SearchTableHeader';
import type {ReportListItemType, TransactionListItemType} from './SelectionList/types';
import TableListItemSkeleton from './Skeletons/TableListItemSkeleton';

type SearchProps = {
query: string;
policyIDs?: string;
};

function isReportListItemType(item: TransactionListItemType | ReportListItemType): item is ReportListItemType {
const reportListItem = item as ReportListItemType;
return reportListItem.transactions !== undefined;
}

function Search({query, policyIDs}: SearchProps) {
const [selectedItems, setSelectedItems] = useState<Array<SearchTransaction | SearchReport>>([]);
const {isOffline} = useNetwork();
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
const styles = useThemeStyles();
const {isLargeScreenWidth} = useWindowDimensions();
useCustomBackHandler();

const hash = SearchUtils.getQueryHash(query, policyIDs);
Expand Down Expand Up @@ -75,16 +85,56 @@ function Search({query, policyIDs}: SearchProps) {
return null;
}

const toggleListItem = (listItem: TransactionListItemType | ReportListItemType) => {
// @TODO: Selecting checkboxes will be handled in a separate PR
if (isReportListItemType(listItem)) {
Log.info(listItem?.reportID?.toString() ?? '');
return;
}

Log.info(listItem.transactionID);
};

const ListItem = SearchUtils.getListItem(type);
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
const data = SearchUtils.getSections(searchResults?.data ?? {}, type);

const data = SearchUtils.getSections(
{
...searchResults?.data,
// @TODO: Remove this comment when report items are returned from the API, uncomment it to test whether ReportListItem is displayed properly.
// report_0: {
// reportID: 0,
// reportName: 'Alice’s Apples owes $110.00',
// total: 1000,
// currency: 'USD',
// action: 'pay',
// },
} ?? {},
type,
);

luacmartins marked this conversation as resolved.
Show resolved Hide resolved
const toggleAllItems = () => {
if (selectedItems.length === data.length) {
setSelectedItems([]);
} else {
setSelectedItems([...data]);
}
};

luacmartins marked this conversation as resolved.
Show resolved Hide resolved
return (
<SelectionList
canSelectMultiple={isLargeScreenWidth}
onSelectAll={toggleAllItems}
onCheckboxPress={toggleListItem}
customListHeader={<SearchTableHeader data={searchResults?.data} />}
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
ListItem={ListItem}
sections={[{data, isDisabled: false}]}
onSelectRow={(item) => {
openReport(item.transactionThreadReportID);
if (isReportListItemType(item)) {
openReport(item.reportID);
return;
}

luacmartins marked this conversation as resolved.
Show resolved Hide resolved
openReport(item.transactionID);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this empty line

}}
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
Expand Down
54 changes: 54 additions & 0 deletions src/components/SelectionList/ExpenseItemHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import {View} from 'react-native';
import Button from '@components/Button';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import type {SearchAccountDetails} from '@src/types/onyx/SearchResults';
import UserInfoCell from './UserInfoCell';

type ExpenseItemHeaderProps = {
participantFrom: SearchAccountDetails;
participantTo: SearchAccountDetails;
buttonText: string;
onButtonPress: () => void;
};

function ExpenseItemHeader({participantFrom, participantTo, buttonText, onButtonPress}: ExpenseItemHeaderProps) {
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const theme = useTheme();

return (
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween, styles.mb2, styles.gap2]}>
<View style={[styles.flexRow, styles.alignItemsCenter, styles.gap1, styles.flex1]}>
<View style={[styles.mw50]}>
<UserInfoCell participant={participantFrom} />
</View>
<Icon
src={Expensicons.ArrowRightLong}
width={variables.iconSizeXXSmall}
height={variables.iconSizeXXSmall}
fill={theme.icon}
/>
<View style={[styles.flex1, styles.mw50]}>
<UserInfoCell participant={participantTo} />
</View>
</View>
<View style={[StyleUtils.getWidthStyle(variables.w80)]}>
<Button
text={buttonText}
onPress={onButtonPress}
small
pressOnEnter
style={[styles.p0]}
/>
</View>
</View>
);
}

export default ExpenseItemHeader;
48 changes: 48 additions & 0 deletions src/components/SelectionList/ListItemCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import {View} from 'react-native';
import Icon from '@components/Icon';
import {PressableWithFeedback} from '@components/Pressable';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';

type ListItemCheckboxProps = {
accessibilityLabel?: string;
isSelected: boolean;
isDisabled: boolean;
isDisabledCheckbox: boolean;
onPress: () => void;
};

function ListItemCheckbox({accessibilityLabel, isSelected, isDisabled, isDisabledCheckbox, onPress}: ListItemCheckboxProps) {
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const theme = useTheme();

return (
<PressableWithFeedback
accessibilityLabel={accessibilityLabel ?? ''}
role={CONST.ROLE.BUTTON}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
disabled={isDisabled || isDisabledCheckbox}
onPress={onPress}
style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), isDisabledCheckbox && styles.cursorDisabled]}
>
<View style={[StyleUtils.getCheckboxContainerStyle(20), StyleUtils.getMultiselectListStyles(!!isSelected, !!isDisabled)]}>
{isSelected && (
<Icon
src={Expensicons.Checkmark}
fill={theme.textLight}
height={14}
width={14}
/>
)}
</View>
</PressableWithFeedback>
);
}

ListItemCheckbox.displayName = 'ListItemCheckbox';

export default ListItemCheckbox;
168 changes: 168 additions & 0 deletions src/components/SelectionList/ReportListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React from 'react';
import {View} from 'react-native';
import Button from '@components/Button';
import Text from '@components/Text';
import TextWithTooltip from '@components/TextWithTooltip';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import BaseListItem from './BaseListItem';
import ExpenseItemHeader from './ExpenseItemHeader';
import ListItemCheckbox from './ListItemCheckbox';
import TransactionListItem from './TransactionListItem';
import TransactionListItemRow from './TransactionListItemRow';
import type {ListItem, ReportListItemProps, ReportListItemType, TransactionListItemType} from './types';

const TYPE_COLUMN_WIDTH = 52;

function ReportListItem<TItem extends ListItem>({
item,
isFocused,
showTooltip,
isDisabled,
canSelectMultiple,
onSelectRow,
onDismissError,
shouldPreventDefaultFocusOnSelectRow,
onFocus,
shouldSyncFocus,
}: ReportListItemProps<TItem>) {
const reportItem = item as unknown as ReportListItemType;

const styles = useThemeStyles();
const {translate} = useLocalize();
const {isLargeScreenWidth} = useWindowDimensions();
const StyleUtils = useStyleUtils();

const listItemPressableStyle = [styles.selectionListPressableItemWrapper, styles.pv3, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive];

const handleOnButtonPress = () => {
onSelectRow(item);
};

const openReportInRHP = (transactionItem: TransactionListItemType) => {
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(CONST.TAB_SEARCH.ALL, transactionItem.transactionThreadReportID));
};

const totalCell = (
<TextWithTooltip
shouldShowTooltip={showTooltip}
text={CurrencyUtils.convertToDisplayString(reportItem?.total, reportItem?.currency)}
style={[styles.optionDisplayName, styles.textNewKansasNormal, styles.pre, styles.justifyContentCenter, isLargeScreenWidth ? undefined : styles.textAlignRight]}
/>
);

const actionCell = (
<Button
text={translate('common.view')}
onPress={handleOnButtonPress}
small
pressOnEnter
style={[styles.p0]}
/>
);

if (reportItem.transactions.length === 1) {
const transactionItem = reportItem.transactions[0];

return (
<TransactionListItem
item={transactionItem as unknown as TItem}
isFocused={isFocused}
showTooltip={showTooltip}
isDisabled={isDisabled}
canSelectMultiple={canSelectMultiple}
onSelectRow={() => openReportInRHP(transactionItem)}
onDismissError={onDismissError}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
onFocus={onFocus}
shouldSyncFocus={shouldSyncFocus}
/>
);
}
const participantFrom = reportItem.transactions[0].from;
const participantTo = reportItem.transactions[0].to;

if (reportItem?.reportName && reportItem.transactions.length > 1) {
return (
<BaseListItem
item={item}
pressableStyle={listItemPressableStyle}
wrapperStyle={[styles.flexRow, styles.flex1, styles.justifyContentBetween, styles.userSelectNone, styles.alignItemsCenter]}
containerStyle={[styles.mb3]}
isFocused={isFocused}
isDisabled={isDisabled}
showTooltip={showTooltip}
canSelectMultiple={canSelectMultiple}
onSelectRow={onSelectRow}
onDismissError={onDismissError}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
errors={item.errors}
pendingAction={item.pendingAction}
keyForList={item.keyForList}
onFocus={onFocus}
shouldSyncFocus={shouldSyncFocus}
hoverStyle={item.isSelected && styles.activeComponentBG}
>
<View style={styles.flex1}>
{!isLargeScreenWidth && (
<ExpenseItemHeader
participantFrom={participantFrom}
participantTo={participantTo}
buttonText={translate('common.view')}
onButtonPress={handleOnButtonPress}
/>
)}
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, isLargeScreenWidth && styles.mr4]}>
{/** marginRight added here to move the action button by the type column distance */}
<View style={[styles.flexRow, styles.flex1, styles.alignItemsCenter, styles.justifyContentBetween, isLargeScreenWidth && {marginRight: TYPE_COLUMN_WIDTH}]}>
<View style={[styles.flexRow, styles.alignItemsCenter, styles.flex2]}>
{canSelectMultiple && (
<ListItemCheckbox
accessibilityLabel={item.text}
isDisabled={Boolean(isDisabled)}
isDisabledCheckbox={Boolean(item.isDisabledCheckbox)}
isSelected={Boolean(item.isSelected)}
onPress={() => {}}
/>
)}

<View style={[styles.flexShrink1, isLargeScreenWidth && styles.ph4]}>
<Text style={[styles.reportListItemTitle]}>{reportItem?.reportName}</Text>
<Text style={[styles.textMicroSupporting]}>{`${reportItem.transactions.length} grouped expenses`}</Text>
</View>
</View>
<View style={[styles.flexRow, styles.flex1, styles.justifyContentEnd]}>{totalCell}</View>
</View>
{isLargeScreenWidth && <View style={[StyleUtils.getSearchTableColumnStyles(CONST.SEARCH_TABLE_COLUMNS.ACTION)]}>{actionCell}</View>}
</View>
<View style={[styles.mt3, styles.reportListItemSeparator]} />
{reportItem.transactions.map((transaction) => (
<TransactionListItemRow
item={transaction}
showTooltip={showTooltip}
isDisabled={Boolean(isDisabled)}
canSelectMultiple={Boolean(canSelectMultiple)}
onButtonPress={() => {
openReportInRHP(transaction);
}}
showItemHeaderOnNarrowLayout={false}
containerStyle={styles.mt3}
/>
))}
</View>
</BaseListItem>
);
}

return null;
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
}

ReportListItem.displayName = 'ReportListItem';

export default ReportListItem;
2 changes: 1 addition & 1 deletion src/components/SelectionList/SearchTableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function SearchTableHeader({data}: SearchTableHeaderProps) {
}

return (
<View style={[styles.ph5, styles.pb3]}>
<View style={[styles.flex1]}>
<View style={[styles.flex1, styles.flexRow, styles.gap3, styles.ph4]}>
<SearchTableHeaderColumn
containerStyle={[StyleUtils.getSearchTableColumnStyles(CONST.SEARCH_TABLE_COLUMNS.DATE)]}
Expand Down
Loading
Loading