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

[TS migration] Migrate 'ReportActionItemSingle.js' component to TypeScript #33552

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
95915b6
Migrate 'ReportActionItemSingle.js' component to TypeScript
rayane-d Dec 23, 2023
e1d5cbb
complete remainnig migration of 'ReportActionItemSingle.js' component…
rayane-d Dec 24, 2023
54d8b05
fix lint errors
rayane-d Dec 24, 2023
cad0057
modify pendingAction type
rayane-d Dec 24, 2023
8ea1a83
fix failing test
rayane-d Dec 24, 2023
192ff3a
ignore lint warning
rayane-d Dec 24, 2023
4ce5877
Merge branch 'main' into Migrate-ReportActionItemSingle.js-component-…
rayane-d Dec 26, 2023
c72d4b6
edit comment
rayane-d Dec 26, 2023
fb0fb1f
edit comment
rayane-d Dec 26, 2023
2e0a571
Merge branch 'main' into Migrate-ReportActionItemSingle.js-component-…
rayane-d Dec 27, 2023
03dd713
make props optional
rayane-d Dec 29, 2023
1e8d47f
fix expression for when login is empty string
rayane-d Dec 29, 2023
f21ecce
using || instead of ??
rayane-d Dec 29, 2023
e357d3d
using undefined instead of null for default pendingAction
rayane-d Dec 29, 2023
a7eac8f
fix secondaryUserAvatar statement
rayane-d Dec 29, 2023
ee3f3b3
fix for when delegateAccountID is empty string
rayane-d Dec 29, 2023
4adc9cb
fix shouldDisableDetailPage statement
rayane-d Dec 29, 2023
a191972
use null instead of undefined for pendingAction default
rayane-d Dec 29, 2023
7238366
fix lint error
rayane-d Dec 29, 2023
0733b12
run prettier
rayane-d Dec 29, 2023
b7b84b6
Merge branch 'main' into Migrate-ReportActionItemSingle.js-component-…
rayane-d Dec 30, 2023
91352ea
Merge branch 'Expensify:main' into Migrate-ReportActionItemSingle.js-…
rayane-d Dec 31, 2023
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
3 changes: 2 additions & 1 deletion src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ROUTES from '@src/ROUTES';
import {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, Report, ReportAction, ReportMetadata, Session, Transaction} from '@src/types/onyx';
import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import {IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage';
import {Status} from '@src/types/onyx/PersonalDetails';
import {NotificationPreference} from '@src/types/onyx/Report';
import {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction';
import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction';
Expand Down Expand Up @@ -329,7 +330,7 @@ type OptionData = {
login?: string | null;
accountID?: number | null;
pronouns?: string;
status?: string | null;
status?: Status | null;
phoneNumber?: string | null;
isUnread?: boolean | null;
isUnreadWithMention?: boolean | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback, useMemo} from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import {StyleProp, View, ViewStyle} from 'react-native';
import Avatar from '@components/Avatar';
import MultipleAvatars from '@components/MultipleAvatars';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
Expand All @@ -12,7 +9,7 @@ import SubscriptAvatar from '@components/SubscriptAvatar';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import UserDetailsTooltip from '@components/UserDetailsTooltip';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -21,112 +18,136 @@ import DateUtils from '@libs/DateUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import * as UserUtils from '@libs/UserUtils';
import reportPropTypes from '@pages/reportPropTypes';
import stylePropTypes from '@styles/stylePropTypes';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Report, ReportAction} from '@src/types/onyx';
import {AvatarType} from '@src/types/onyx/OnyxCommon';
import ReportActionItemDate from './ReportActionItemDate';
import ReportActionItemFragment from './ReportActionItemFragment';
import reportActionPropTypes from './reportActionPropTypes';

const propTypes = {
type ReportActionItemSingleProps = {
/** All the data of the action */
action: PropTypes.shape(reportActionPropTypes).isRequired,
action: ReportAction;

/** Styles for the outermost View */
wrapperStyle: stylePropTypes,
wrapperStyle?: StyleProp<ViewStyle>;

/** Children view component for this action item */
children: PropTypes.node.isRequired,
children: React.ReactNode;

rayane-d marked this conversation as resolved.
Show resolved Hide resolved
/** Report for this action */
report: reportPropTypes,
report: Report;

/** IOU Report for this action, if any */
iouReport: reportPropTypes,
iouReport?: Report;

/** Show header for action */
showHeader: PropTypes.bool,
showHeader?: boolean;

/** Determines if the avatar is displayed as a subscript (positioned lower than normal) */
shouldShowSubscriptAvatar: PropTypes.bool,
shouldShowSubscriptAvatar?: boolean;

/** If the message has been flagged for moderation */
hasBeenFlagged: PropTypes.bool,
hasBeenFlagged?: boolean;

/** If the action is being hovered */
isHovered: PropTypes.bool,

...withLocalizePropTypes,
isHovered?: boolean;
};

const defaultProps = {
wrapperStyle: undefined,
showHeader: true,
shouldShowSubscriptAvatar: false,
hasBeenFlagged: false,
report: undefined,
iouReport: undefined,
isHovered: false,
type SubAvatar = {
/** Avatar source to display */
rayane-d marked this conversation as resolved.
Show resolved Hide resolved
source: UserUtils.AvatarSource;

/** Denotes whether it is a user avatar or a workspace avatar */
type: AvatarType;

/** Owner of the avatar. If user, displayName. If workspace, policy name */
name: string;

/** Avatar id */
id?: number | string;

/** A fallback avatar icon to display when there is an error on loading avatar from remote URL */
fallbackIcon?: UserUtils.AvatarSource;
};

const showUserDetails = (accountID) => {
const showUserDetails = (accountID: string) => {
Navigation.navigate(ROUTES.PROFILE.getRoute(accountID));
};

const showWorkspaceDetails = (reportID) => {
const showWorkspaceDetails = (reportID: string) => {
Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID));
};

function ReportActionItemSingle(props) {
function ReportActionItemSingle({
action,
children,
wrapperStyle,
showHeader = true,
shouldShowSubscriptAvatar = false,
hasBeenFlagged = false,
report,
iouReport,
isHovered = false,
}: ReportActionItemSingleProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
const actorAccountID = props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && props.iouReport ? props.iouReport.managerID : props.action.actorAccountID;
const {translate} = useLocalize();
const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT;
const actorAccountID = action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action.actorAccountID;
let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID);
const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID] || {};
let actorHint = (login || displayName || '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '');
const displayAllActors = useMemo(() => props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && props.iouReport, [props.action.actionName, props.iouReport]);
const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(props.report) && (!actorAccountID || displayAllActors);
let avatarSource = UserUtils.getAvatar(avatar, actorAccountID);
const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {};
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '');
const displayAllActors = useMemo(() => action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action.actionName, iouReport]);
const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors);
let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID);

if (isWorkspaceActor) {
displayName = ReportUtils.getPolicyName(props.report);
displayName = ReportUtils.getPolicyName(report);
actorHint = displayName;
avatarSource = ReportUtils.getWorkspaceAvatar(props.report);
} else if (props.action.delegateAccountID && personalDetails[props.action.delegateAccountID]) {
avatarSource = ReportUtils.getWorkspaceAvatar(report);
} else if (action.delegateAccountID && personalDetails[action.delegateAccountID]) {
// We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their
// details. This will be improved upon when the Copilot feature is implemented.
const delegateDetails = personalDetails[props.action.delegateAccountID];
const delegateDisplayName = delegateDetails.displayName;
actorHint = `${delegateDisplayName} (${props.translate('reportAction.asCopilot')} ${displayName})`;
const delegateDetails = personalDetails[action.delegateAccountID];
const delegateDisplayName = delegateDetails?.displayName;
actorHint = `${delegateDisplayName} (${translate('reportAction.asCopilot')} ${displayName})`;
displayName = actorHint;
avatarSource = UserUtils.getAvatar(delegateDetails.avatar, props.action.delegateAccountID);
avatarSource = UserUtils.getAvatar(delegateDetails?.avatar ?? '', Number(action.delegateAccountID));
}

// If this is a report preview, display names and avatars of both people involved
let secondaryAvatar = {};
let secondaryAvatar: SubAvatar;
const primaryDisplayName = displayName;
if (displayAllActors) {
// The ownerAccountID and actorAccountID can be the same if the a user requests money back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice
const secondaryAccountId = props.iouReport.ownerAccountID === actorAccountID ? props.iouReport.managerID : props.iouReport.ownerAccountID;
const secondaryUserDetails = personalDetails[secondaryAccountId] || {};
const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID ? iouReport?.managerID : iouReport?.ownerAccountID;
const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? '';
const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId);
displayName = `${primaryDisplayName} & ${secondaryDisplayName}`;
secondaryAvatar = {
source: UserUtils.getAvatar(secondaryUserDetails.avatar, secondaryAccountId),
source: UserUtils.getAvatar(secondaryUserAvatar, secondaryAccountId),
type: CONST.ICON_TYPE_AVATAR,
name: secondaryDisplayName,
name: secondaryDisplayName ?? '',
id: secondaryAccountId,
};
} else if (!isWorkspaceActor) {
const avatarIconIndex = props.report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(props.report) ? 0 : 1;
const reportIcons = ReportUtils.getIcons(props.report, {});
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const avatarIconIndex = report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(report) ? 0 : 1;
const reportIcons = ReportUtils.getIcons(report, {});

secondaryAvatar = reportIcons[avatarIconIndex];
} else {
secondaryAvatar = {name: '', source: '', type: 'avatar'};
}
const icon = {source: avatarSource, type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR, name: primaryDisplayName, id: isWorkspaceActor ? '' : actorAccountID};
const icon = {
source: avatarSource,
type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR,
name: primaryDisplayName ?? '',
id: isWorkspaceActor ? '' : actorAccountID,
};

// Since the display name for a report action message is delivered with the report history as an array of fragments
// we'll need to take the displayName from personal details and have it be in the same format for now. Eventually,
Expand All @@ -138,29 +159,29 @@ function ReportActionItemSingle(props) {
text: displayName,
},
]
: props.action.person;
: action.person;

Copy link
Contributor

Choose a reason for hiding this comment

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

here.

const reportID = props.report && props.report.reportID;
const iouReportID = props.iouReport && props.iouReport.reportID;
const reportID = report?.reportID;
const iouReportID = iouReport?.reportID;

const showActorDetails = useCallback(() => {
if (isWorkspaceActor) {
showWorkspaceDetails(reportID);
} else {
// Show participants page IOU report preview
if (displayAllActors) {
if (iouReportID && displayAllActors) {
Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID));
return;
}
showUserDetails(props.action.delegateAccountID ? props.action.delegateAccountID : actorAccountID);
showUserDetails(action.delegateAccountID ? action.delegateAccountID : String(actorAccountID));
}
}, [isWorkspaceActor, reportID, actorAccountID, props.action.delegateAccountID, iouReportID, displayAllActors]);
}, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]);

const shouldDisableDetailPage = useMemo(
() =>
actorAccountID === CONST.ACCOUNT_ID.NOTIFICATIONS ||
(!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(props.action.delegateAccountID ? props.action.delegateAccountID : actorAccountID)),
[props.action, isWorkspaceActor, actorAccountID],
(!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)),
[action, isWorkspaceActor, actorAccountID],
);

const getAvatar = () => {
Expand All @@ -170,25 +191,23 @@ function ReportActionItemSingle(props) {
icons={[icon, secondaryAvatar]}
isInReportAction
shouldShowTooltip
secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(theme.appBG), props.isHovered ? StyleUtils.getBackgroundAndBorderStyle(theme.hoverComponentBG) : undefined]}
secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(theme.appBG), isHovered ? StyleUtils.getBackgroundAndBorderStyle(theme.hoverComponentBG) : undefined]}
/>
);
}
if (props.shouldShowSubscriptAvatar) {
if (shouldShowSubscriptAvatar) {
return (
<SubscriptAvatar
mainAvatar={icon}
secondaryAvatar={secondaryAvatar}
mainTooltip={actorHint}
secondaryTooltip={ReportUtils.getPolicyName(props.report)}
noMargin
/>
);
}
return (
<UserDetailsTooltip
accountID={actorAccountID}
delegateAccountID={props.action.delegateAccountID}
delegateAccountID={action.delegateAccountID}
icon={icon}
>
<View>
Expand All @@ -203,13 +222,13 @@ function ReportActionItemSingle(props) {
</UserDetailsTooltip>
);
};
const hasEmojiStatus = !displayAllActors && status && status.emojiCode;
const formattedDate = DateUtils.getStatusUntilDate(lodashGet(status, 'clearAfter'));
const statusText = lodashGet(status, 'text', '');
const hasEmojiStatus = !displayAllActors && status?.emojiCode;
const formattedDate = DateUtils.getStatusUntilDate(status?.clearAfter ?? '');
const statusText = status?.text ?? '';
const statusTooltipText = formattedDate ? `${statusText ? `${statusText} ` : ''}(${formattedDate})` : statusText;

return (
<View style={[styles.chatItem, props.wrapperStyle]}>
<View style={[styles.chatItem, wrapperStyle]}>
<PressableWithoutFeedback
style={[styles.alignSelfStart, styles.mr3]}
onPressIn={ControlSelection.block}
Expand All @@ -219,10 +238,10 @@ function ReportActionItemSingle(props) {
accessibilityLabel={actorHint}
role={CONST.ROLE.BUTTON}
>
<OfflineWithFeedback pendingAction={lodashGet(pendingFields, 'avatar', null)}>{getAvatar()}</OfflineWithFeedback>
<OfflineWithFeedback pendingAction={pendingFields?.avatar ?? undefined}>{getAvatar()}</OfflineWithFeedback>
</PressableWithoutFeedback>
<View style={[styles.chatItemRight]}>
{props.showHeader ? (
{showHeader ? (
<View style={[styles.chatItemMessageHeader]}>
<PressableWithoutFeedback
style={[styles.flexShrink1, styles.mr1]}
Expand All @@ -233,12 +252,13 @@ function ReportActionItemSingle(props) {
accessibilityLabel={actorHint}
role={CONST.ROLE.BUTTON}
>
{_.map(personArray, (fragment, index) => (
{personArray?.map((fragment, index) => (
<ReportActionItemFragment
key={`person-${props.action.reportActionID}-${index}`}
// eslint-disable-next-line react/no-array-index-key
key={`person-${action.reportActionID}-${index}`}
accountID={actorAccountID}
fragment={fragment}
delegateAccountID={props.action.delegateAccountID}
Copy link
Contributor

@ishpaul777 ishpaul777 Jan 4, 2024

Choose a reason for hiding this comment

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

Hii @blazejkustram i am working on ReportActionItemFragment this fragment prop is inferred as person type from here type while we the type we need is Message, wondering what should be solution here.. Can you take a look please?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you post a draft PR so I can check it out? @ishpaul777

Copy link
Contributor

Choose a reason for hiding this comment

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

#33958, Its not 100% complete still need clean up and testing

delegateAccountID={action.delegateAccountID}
isSingleLine
actorIcon={icon}
/>
Expand All @@ -249,20 +269,18 @@ function ReportActionItemSingle(props) {
<Text
style={styles.userReportStatusEmoji}
numberOfLines={1}
>{`${status.emojiCode}`}</Text>
>{`${status?.emojiCode}`}</Text>
</Tooltip>
)}
<ReportActionItemDate created={props.action.created} />
<ReportActionItemDate created={action.created} />
</View>
) : null}
<View style={props.hasBeenFlagged ? styles.blockquote : {}}>{props.children}</View>
<View style={hasBeenFlagged ? styles.blockquote : {}}>{children}</View>
</View>
</View>
);
}

ReportActionItemSingle.propTypes = propTypes;
ReportActionItemSingle.defaultProps = defaultProps;
ReportActionItemSingle.displayName = 'ReportActionItemSingle';

export default withLocalize(ReportActionItemSingle);
export default ReportActionItemSingle;
15 changes: 13 additions & 2 deletions src/types/onyx/PersonalDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ type Timezone = {
automatic?: boolean;
};

type Status = {
/** The emoji code of the status */
emojiCode: string;

/** The text of the draft status */
text?: string;

/** The timestamp of when the status should be cleared */
clearAfter: string; // ISO 8601 format;
};

type PersonalDetails = {
/** ID of the current user from their personal details */
accountID: number;
Expand Down Expand Up @@ -70,11 +81,11 @@ type PersonalDetails = {
fallbackIcon?: string;

/** Status of the current user from their personal details */
status?: string;
status?: Status;
};

type PersonalDetailsList = Record<string, PersonalDetails | null>;

export default PersonalDetails;

export type {Timezone, SelectedTimezone, PersonalDetailsList};
export type {Timezone, Status, SelectedTimezone, PersonalDetailsList};
Loading