-
Notifications
You must be signed in to change notification settings - Fork 3.7k
feat: Person Amount and Attendees in Expense Table #85332
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
d64c058
d6de2ee
c7c8457
c2ef12a
c7ac28c
21c7142
3aff07e
8c93a95
b2a3aa4
b2a4de5
2e65591
6b8f45c
bb9ba44
ff0857d
3a4438d
384ae0c
4e9cea8
c960ff0
57aa4f0
de668d5
83c9223
3df12e0
af7ca04
d8bf593
8966c13
d8db554
e3a8f7e
e01e871
3321898
7ad2906
b364061
f3ae724
3c00f7e
17b20bd
98d6923
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,136 @@ | ||
| import React from 'react'; | ||
| import {View} from 'react-native'; | ||
| import Avatar from '@components/Avatar'; | ||
| import Text from '@components/Text'; | ||
| import Tooltip from '@components/Tooltip'; | ||
| import UserDetailsTooltip from '@components/UserDetailsTooltip'; | ||
| import useDefaultAvatars from '@hooks/useDefaultAvatars'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import useStyleUtils from '@hooks/useStyleUtils'; | ||
| import useTheme from '@hooks/useTheme'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import {getUserDetailTooltipText, sortIconsByName} from '@libs/ReportUtils'; | ||
| import {getDefaultAvatar} from '@libs/UserAvatarUtils'; | ||
| import colors from '@styles/theme/colors'; | ||
| import variables from '@styles/variables'; | ||
| import CONST from '@src/CONST'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import type {Attendee} from '@src/types/onyx/IOU'; | ||
| import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; | ||
|
|
||
| type AttendeesCellProps = { | ||
| attendees: Attendee[]; | ||
| isHovered: boolean; | ||
| isPressed: boolean; | ||
| }; | ||
|
|
||
| function AttendeesCell({attendees, isHovered, isPressed}: AttendeesCellProps) { | ||
| const defaultAvatars = useDefaultAvatars(); | ||
| const attendeeIcons: IconType[] = attendees.map((attendee) => ({ | ||
| id: attendee.accountID ?? CONST.DEFAULT_NUMBER_ID, | ||
| name: attendee.displayName ?? attendee.email, | ||
| source: (attendee.avatarUrl || getDefaultAvatar({accountID: attendee.accountID, accountEmail: attendee.email, defaultAvatars})) ?? '', | ||
| type: CONST.ICON_TYPE_AVATAR, | ||
| })); | ||
|
|
||
| const theme = useTheme(); | ||
| const styles = useThemeStyles(); | ||
| const StyleUtils = useStyleUtils(); | ||
| const {localeCompare, formatPhoneNumber} = useLocalize(); | ||
|
|
||
| const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); | ||
|
|
||
| const size = CONST.AVATAR_SIZE.SMALLER; | ||
| const maxAvatarsInRow = CONST.AVATAR_ROW_SIZE.DEFAULT; | ||
| const oneAvatarSize = StyleUtils.getAvatarStyle(size); | ||
| const oneAvatarBorderWidth = StyleUtils.getAvatarBorderWidth(size).borderWidth ?? 0; | ||
| const overlapSize = oneAvatarSize.width / 3 + 2 * oneAvatarBorderWidth; | ||
| const height = oneAvatarSize.height; | ||
| const avatarContainerStyles = StyleUtils.combineStyles([styles.alignItemsCenter, styles.flexRow, StyleUtils.getHeight(height), styles.overflowHidden]); | ||
|
|
||
| const icons = sortIconsByName(attendeeIcons, personalDetails, localeCompare); | ||
| const tooltipTexts = icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, icon.name)); | ||
|
|
||
| return ( | ||
| <View | ||
| style={avatarContainerStyles} | ||
| testID="AttendeesCell-Row" | ||
| > | ||
| {[...icons].splice(0, maxAvatarsInRow).map((icon, index) => ( | ||
| <UserDetailsTooltip | ||
| // eslint-disable-next-line react/no-array-index-key | ||
| key={`stackedAvatars-${icon.id}-${index}`} | ||
| accountID={Number(icon.id)} | ||
| icon={icon} | ||
| fallbackUserDetails={{ | ||
| // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing | ||
| displayName: icon.name, | ||
| }} | ||
| shouldRender | ||
| > | ||
| <View style={[StyleUtils.getHorizontalStackedAvatarStyle(index, overlapSize, -oneAvatarBorderWidth), StyleUtils.getAvatarBorderRadius(size, icon.type)]}> | ||
| <Avatar | ||
| iconAdditionalStyles={[ | ||
| StyleUtils.getHorizontalStackedAvatarBorderStyle({ | ||
| theme, | ||
| isHovered, | ||
| isPressed, | ||
| isInReportAction: true, | ||
| shouldUseCardBackground: true, | ||
| isActive: false, | ||
| customPressedBorderColor: theme.activeComponentBG, | ||
| }), | ||
| StyleUtils.getAvatarBorderWidth(size), | ||
| ]} | ||
| source={icon.source} | ||
| size={size} | ||
| name={icon.name} | ||
| avatarID={icon.id} | ||
| type={icon.type} | ||
| fallbackIcon={icon.fallbackIcon} | ||
| testID="AttendeesCell-Avatar" | ||
| /> | ||
| </View> | ||
| </UserDetailsTooltip> | ||
| ))} | ||
| {icons.length > maxAvatarsInRow && ( | ||
| <Tooltip | ||
| // We only want to cap tooltips to only 10 users or so since some reports have hundreds of users, causing performance to degrade. | ||
| text={tooltipTexts.slice(maxAvatarsInRow - 1, maxAvatarsInRow + 9).join(', ')} | ||
|
justinpersaud marked this conversation as resolved.
|
||
| shouldRender | ||
| > | ||
| <View | ||
| testID="AttendeesCell-LimitReached" | ||
| style={[ | ||
| styles.alignItemsCenter, | ||
| styles.justifyContentCenter, | ||
| StyleUtils.getHorizontalStackedAvatarBorderStyle({ | ||
| theme, | ||
| isHovered, | ||
| isPressed, | ||
| isInReportAction: true, | ||
| shouldUseCardBackground: true, | ||
| customPressedBorderColor: theme.activeComponentBG, | ||
| }), | ||
|
|
||
| // Set overlay background color with RGBA value so that the text will not inherit opacity | ||
| StyleUtils.getHorizontalStackedOverlayAvatarStyle(oneAvatarSize, oneAvatarBorderWidth), | ||
| icons.at(3)?.type === CONST.ICON_TYPE_WORKSPACE && StyleUtils.getAvatarBorderRadius(size, icons.at(3)?.type), | ||
|
justinpersaud marked this conversation as resolved.
|
||
| StyleUtils.getBackgroundColorWithOpacityStyle(colors.productDark400, variables.overlayOpacity), | ||
| ]} | ||
| > | ||
| <View style={[styles.justifyContentCenter, styles.alignItemsCenter, StyleUtils.getHeight(oneAvatarSize.height), StyleUtils.getWidthStyle(oneAvatarSize.width)]}> | ||
| <Text | ||
| style={[styles.avatarInnerTextSmall, StyleUtils.getAvatarExtraFontSizeStyle(size), styles.userSelectNone, styles.textMicroBold, styles.buttonSuccessText]} | ||
| dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} | ||
| >{`+${icons.length - maxAvatarsInRow}`}</Text> | ||
| </View> | ||
| </View> | ||
| </Tooltip> | ||
| )} | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| export default AttendeesCell; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import type {TransactionWithOptionalHighlight} from '@components/MoneyRequestRep | |
| import {PressableWithFeedback} from '@components/Pressable'; | ||
| import RadioButton from '@components/RadioButton'; | ||
| import ActionCell from '@components/Search/SearchList/ListItem/ActionCell'; | ||
| import AttendeesCell from '@components/Search/SearchList/ListItem/AttendeesCell'; | ||
| import DateCell from '@components/Search/SearchList/ListItem/DateCell'; | ||
| import ExportedIconCell from '@components/Search/SearchList/ListItem/ExportedIconCell'; | ||
| import StatusCell from '@components/Search/SearchList/ListItem/StatusCell'; | ||
|
|
@@ -16,6 +17,7 @@ import UserInfoCell from '@components/Search/SearchList/ListItem/UserInfoCell'; | |
| import WorkspaceCell from '@components/Search/SearchList/ListItem/WorkspaceCell'; | ||
| import type {SearchColumnType, TableColumnSize} from '@components/Search/types'; | ||
| import Text from '@components/Text'; | ||
| import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; | ||
| import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
|
|
@@ -29,6 +31,9 @@ import {getReportName} from '@libs/ReportNameUtils'; | |
| import {isExpenseReport, isIOUReport, isSettled} from '@libs/ReportUtils'; | ||
| import StringUtils from '@libs/StringUtils'; | ||
| import { | ||
| getAmount, | ||
| getAttendees, | ||
| getCurrency, | ||
| getDescription, | ||
| getExchangeRate, | ||
| getMerchant, | ||
|
|
@@ -43,6 +48,7 @@ import { | |
| isScanning, | ||
| isTimeRequest, | ||
| isUnreportedAndHasInvalidDistanceRateTransaction, | ||
| shouldShowAttendees as shouldShowAttendeesUtils, | ||
| } from '@libs/TransactionUtils'; | ||
| import variables from '@styles/variables'; | ||
| import CONST from '@src/CONST'; | ||
|
|
@@ -202,6 +208,7 @@ function TransactionItemRow({ | |
| const hasCategoryOrTag = !isCategoryMissing(transactionItem?.category) || !!transactionItem.tag; | ||
| const createdAt = getTransactionCreated(transactionItem); | ||
| const expensicons = useMemoizedLazyExpensifyIcons(['ArrowRight']); | ||
| const currentUserPersonalDetails = useCurrentUserPersonalDetails(); | ||
| const transactionThreadReportID = reportActions ? getIOUActionForTransactionID(reportActions, transactionItem.transactionID)?.childReportID : undefined; | ||
|
|
||
| const isDateColumnWide = dateColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE; | ||
|
|
@@ -267,6 +274,21 @@ function TransactionItemRow({ | |
| return transactionItem.cardName; | ||
| }, [transactionItem.cardID, transactionItem.cardName, transactionItem.isCardFeedDeleted, customCardNames, translate]); | ||
|
|
||
| const transactionAttendees = useMemo(() => getAttendees(transactionItem, currentUserPersonalDetails), [transactionItem, currentUserPersonalDetails]); | ||
|
|
||
| const shouldShowAttendees = shouldShowAttendeesUtils(CONST.IOU.TYPE.SUBMIT, policy) && transactionAttendees.length > 0; | ||
|
|
||
| const totalPerAttendee = useMemo(() => { | ||
| const attendeesCount = transactionAttendees.length ?? 0; | ||
| const totalAmount = getAmount(transactionItem); | ||
|
|
||
| if (!attendeesCount || totalAmount === undefined) { | ||
|
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 think we also need to check
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. @nkdengineer can you look into this one?
Contributor
Author
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. @justinpersaud What we should show in the case
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.
Contributor
Author
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. @justinpersaud Updated. |
||
| return undefined; | ||
| } | ||
|
|
||
| return totalAmount / attendeesCount; | ||
| }, [transactionAttendees.length, transactionItem]); | ||
|
|
||
| const renderColumn = (column: SearchColumnType): React.ReactNode => { | ||
| switch (column) { | ||
| case CONST.SEARCH.TABLE_COLUMNS.TYPE: | ||
|
|
@@ -495,6 +517,21 @@ function TransactionItemRow({ | |
| <TextCell text={cardName} /> | ||
| </View> | ||
| ); | ||
| case CONST.SEARCH.TABLE_COLUMNS.ATTENDEES: | ||
| return ( | ||
| <View | ||
| key={column} | ||
| style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.ATTENDEES)]} | ||
| > | ||
| {shouldShowAttendees && ( | ||
| <AttendeesCell | ||
| attendees={transactionAttendees} | ||
| isHovered={isHover} | ||
| isPressed={isSelected} | ||
| /> | ||
| )} | ||
| </View> | ||
| ); | ||
| case CONST.SEARCH.TABLE_COLUMNS.COMMENTS: | ||
| return ( | ||
| <View | ||
|
|
@@ -529,6 +566,21 @@ function TransactionItemRow({ | |
| /> | ||
| </View> | ||
| ); | ||
| case CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE: | ||
| return ( | ||
| <View | ||
| key={column} | ||
| style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.TOTAL_PER_ATTENDEE, undefined, isAmountColumnWide)]} | ||
| > | ||
| {shouldShowAttendees && ( | ||
| <AmountCell | ||
| total={totalPerAttendee ?? 0} | ||
| currency={getCurrency(transactionItem)} | ||
| isScanning={isScanning(transactionItem)} | ||
| /> | ||
| )} | ||
| </View> | ||
| ); | ||
| case CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT: | ||
| return ( | ||
| <View | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NAB: Let's remove
useMemo, inline style, so react-compiler can optimize the component itselfThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a few feedbacks so let's fix this @nkdengineer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one as well @nkdengineer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@justinpersaud I updated.