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 12 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
53 changes: 50 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,53 @@ 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);
const reportID = isReportListItemType(item) ? item.reportID : item.transactionThreadReportID;

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we use SearchUtils.getSearchType(searchResults?.search); instead of introducing a new function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This type guard allows to check the type of the item, and then we can use a specific item field to open the report. SearchUtils.getSearchType(searchResults?.search); won't determine an exact type of the item.

openReport(reportID);
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/Search/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) {
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;
170 changes: 170 additions & 0 deletions src/components/SelectionList/Search/ReportListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import React from 'react';
import {View} from 'react-native';
import Button from '@components/Button';
import Checkbox from '@components/Checkbox';
import BaseListItem from '@components/SelectionList/BaseListItem';
import type {ListItem, ReportListItemProps, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
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 ExpenseItemHeader from './ExpenseItemHeader';
import TransactionListItem from './TransactionListItem';
import TransactionListItemRow from './TransactionListItemRow';

const TYPE_COLUMN_WIDTH = 52;
luacmartins marked this conversation as resolved.
Show resolved Hide resolved

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));
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
};

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')}
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to add the logic to display the correct button text. Let's see the design, the button text could be "Review", "Submit",...
Screenshot 2024-05-29 at 15 04 52

Copy link
Contributor

Choose a reason for hiding this comment

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

For now the only option is view, but I agree that we should be getting that from the API results.

Copy link
Contributor

@Kicu Kicu May 29, 2024

Choose a reason for hiding this comment

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

I have started to work on this a while ago and was explicitly told to wait, and the task is on HOLD so it should stay View for now.
We can fix buttons in a dedicated PR - I have a WIP draft open

onPress={handleOnButtonPress}
small
pressOnEnter
style={[styles.p0]}
/>
);

if (!reportItem?.reportName && reportItem.transactions.length > 1) {
return null;
}

const participantFrom = reportItem.transactions[0].from;
const participantTo = reportItem.transactions[0].to;

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}
/>
);
}

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')}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same above

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}]}>
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
<View style={[styles.flexRow, styles.alignItemsCenter, styles.flex2]}>
{canSelectMultiple && (
<Checkbox
onPress={() => {}}
isChecked={item.isSelected}
containerStyle={[StyleUtils.getCheckboxContainerStyle(20), StyleUtils.getMultiselectListStyles(Boolean(item.isSelected), Boolean(item.isDisabled))]}
disabled={Boolean(isDisabled) || item.isDisabledCheckbox}
accessibilityLabel={item.text ?? ''}
style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled]}
/>
)}
luacmartins marked this conversation as resolved.
Show resolved Hide resolved

<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>
);
}

ReportListItem.displayName = 'ReportListItem';

export default ReportListItem;
Loading
Loading