From 936e86161b5f6e7662f6863480e5cde8b00dfb5c Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 18 Dec 2023 18:04:32 +0700 Subject: [PATCH 01/28] update logic to store last message text --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 24e795919649..8c6402e09b03 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1186,7 +1186,7 @@ function formatReportLastMessageText(lastMessageText: string, isModifiedExpenseM if (isModifiedExpenseMessage) { return String(lastMessageText).trim().replace(CONST.REGEX.LINE_BREAK, '').trim(); } - return String(lastMessageText).trim().replace(CONST.REGEX.AFTER_FIRST_LINE_BREAK, '').substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(); + return String(lastMessageText).trim().replace(CONST.REGEX.LINE_BREAK, ' ').substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(); } /** From ee93ba6a6e50b539f552648bbf68bdef3006b60a Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 22 Dec 2023 16:40:32 +0800 Subject: [PATCH 02/28] don't focus if a popover is visible --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 8def3a53ca0d..7d6c00ddaf6d 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -37,6 +37,7 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {defaultProps, propTypes} from './composerWithSuggestionsProps'; +import { PopoverContext } from '@components/PopoverProvider'; const {RNTextInputReset} = NativeModules; @@ -103,6 +104,7 @@ function ComposerWithSuggestions({ // For testing children, }) { + const {isOpen: isPopoverOpen} = React.useContext(PopoverContext); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -399,8 +401,11 @@ function ComposerWithSuggestions({ * @memberof ReportActionCompose */ const focus = useCallback((shouldDelay = false) => { + if (isPopoverOpen) { + return; + } focusComposerWithDelay(textInputRef.current)(shouldDelay); - }, []); + }, [isPopoverOpen]); const setUpComposeFocusManager = useCallback(() => { // This callback is used in the contextMenuActions to manage giving focus back to the compose input. From 7c64b0fd1daa0fbb6e1dec784d50e355d097c4b0 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 22 Dec 2023 16:51:20 +0800 Subject: [PATCH 03/28] prettier --- .../ComposerWithSuggestions.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 7d6c00ddaf6d..4de6201d6f6e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -5,6 +5,7 @@ import {findNodeHandle, NativeModules, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Composer from '@components/Composer'; +import {PopoverContext} from '@components/PopoverProvider'; import withKeyboardState from '@components/withKeyboardState'; import useDebounce from '@hooks/useDebounce'; import useLocalize from '@hooks/useLocalize'; @@ -37,7 +38,6 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {defaultProps, propTypes} from './composerWithSuggestionsProps'; -import { PopoverContext } from '@components/PopoverProvider'; const {RNTextInputReset} = NativeModules; @@ -400,12 +400,15 @@ function ComposerWithSuggestions({ * @param {Boolean} [shouldDelay=false] Impose delay before focusing the composer * @memberof ReportActionCompose */ - const focus = useCallback((shouldDelay = false) => { - if (isPopoverOpen) { - return; - } - focusComposerWithDelay(textInputRef.current)(shouldDelay); - }, [isPopoverOpen]); + const focus = useCallback( + (shouldDelay = false) => { + if (isPopoverOpen) { + return; + } + focusComposerWithDelay(textInputRef.current)(shouldDelay); + }, + [isPopoverOpen], + ); const setUpComposeFocusManager = useCallback(() => { // This callback is used in the contextMenuActions to manage giving focus back to the compose input. From d0d9d2af9a4cee024168a9b973f4f3ae23617564 Mon Sep 17 00:00:00 2001 From: Sam Hariri <137707942+samh-nl@users.noreply.github.com> Date: Fri, 22 Dec 2023 14:13:33 +0100 Subject: [PATCH 04/28] fix: allow empty draft messages --- src/components/LHNOptionsList/OptionRowLHN.js | 2 +- src/components/ShowContextMenuContext.js | 2 +- src/libs/actions/Report.ts | 9 ++++- .../report/ContextMenu/ContextMenuActions.js | 8 ++++- .../ContextMenu/ReportActionContextMenu.ts | 2 +- ...genericReportActionContextMenuPropTypes.js | 2 +- src/pages/home/report/ReportActionItem.js | 34 +++++++++---------- .../report/ReportActionItemMessageEdit.js | 12 ++----- src/types/onyx/ReportActionsDrafts.ts | 6 +++- 9 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 59a392ff4e67..a4f4d8246caf 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -135,7 +135,7 @@ function OptionRowLHN(props) { props.reportID, '0', props.reportID, - '', + undefined, () => {}, () => setIsContextMenuActive(false), false, diff --git a/src/components/ShowContextMenuContext.js b/src/components/ShowContextMenuContext.js index 28822451956d..04ccd5002b60 100644 --- a/src/components/ShowContextMenuContext.js +++ b/src/components/ShowContextMenuContext.js @@ -35,7 +35,7 @@ function showContextMenuForReport(event, anchor, reportID, action, checkIfContex reportID, action.reportActionID, ReportUtils.getOriginalReportID(reportID, action), - '', + undefined, checkIfContextMenuActive, checkIfContextMenuActive, isArchivedRoom, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 1ed54c826e40..6c9aacedaad4 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1345,10 +1345,16 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry Report.saveReportActionDraft(reportID, reportAction, _.isEmpty(draftMessage) ? getActionText(reportAction) : ''); + const editAction = () => { + if (_.isUndefined(draftMessage)) { + Report.saveReportActionDraft(reportID, reportAction, getActionText(reportAction)); + } else { + Report.deleteReportActionDraft(reportID, reportAction); + } + }; if (closePopover) { // Hide popover, then call editAction diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index b269bc276b55..1e1fc700d8e0 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -98,7 +98,7 @@ function showContextMenu( reportID = '0', reportActionID = '0', originalReportID = '0', - draftMessage = '', + draftMessage = undefined, onShow = () => {}, onHide = () => {}, isArchivedRoom = false, diff --git a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js b/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js index 3d8667e44e62..b9f892c1b9ff 100644 --- a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js +++ b/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js @@ -28,7 +28,7 @@ const defaultProps = { isMini: false, isVisible: false, selection: '', - draftMessage: '', + draftMessage: undefined, }; export {propTypes, defaultProps}; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index d37f84e0c908..73858da894e1 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -119,7 +119,7 @@ const propTypes = { }; const defaultProps = { - draftMessage: '', + draftMessage: undefined, preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, emojiReactions: {}, shouldShowSubscriptAvatar: false, @@ -202,7 +202,7 @@ function ReportActionItem(props) { }, [isDeletedParentAction, props.action.reportActionID]); useEffect(() => { - if (prevDraftMessage || !props.draftMessage) { + if (!_.isUndefined(prevDraftMessage) || _.isUndefined(props.draftMessage)) { return; } @@ -224,10 +224,10 @@ function ReportActionItem(props) { }, [props.action, props.report.reportID]); useEffect(() => { - if (!props.draftMessage || !ReportActionsUtils.isDeletedAction(props.action)) { + if (_.isUndefined(props.draftMessage) || !ReportActionsUtils.isDeletedAction(props.action)) { return; } - Report.saveReportActionDraft(props.report.reportID, props.action, ''); + Report.deleteReportActionDraft(props.report.reportID, props.action); }, [props.draftMessage, props.action, props.report.reportID]); // Hide the message if it is being moderated for a higher offense, or is hidden by a moderator @@ -265,7 +265,7 @@ function ReportActionItem(props) { const showPopover = useCallback( (event) => { // Block menu on the message being Edited or if the report action item has errors - if (props.draftMessage || !_.isEmpty(props.action.errors)) { + if (!_.isUndefined(props.draftMessage) || !_.isEmpty(props.action.errors)) { return; } @@ -428,7 +428,7 @@ function ReportActionItem(props) { const hasBeenFlagged = !_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], moderationDecision); children = ( - {!props.draftMessage ? ( + {_.isUndefined(props.draftMessage) ? ( Number(accountID)); - const draftMessageRightAlign = props.draftMessage ? styles.chatItemReactionsDraftRight : {}; + const draftMessageRightAlign = !_.isUndefined(props.draftMessage) ? styles.chatItemReactionsDraftRight : {}; return ( <> {children} {Permissions.canUseLinkPreviews() && !isHidden && !_.isEmpty(props.action.linkMetadata) && ( - + !_.isEmpty(item))} /> )} @@ -544,7 +544,7 @@ function ReportActionItem(props) { const renderReportActionItem = (hovered, isWhisper, hasErrors) => { const content = renderItemContent(hovered || isContextMenuActive, isWhisper, hasErrors); - if (props.draftMessage) { + if (!_.isUndefined(props.draftMessage)) { return {content}; } @@ -552,7 +552,7 @@ function ReportActionItem(props) { return ( ${props.translate('parentReportAction.deletedTask')}`} /> @@ -666,13 +666,13 @@ function ReportActionItem(props) { onPressIn={() => props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onSecondaryInteraction={showPopover} - preventDefaultContextMenu={!props.draftMessage && !hasErrors} + preventDefaultContextMenu={_.isUndefined(props.draftMessage) && !hasErrors} withoutFocusOnSecondaryInteraction accessibilityLabel={props.translate('accessibilityHints.chatMessage')} > {(hovered) => ( @@ -683,14 +683,14 @@ function ReportActionItem(props) { originalReportID={originalReportID} isArchivedRoom={ReportUtils.isArchivedRoom(props.report)} displayAsGroup={props.displayAsGroup} - isVisible={hovered && !props.draftMessage && !hasErrors} + isVisible={hovered && _.isUndefined(props.draftMessage) && !hasErrors} draftMessage={props.draftMessage} isChronosReport={ReportUtils.chatIncludesChronos(originalReport)} /> - + ReportActions.clearReportActionErrors(props.report.reportID, props.action)} - pendingAction={props.draftMessage ? null : props.action.pendingAction} + pendingAction={!_.isUndefined(props.draftMessage) ? null : props.action.pendingAction} shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)} errors={props.action.errors} errorRowStyles={[styles.ml10, styles.mr2]} @@ -745,7 +745,7 @@ export default compose( transformValue: (drafts, props) => { const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; - return lodashGet(drafts, [draftKey, props.action.reportActionID], ''); + return lodashGet(drafts, [draftKey, props.action.reportActionID, 'message']); }, }), withOnyx({ diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 55b031c198e0..5cad80711bcb 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -270,14 +270,8 @@ function ReportActionItemMessageEdit(props) { draftRef.current = newDraft; - // This component is rendered only when draft is set to a non-empty string. In order to prevent component - // unmount when user deletes content of textarea, we set previous message instead of empty string. - if (newDraft.trim().length > 0) { - // We want to escape the draft message to differentiate the HTML from the report action and the HTML the user drafted. - debouncedSaveDraft(_.escape(newDraft)); - } else { - debouncedSaveDraft(props.action.message[0].html); - } + // We want to escape the draft message to differentiate the HTML from the report action and the HTML the user drafted. + debouncedSaveDraft(_.escape(newDraft)); }, [props.action.message, debouncedSaveDraft, debouncedUpdateFrequentlyUsedEmojis, props.preferredSkinTone, preferredLocale, selection.end], ); @@ -292,7 +286,7 @@ function ReportActionItemMessageEdit(props) { */ const deleteDraft = useCallback(() => { debouncedSaveDraft.cancel(); - Report.saveReportActionDraft(props.reportID, props.action, ''); + Report.deleteReportActionDraft(props.reportID, props.action); if (isActive()) { ReportActionComposeFocusManager.clear(); diff --git a/src/types/onyx/ReportActionsDrafts.ts b/src/types/onyx/ReportActionsDrafts.ts index e40007b6b47a..34ccc977ef48 100644 --- a/src/types/onyx/ReportActionsDrafts.ts +++ b/src/types/onyx/ReportActionsDrafts.ts @@ -1,3 +1,7 @@ -type ReportActionsDrafts = Record; +type ReportActionsDraft = { + message: string; +}; + +type ReportActionsDrafts = Record; export default ReportActionsDrafts; From e7c762a9c6b8dfe3b9effbe524183ba4e1c7e215 Mon Sep 17 00:00:00 2001 From: Sam Hariri <137707942+samh-nl@users.noreply.github.com> Date: Fri, 22 Dec 2023 14:21:13 +0100 Subject: [PATCH 05/28] fix: migrate empty report actions drafts --- src/libs/migrateOnyx.js | 3 +- .../RemoveEmptyReportActionsDrafts.js | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/libs/migrations/RemoveEmptyReportActionsDrafts.js diff --git a/src/libs/migrateOnyx.js b/src/libs/migrateOnyx.js index 5daba3686208..9b8b4056e3e5 100644 --- a/src/libs/migrateOnyx.js +++ b/src/libs/migrateOnyx.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import Log from './Log'; import KeyReportActionsDraftByReportActionID from './migrations/KeyReportActionsDraftByReportActionID'; import PersonalDetailsByAccountID from './migrations/PersonalDetailsByAccountID'; +import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportActionsDrafts'; import RenameReceiptFilename from './migrations/RenameReceiptFilename'; import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection'; @@ -11,7 +12,7 @@ export default function () { return new Promise((resolve) => { // Add all migrations to an array so they are executed in order - const migrationPromises = [PersonalDetailsByAccountID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection]; + const migrationPromises = [PersonalDetailsByAccountID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the // previous promise to finish before moving onto the next one. diff --git a/src/libs/migrations/RemoveEmptyReportActionsDrafts.js b/src/libs/migrations/RemoveEmptyReportActionsDrafts.js new file mode 100644 index 000000000000..586cca8ca1ed --- /dev/null +++ b/src/libs/migrations/RemoveEmptyReportActionsDrafts.js @@ -0,0 +1,69 @@ +import Onyx from 'react-native-onyx'; +import _ from 'underscore'; +import Log from '@libs/Log'; +import ONYXKEYS from '@src/ONYXKEYS'; + +/** + * This migration removes empty drafts from reportActionsDrafts, which was previously used to mark a draft as being non-existent (e.g. upon cancel). + * + * @returns {Promise} + */ +export default function () { + return new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, + waitForCollectionCallback: true, + callback: (allReportActionsDrafts) => { + Onyx.disconnect(connectionID); + + if (!allReportActionsDrafts) { + Log.info('[Migrate Onyx] Skipped migration RemoveEmptyReportActionsDrafts because there were no reportActionsDrafts'); + return resolve(); + } + + const newReportActionsDrafts = {}; + _.each(allReportActionsDrafts, (reportActionDrafts, onyxKey) => { + newReportActionsDrafts[onyxKey] = {}; + + // Whether there is at least one draft in this report that has to be migrated + let hasUnmigratedDraft = false; + + _.each(reportActionDrafts, (reportActionDraft, reportActionID) => { + // If the draft is a string, it means it hasn't been migrated yet + if (_.isString(reportActionDraft)) { + hasUnmigratedDraft = true; + Log.info(`[Migrate Onyx] Migrating draft for report action ${reportActionID}`); + + if (_.isEmpty(reportActionDraft)) { + Log.info(`[Migrate Onyx] Removing draft for report action ${reportActionID}`); + return; + } + + newReportActionsDrafts[onyxKey][reportActionID] = {message: reportActionDraft}; + } else { + // We've already migrated this draft, so keep the existing value + newReportActionsDrafts[onyxKey][reportActionID] = reportActionDraft; + } + }); + + if (_.isEmpty(newReportActionsDrafts[onyxKey])) { + // Clear if there are no drafts remaining + newReportActionsDrafts[onyxKey] = null; + } else if (!hasUnmigratedDraft) { + // All drafts for this report have already been migrated, there's no need to overwrite this onyx key with the same data + delete newReportActionsDrafts[onyxKey]; + } + }); + + if (_.isEmpty(newReportActionsDrafts)) { + Log.info('[Migrate Onyx] Skipped migration RemoveEmptyReportActionsDrafts because there are no actions drafts to migrate'); + return resolve(); + } + + Log.info(`[Migrate Onyx] Ran migration RemoveEmptyReportActionsDrafts and updated drafts for ${_.keys(newReportActionsDrafts).length} reports`); + // eslint-disable-next-line rulesdir/prefer-actions-set-data + return Onyx.multiSet(newReportActionsDrafts).then(resolve); + }, + }); + }); +} From 374dd6cdfcb3645d107a9cea1e1083c7bc718723 Mon Sep 17 00:00:00 2001 From: Sam Hariri <137707942+samh-nl@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:46:59 +0100 Subject: [PATCH 06/28] fix: change empty string to undefined --- .../home/report/ContextMenu/PopoverReportActionContextMenu.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index 7f60b9d9b4d5..2bae8227ffde 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -17,7 +17,7 @@ function PopoverReportActionContextMenu(_props, ref) { const reportActionIDRef = useRef('0'); const originalReportIDRef = useRef('0'); const selectionRef = useRef(''); - const reportActionDraftMessageRef = useRef(''); + const reportActionDraftMessageRef = useRef(undefined); const cursorRelativePosition = useRef({ horizontal: 0, @@ -226,7 +226,7 @@ function PopoverReportActionContextMenu(_props, ref) { } selectionRef.current = ''; - reportActionDraftMessageRef.current = ''; + reportActionDraftMessageRef.current = undefined; setIsPopoverVisible(false); }; From 285a312486f85ba4794a0b9a87fe80a00306bc6d Mon Sep 17 00:00:00 2001 From: Sam Hariri <137707942+samh-nl@users.noreply.github.com> Date: Fri, 22 Dec 2023 17:50:41 +0100 Subject: [PATCH 07/28] style: remove unnecessary dependency --- src/pages/home/report/ReportActionItemMessageEdit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 5cad80711bcb..06cccc607461 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -273,7 +273,7 @@ function ReportActionItemMessageEdit(props) { // We want to escape the draft message to differentiate the HTML from the report action and the HTML the user drafted. debouncedSaveDraft(_.escape(newDraft)); }, - [props.action.message, debouncedSaveDraft, debouncedUpdateFrequentlyUsedEmojis, props.preferredSkinTone, preferredLocale, selection.end], + [debouncedSaveDraft, debouncedUpdateFrequentlyUsedEmojis, props.preferredSkinTone, preferredLocale, selection.end], ); useEffect(() => { From 97a1be4cfd33b4d64313f01e828d1c7299a1dc55 Mon Sep 17 00:00:00 2001 From: Sam Hariri <137707942+samh-nl@users.noreply.github.com> Date: Sat, 23 Dec 2023 11:27:15 +0100 Subject: [PATCH 08/28] refactor: migrate to typescript --- .../RemoveEmptyReportActionsDrafts.js | 69 ----------------- .../RemoveEmptyReportActionsDrafts.ts | 76 +++++++++++++++++++ src/types/onyx/ReportActionsDraft.ts | 7 ++ src/types/onyx/ReportActionsDrafts.ts | 4 +- src/types/onyx/index.ts | 2 + 5 files changed, 86 insertions(+), 72 deletions(-) delete mode 100644 src/libs/migrations/RemoveEmptyReportActionsDrafts.js create mode 100644 src/libs/migrations/RemoveEmptyReportActionsDrafts.ts create mode 100644 src/types/onyx/ReportActionsDraft.ts diff --git a/src/libs/migrations/RemoveEmptyReportActionsDrafts.js b/src/libs/migrations/RemoveEmptyReportActionsDrafts.js deleted file mode 100644 index 586cca8ca1ed..000000000000 --- a/src/libs/migrations/RemoveEmptyReportActionsDrafts.js +++ /dev/null @@ -1,69 +0,0 @@ -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import Log from '@libs/Log'; -import ONYXKEYS from '@src/ONYXKEYS'; - -/** - * This migration removes empty drafts from reportActionsDrafts, which was previously used to mark a draft as being non-existent (e.g. upon cancel). - * - * @returns {Promise} - */ -export default function () { - return new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, - waitForCollectionCallback: true, - callback: (allReportActionsDrafts) => { - Onyx.disconnect(connectionID); - - if (!allReportActionsDrafts) { - Log.info('[Migrate Onyx] Skipped migration RemoveEmptyReportActionsDrafts because there were no reportActionsDrafts'); - return resolve(); - } - - const newReportActionsDrafts = {}; - _.each(allReportActionsDrafts, (reportActionDrafts, onyxKey) => { - newReportActionsDrafts[onyxKey] = {}; - - // Whether there is at least one draft in this report that has to be migrated - let hasUnmigratedDraft = false; - - _.each(reportActionDrafts, (reportActionDraft, reportActionID) => { - // If the draft is a string, it means it hasn't been migrated yet - if (_.isString(reportActionDraft)) { - hasUnmigratedDraft = true; - Log.info(`[Migrate Onyx] Migrating draft for report action ${reportActionID}`); - - if (_.isEmpty(reportActionDraft)) { - Log.info(`[Migrate Onyx] Removing draft for report action ${reportActionID}`); - return; - } - - newReportActionsDrafts[onyxKey][reportActionID] = {message: reportActionDraft}; - } else { - // We've already migrated this draft, so keep the existing value - newReportActionsDrafts[onyxKey][reportActionID] = reportActionDraft; - } - }); - - if (_.isEmpty(newReportActionsDrafts[onyxKey])) { - // Clear if there are no drafts remaining - newReportActionsDrafts[onyxKey] = null; - } else if (!hasUnmigratedDraft) { - // All drafts for this report have already been migrated, there's no need to overwrite this onyx key with the same data - delete newReportActionsDrafts[onyxKey]; - } - }); - - if (_.isEmpty(newReportActionsDrafts)) { - Log.info('[Migrate Onyx] Skipped migration RemoveEmptyReportActionsDrafts because there are no actions drafts to migrate'); - return resolve(); - } - - Log.info(`[Migrate Onyx] Ran migration RemoveEmptyReportActionsDrafts and updated drafts for ${_.keys(newReportActionsDrafts).length} reports`); - // eslint-disable-next-line rulesdir/prefer-actions-set-data - return Onyx.multiSet(newReportActionsDrafts).then(resolve); - }, - }); - }); -} diff --git a/src/libs/migrations/RemoveEmptyReportActionsDrafts.ts b/src/libs/migrations/RemoveEmptyReportActionsDrafts.ts new file mode 100644 index 000000000000..d8816198e537 --- /dev/null +++ b/src/libs/migrations/RemoveEmptyReportActionsDrafts.ts @@ -0,0 +1,76 @@ +import _ from 'lodash'; +import Onyx, {OnyxEntry} from 'react-native-onyx'; +import Log from '@libs/Log'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {ReportActionsDraft, ReportActionsDrafts} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type ReportActionsDraftsKey = `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`; + +/** + * This migration removes empty drafts from reportActionsDrafts, which was previously used to mark a draft as being non-existent (e.g. upon cancel). + */ +export default function (): Promise { + return new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, + waitForCollectionCallback: true, + callback: (allReportActionsDrafts) => { + Onyx.disconnect(connectionID); + + if (!allReportActionsDrafts) { + Log.info('[Migrate Onyx] Skipped migration RemoveEmptyReportActionsDrafts because there were no reportActionsDrafts'); + return resolve(); + } + + const newReportActionsDrafts: Record> = {}; + Object.entries(allReportActionsDrafts).forEach(([onyxKey, reportActionDrafts]) => { + const newReportActionsDraftsForReport: Record = {}; + + // Whether there is at least one draft in this report that has to be migrated + let hasUnmigratedDraft = false; + + if (reportActionDrafts) { + Object.entries(reportActionDrafts).forEach(([reportActionID, reportActionDraft]) => { + // If the draft is a string, it means it hasn't been migrated yet + if (typeof reportActionDraft === 'string') { + hasUnmigratedDraft = true; + Log.info(`[Migrate Onyx] Migrating draft for report action ${reportActionID}`); + + if (_.isEmpty(reportActionDraft)) { + Log.info(`[Migrate Onyx] Removing draft for report action ${reportActionID}`); + return; + } + + newReportActionsDraftsForReport[reportActionID] = {message: reportActionDraft}; + } else { + // We've already migrated this draft, so keep the existing value + newReportActionsDraftsForReport[reportActionID] = reportActionDraft; + } + }); + } + + if (isEmptyObject(newReportActionsDraftsForReport)) { + Log.info('[Migrate Onyx] NO REMAINING'); + // Clear if there are no drafts remaining + newReportActionsDrafts[onyxKey as ReportActionsDraftsKey] = null; + } else if (hasUnmigratedDraft) { + // Only migrate if there are unmigrated drafts, there's no need to overwrite this onyx key with the same data + newReportActionsDrafts[onyxKey as ReportActionsDraftsKey] = newReportActionsDraftsForReport; + } + }); + + if (isEmptyObject(newReportActionsDrafts)) { + Log.info('[Migrate Onyx] Skipped migration RemoveEmptyReportActionsDrafts because there are no actions drafts to migrate'); + return resolve(); + } + + Log.info(`[Migrate Onyx] Updating drafts for ${Object.keys(newReportActionsDrafts).length} reports`); + Onyx.multiSet(newReportActionsDrafts).then(() => { + Log.info('[Migrate Onyx] Ran migration RemoveEmptyReportActionsDrafts successfully'); + resolve(); + }); + }, + }); + }); +} diff --git a/src/types/onyx/ReportActionsDraft.ts b/src/types/onyx/ReportActionsDraft.ts new file mode 100644 index 000000000000..41a701f16e71 --- /dev/null +++ b/src/types/onyx/ReportActionsDraft.ts @@ -0,0 +1,7 @@ +type ReportActionsDraft = + | { + message: string; + } + | string; + +export default ReportActionsDraft; diff --git a/src/types/onyx/ReportActionsDrafts.ts b/src/types/onyx/ReportActionsDrafts.ts index 34ccc977ef48..ad2782111144 100644 --- a/src/types/onyx/ReportActionsDrafts.ts +++ b/src/types/onyx/ReportActionsDrafts.ts @@ -1,6 +1,4 @@ -type ReportActionsDraft = { - message: string; -}; +import ReportActionsDraft from './ReportActionsDraft'; type ReportActionsDrafts = Record; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 229fd0a53158..45c47793c458 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -37,6 +37,7 @@ import ReimbursementAccountDraft from './ReimbursementAccountDraft'; import Report from './Report'; import ReportAction, {ReportActions} from './ReportAction'; import ReportActionReactions from './ReportActionReactions'; +import ReportActionsDraft from './ReportActionsDraft'; import ReportActionsDrafts from './ReportActionsDrafts'; import ReportMetadata from './ReportMetadata'; import ReportNextStep from './ReportNextStep'; @@ -107,6 +108,7 @@ export type { ReportAction, ReportActionReactions, ReportActions, + ReportActionsDraft, ReportActionsDrafts, ReportMetadata, ReportNextStep, From 95915b6afae4eaecc2592479ba619080cececc44 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Sat, 23 Dec 2023 18:15:02 +0100 Subject: [PATCH 09/28] Migrate 'ReportActionItemSingle.js' component to TypeScript --- ...emSingle.js => ReportActionItemSingle.tsx} | 137 +++++++++--------- 1 file changed, 66 insertions(+), 71 deletions(-) rename src/pages/home/report/{ReportActionItemSingle.js => ReportActionItemSingle.tsx} (66%) diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.tsx similarity index 66% rename from src/pages/home/report/ReportActionItemSingle.js rename to src/pages/home/report/ReportActionItemSingle.tsx index 5737d876779f..4b5f87df745a 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -1,7 +1,6 @@ import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useMemo} from 'react'; -import {View} from 'react-native'; +import {StyleProp, View, ViewStyle} from 'react-native'; import _ from 'underscore'; import Avatar from '@components/Avatar'; import MultipleAvatars from '@components/MultipleAvatars'; @@ -12,7 +11,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'; @@ -21,88 +20,86 @@ 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 ChildrenProps from '@src/types/utils/ChildrenProps'; 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; /** Children view component for this action item */ - children: PropTypes.node.isRequired, + children: ChildrenProps; /** 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, -}; - -const defaultProps = { - wrapperStyle: undefined, - showHeader: true, - shouldShowSubscriptAvatar: false, - hasBeenFlagged: false, - report: undefined, - iouReport: undefined, - isHovered: false, + isHovered: boolean; }; -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 {translate} = useLocalize(); 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 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] || {}; + const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] || {}; 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 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 @@ -110,38 +107,38 @@ function ReportActionItemSingle(props) { 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 secondaryUserDetails = personalDetails[secondaryAccountId ?? -1] || {}; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; secondaryAvatar = { - source: UserUtils.getAvatar(secondaryUserDetails.avatar, secondaryAccountId), + source: UserUtils.getAvatar(secondaryUserDetails.avatar ?? '', secondaryAccountId), type: CONST.ICON_TYPE_AVATAR, name: secondaryDisplayName, id: secondaryAccountId, }; } else if (!isWorkspaceActor) { - const avatarIconIndex = props.report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(props.report) ? 0 : 1; - const reportIcons = ReportUtils.getIcons(props.report, {}); + const avatarIconIndex = report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(report) ? 0 : 1; + const reportIcons = ReportUtils.getIcons(report, {}); secondaryAvatar = reportIcons[avatarIconIndex]; } - 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, // we should stop referring to the report history items entirely for this information. const personArray = displayName - ? [ + ?? [ { type: 'TEXT', text: displayName, }, ] - : props.action.person; + ?? action.person ?? {}; - const reportID = props.report && props.report.reportID; - const iouReportID = props.iouReport && props.iouReport.reportID; + const reportID = report && report.reportID; + const iouReportID = iouReport && iouReport.reportID; const showActorDetails = useCallback(() => { if (isWorkspaceActor) { @@ -152,15 +149,15 @@ function ReportActionItemSingle(props) { Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } - showUserDetails(props.action.delegateAccountID ? props.action.delegateAccountID : actorAccountID); + showUserDetails(action.delegateAccountID ?? String(actorAccountID) ?? -1); } - }, [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(Number(action.delegateAccountID ?? actorAccountID) ?? -1)), + [action, isWorkspaceActor, actorAccountID], ); const getAvatar = () => { @@ -170,17 +167,17 @@ 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 ( ); @@ -188,7 +185,7 @@ function ReportActionItemSingle(props) { return ( @@ -209,7 +206,7 @@ function ReportActionItemSingle(props) { const statusTooltipText = formattedDate ? `${statusText} (${formattedDate})` : statusText; return ( - + {getAvatar()} - {props.showHeader ? ( + {showHeader ? ( {_.map(personArray, (fragment, index) => ( @@ -249,20 +246,18 @@ function ReportActionItemSingle(props) { {`${status.emojiCode}`} + >{`${status?.emojiCode}`} )} - + ) : null} - {props.children} + {children} ); } -ReportActionItemSingle.propTypes = propTypes; -ReportActionItemSingle.defaultProps = defaultProps; ReportActionItemSingle.displayName = 'ReportActionItemSingle'; -export default withLocalize(ReportActionItemSingle); +export default ReportActionItemSingle; From e1d5cbbe15da70378970a5a5a7d58e5fe984bb1c Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Sun, 24 Dec 2023 13:51:58 +0100 Subject: [PATCH 10/28] complete remainnig migration of 'ReportActionItemSingle.js' component to TypeScript anf fixing lint errors --- src/components/OfflineWithFeedback.tsx | 2 +- .../home/report/ReportActionItemSingle.tsx | 74 ++++++++++++------- src/types/onyx/PersonalDetails.ts | 13 +++- 3 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 5fcf1fe7442b..33d2131acde8 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -18,7 +18,7 @@ import MessagesRow from './MessagesRow'; type OfflineWithFeedbackProps = ChildrenProps & { /** The type of action that's pending */ - pendingAction: OnyxCommon.PendingAction; + pendingAction?: OnyxCommon.PendingAction | null; /** Determine whether to hide the component's children if deletion is pending */ shouldHideOnDelete?: boolean; diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 4b5f87df745a..12fc915e5bb2 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -1,7 +1,5 @@ -import lodashGet from 'lodash/get'; import React, {useCallback, useMemo} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; -import _ from 'underscore'; import Avatar from '@components/Avatar'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -23,7 +21,7 @@ import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Report, ReportAction} from '@src/types/onyx'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; +import {AvatarType} from '@src/types/onyx/OnyxCommon'; import ReportActionItemDate from './ReportActionItemDate'; import ReportActionItemFragment from './ReportActionItemFragment'; @@ -35,7 +33,7 @@ type ReportActionItemSingleProps = { wrapperStyle?: StyleProp; /** Children view component for this action item */ - children: ChildrenProps; + children: React.ReactNode; /** Report for this action */ report: Report; @@ -56,6 +54,23 @@ type ReportActionItemSingleProps = { isHovered: boolean; }; +type SubAvatar = { + /** Avatar source to display */ + source: UserUtils.AvatarSource; + + /** Denotes whether it is an 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: string) => { Navigation.navigate(ROUTES.PROFILE.getRoute(accountID)); }; @@ -79,13 +94,13 @@ function ReportActionItemSingle({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; + 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 ?? -1] || {}; - let actorHint = (login || displayName || '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); + const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; + 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); + const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID ?? displayAllActors); let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); if (isWorkspaceActor) { @@ -103,42 +118,49 @@ function ReportActionItemSingle({ } // 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 = iouReport.ownerAccountID === actorAccountID ? iouReport.managerID : iouReport.ownerAccountID; - const secondaryUserDetails = personalDetails[secondaryAccountId ?? -1] || {}; + 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 = report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(report) ? 0 : 1; + 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, // we should stop referring to the report history items entirely for this information. const personArray = displayName - ?? [ + ? [ { type: 'TEXT', text: displayName, }, ] - ?? action.person ?? {}; + : action.person; - const reportID = report && report.reportID; - const iouReportID = iouReport && iouReport.reportID; + const reportID = report?.reportID; + const iouReportID = iouReport?.reportID; const showActorDetails = useCallback(() => { if (isWorkspaceActor) { @@ -154,9 +176,7 @@ function ReportActionItemSingle({ }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); const shouldDisableDetailPage = useMemo( - () => - actorAccountID === CONST.ACCOUNT_ID.NOTIFICATIONS || - (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(Number(action.delegateAccountID ?? actorAccountID) ?? -1)), + () => actorAccountID === CONST.ACCOUNT_ID.NOTIFICATIONS ?? (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(Number(action.delegateAccountID ?? actorAccountID) ?? -1)), [action, isWorkspaceActor, actorAccountID], ); @@ -176,8 +196,6 @@ function ReportActionItemSingle({ ); @@ -200,9 +218,9 @@ function ReportActionItemSingle({ ); }; - 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} (${formattedDate})` : statusText; return ( @@ -216,7 +234,7 @@ function ReportActionItemSingle({ accessibilityLabel={actorHint} role={CONST.ROLE.BUTTON} > - {getAvatar()} + {getAvatar()} {showHeader ? ( @@ -230,7 +248,7 @@ function ReportActionItemSingle({ accessibilityLabel={actorHint} role={CONST.ROLE.BUTTON} > - {_.map(personArray, (fragment, index) => ( + {personArray?.map((fragment, index) => ( ; From 54d8b05b5f1c87d84618196b01a838de9de684ac Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Sun, 24 Dec 2023 14:00:58 +0100 Subject: [PATCH 11/28] fix lint errors --- src/libs/ReportUtils.ts | 3 ++- src/pages/home/report/ReportActionItemSingle.tsx | 1 + src/types/onyx/PersonalDetails.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8e0ff19bf3dc..1c0452b6a0fd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -16,6 +16,7 @@ import ROUTES from '@src/ROUTES'; import {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import {IOUMessage, OriginalMessageActionName, OriginalMessageCreated, ReimbursementDeQueuedMessage} 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'; @@ -323,7 +324,7 @@ type OptionData = { subtitle?: string | null; login?: string | null; accountID?: number | null; - status?: string | null; + status?: Status | null; phoneNumber?: string | null; isUnread?: boolean | null; isUnreadWithMention?: boolean | null; diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 12fc915e5bb2..3a6dd3e60544 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -250,6 +250,7 @@ function ReportActionItemSingle({ > {personArray?.map((fragment, index) => ( ; export default PersonalDetails; -export type {Timezone, SelectedTimezone, PersonalDetailsList}; +export type {Timezone, Status, SelectedTimezone, PersonalDetailsList}; From cad00570a743be413cf5b42b181a5d3185119c69 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Sun, 24 Dec 2023 14:09:07 +0100 Subject: [PATCH 12/28] modify pendingAction type --- src/components/OfflineWithFeedback.tsx | 2 +- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 33d2131acde8..586b3cb30b6c 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -18,7 +18,7 @@ import MessagesRow from './MessagesRow'; type OfflineWithFeedbackProps = ChildrenProps & { /** The type of action that's pending */ - pendingAction?: OnyxCommon.PendingAction | null; + pendingAction: OnyxCommon.PendingAction | null; /** Determine whether to hide the component's children if deletion is pending */ shouldHideOnDelete?: boolean; diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 3a6dd3e60544..157868c829c7 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -234,7 +234,7 @@ function ReportActionItemSingle({ accessibilityLabel={actorHint} role={CONST.ROLE.BUTTON} > - {getAvatar()} + {getAvatar()} {showHeader ? ( From 8ea1a83e2e2b2523a1b3828e24f23c6207bc8954 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Sun, 24 Dec 2023 15:47:18 +0100 Subject: [PATCH 13/28] fix failing test --- src/pages/home/report/ReportActionItemSingle.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 157868c829c7..a3a6f84ce662 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -123,17 +123,17 @@ function ReportActionItemSingle({ 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 = iouReport.ownerAccountID === actorAccountID ? iouReport.managerID : iouReport.ownerAccountID; - const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? ''; + const secondaryUserAvatar = secondaryAccountId ? personalDetails?.[secondaryAccountId]?.avatar ?? {} : {}; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; secondaryAvatar = { - source: UserUtils.getAvatar(secondaryUserAvatar ?? '', secondaryAccountId), + source: UserUtils.getAvatar(secondaryUserAvatar, secondaryAccountId), type: CONST.ICON_TYPE_AVATAR, name: secondaryDisplayName ?? '', id: secondaryAccountId, }; } else if (!isWorkspaceActor) { - const avatarIconIndex = report.isOwnPolicyExpenseChat ?? ReportUtils.isPolicyExpenseChat(report) ? 0 : 1; + const avatarIconIndex = report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(report) ? 0 : 1; const reportIcons = ReportUtils.getIcons(report, {}); secondaryAvatar = reportIcons[avatarIconIndex]; @@ -176,7 +176,7 @@ function ReportActionItemSingle({ }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); const shouldDisableDetailPage = useMemo( - () => actorAccountID === CONST.ACCOUNT_ID.NOTIFICATIONS ?? (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(Number(action.delegateAccountID ?? actorAccountID) ?? -1)), + () => actorAccountID === CONST.ACCOUNT_ID.NOTIFICATIONS || (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(Number(action.delegateAccountID ?? actorAccountID) ?? -1)), [action, isWorkspaceActor, actorAccountID], ); From 192ff3a45b55b916c0ef0f1ba69e9961bcd0892d Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Sun, 24 Dec 2023 15:55:14 +0100 Subject: [PATCH 14/28] ignore lint warning --- src/pages/home/report/ReportActionItemSingle.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index a3a6f84ce662..7bb2eb46af0b 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -133,6 +133,7 @@ function ReportActionItemSingle({ id: secondaryAccountId, }; } else if (!isWorkspaceActor) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const avatarIconIndex = report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(report) ? 0 : 1; const reportIcons = ReportUtils.getIcons(report, {}); From c72d4b61acd8665317065c2b451f40e0eae0b78f Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 26 Dec 2023 22:58:19 +0100 Subject: [PATCH 15/28] edit comment Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 7bb2eb46af0b..912df6b634ad 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -58,7 +58,7 @@ type SubAvatar = { /** Avatar source to display */ source: UserUtils.AvatarSource; - /** Denotes whether it is an avatar or a workspace avatar */ + /** Denotes whether it is a user avatar or a workspace avatar */ type: AvatarType; /** Owner of the avatar. If user, displayName. If workspace, policy name */ From fb0fb1fd1f73276b621c702d4404d72da2142f67 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 26 Dec 2023 22:58:38 +0100 Subject: [PATCH 16/28] edit comment Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 912df6b634ad..85705fe3c6d7 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -67,7 +67,7 @@ type SubAvatar = { /** Avatar id */ id?: number | string; - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL */ fallbackIcon?: UserUtils.AvatarSource; }; From 03dd713d22be9f7bc2540fb2931a8ade1e758f7e Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 29 Dec 2023 17:50:37 +0100 Subject: [PATCH 17/28] make props optional --- src/pages/home/report/ReportActionItemSingle.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 23313841529b..5989278e7a20 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -39,19 +39,19 @@ type ReportActionItemSingleProps = { report: Report; /** IOU Report for this action, if any */ - iouReport: Report; + iouReport?: Report; /** Show header for action */ - showHeader: boolean; + showHeader?: boolean; /** Determines if the avatar is displayed as a subscript (positioned lower than normal) */ - shouldShowSubscriptAvatar: boolean; + shouldShowSubscriptAvatar?: boolean; /** If the message has been flagged for moderation */ - hasBeenFlagged: boolean; + hasBeenFlagged?: boolean; /** If the action is being hovered */ - isHovered: boolean; + isHovered?: boolean; }; type SubAvatar = { @@ -122,7 +122,7 @@ function ReportActionItemSingle({ 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 = iouReport.ownerAccountID === actorAccountID ? iouReport.managerID : iouReport.ownerAccountID; + const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID ? iouReport?.managerID : iouReport?.ownerAccountID; const secondaryUserAvatar = secondaryAccountId ? personalDetails?.[secondaryAccountId]?.avatar ?? {} : {}; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; @@ -168,7 +168,7 @@ function ReportActionItemSingle({ showWorkspaceDetails(reportID); } else { // Show participants page IOU report preview - if (displayAllActors) { + if (iouReportID && displayAllActors) { Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } From 1e8d47f091af858c3f84d96bbc2f62d25096854b Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 29 Dec 2023 17:57:44 +0100 Subject: [PATCH 18/28] fix expression for when login is empty string --- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 5989278e7a20..4d94936c7058 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -98,7 +98,7 @@ function ReportActionItemSingle({ 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 ?? -1] ?? {}; - let actorHint = (login ?? displayName ?? '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); + 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); From f21ecce1bb9d05e140ce8140d87ae5683dd3ad54 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 29 Dec 2023 18:03:43 +0100 Subject: [PATCH 19/28] using || instead of ?? --- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 4d94936c7058..a1f6967f005e 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -100,7 +100,7 @@ function ReportActionItemSingle({ const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; 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); + const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors); let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); if (isWorkspaceActor) { From e357d3d706c2b748f085029e28c1eea87190ec22 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 29 Dec 2023 18:25:08 +0100 Subject: [PATCH 20/28] using undefined instead of null for default pendingAction --- src/components/OfflineWithFeedback.tsx | 2 +- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 586b3cb30b6c..4522595826af 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -18,7 +18,7 @@ import MessagesRow from './MessagesRow'; type OfflineWithFeedbackProps = ChildrenProps & { /** The type of action that's pending */ - pendingAction: OnyxCommon.PendingAction | null; + pendingAction?: OnyxCommon.PendingAction; /** Determine whether to hide the component's children if deletion is pending */ shouldHideOnDelete?: boolean; diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index a1f6967f005e..068c141bdda9 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -235,7 +235,7 @@ function ReportActionItemSingle({ accessibilityLabel={actorHint} role={CONST.ROLE.BUTTON} > - {getAvatar()} + {getAvatar()} {showHeader ? ( From a7eac8f885a081305add542addfe40c3c7009b80 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 29 Dec 2023 18:41:02 +0100 Subject: [PATCH 21/28] fix secondaryUserAvatar statement --- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 068c141bdda9..f54d0bfff6a5 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -123,7 +123,7 @@ function ReportActionItemSingle({ 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 = iouReport?.ownerAccountID === actorAccountID ? iouReport?.managerID : iouReport?.ownerAccountID; - const secondaryUserAvatar = secondaryAccountId ? personalDetails?.[secondaryAccountId]?.avatar ?? {} : {}; + const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? ''; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; secondaryAvatar = { From ee3f3b3ed46dbb4f36cfba777df699a1a2363d54 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 29 Dec 2023 18:51:33 +0100 Subject: [PATCH 22/28] fix for when delegateAccountID is empty string --- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index f54d0bfff6a5..b38b0997878c 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -172,7 +172,7 @@ function ReportActionItemSingle({ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } - showUserDetails(action.delegateAccountID ?? String(actorAccountID) ?? -1); + showUserDetails(action.delegateAccountID ? action.delegateAccountID : String(actorAccountID)); } }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); From 4adc9cb5b882eacb07a5365cfc10acfe086b5db1 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 29 Dec 2023 19:06:18 +0100 Subject: [PATCH 23/28] fix shouldDisableDetailPage statement --- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index b38b0997878c..956b7ab0016c 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -177,7 +177,7 @@ function ReportActionItemSingle({ }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); const shouldDisableDetailPage = useMemo( - () => actorAccountID === CONST.ACCOUNT_ID.NOTIFICATIONS || (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(Number(action.delegateAccountID ?? actorAccountID) ?? -1)), + () => actorAccountID === CONST.ACCOUNT_ID.NOTIFICATIONS || (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)), [action, isWorkspaceActor, actorAccountID], ); From a191972c35152d4e4cbfcbbb4331c5ba4e6e3e80 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 29 Dec 2023 19:07:51 +0100 Subject: [PATCH 24/28] use null instead of undefined for pendingAction default --- src/components/OfflineWithFeedback.tsx | 2 +- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 4522595826af..586b3cb30b6c 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -18,7 +18,7 @@ import MessagesRow from './MessagesRow'; type OfflineWithFeedbackProps = ChildrenProps & { /** The type of action that's pending */ - pendingAction?: OnyxCommon.PendingAction; + pendingAction: OnyxCommon.PendingAction | null; /** Determine whether to hide the component's children if deletion is pending */ shouldHideOnDelete?: boolean; diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 956b7ab0016c..9106131db3c2 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -235,7 +235,7 @@ function ReportActionItemSingle({ accessibilityLabel={actorHint} role={CONST.ROLE.BUTTON} > - {getAvatar()} + {getAvatar()} {showHeader ? ( From 7238366adb278f80d033b0238a8a79d41d7cb43b Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 29 Dec 2023 19:09:09 +0100 Subject: [PATCH 25/28] fix lint error --- src/pages/home/report/ReportActionItemSingle.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 9106131db3c2..a7088d473ccc 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -98,6 +98,7 @@ function ReportActionItemSingle({ 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 ?? -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); From 0733b12882116d3a83444ce21efd428a588a02d9 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 29 Dec 2023 19:16:11 +0100 Subject: [PATCH 26/28] run prettier --- src/pages/home/report/ReportActionItemSingle.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index a7088d473ccc..91dcab4df9ee 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -178,7 +178,9 @@ function ReportActionItemSingle({ }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); const shouldDisableDetailPage = useMemo( - () => actorAccountID === CONST.ACCOUNT_ID.NOTIFICATIONS || (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)), + () => + actorAccountID === CONST.ACCOUNT_ID.NOTIFICATIONS || + (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)), [action, isWorkspaceActor, actorAccountID], ); From d0e03fe4305d9620ad2f5015246e113b4e9f369d Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 30 Dec 2023 18:00:37 +0530 Subject: [PATCH 27/28] fix type --- src/components/HeaderWithBackButton/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 939b2530fa3d..99e93e8d18d2 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -18,7 +18,7 @@ type ThreeDotsMenuItems = { onSelected: () => void; }; -type HeaderWithBackButtonProps = ChildrenProps & { +type HeaderWithBackButtonProps = Partial & { /** Title of the Header */ title?: string; From 4678d774e8eade1a28944f16ce48250e33308e07 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 2 Jan 2024 11:34:52 +0530 Subject: [PATCH 28/28] fixed calendar picker test --- tests/unit/CalendarPickerTest.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/CalendarPickerTest.js b/tests/unit/CalendarPickerTest.js index 9c5ea4bc72f6..e1c11fbb8ca8 100644 --- a/tests/unit/CalendarPickerTest.js +++ b/tests/unit/CalendarPickerTest.js @@ -1,5 +1,5 @@ import {fireEvent, render, within} from '@testing-library/react-native'; -import {addMonths, addYears, subYears} from 'date-fns'; +import {addMonths, addYears, subMonths, subYears} from 'date-fns'; import CalendarPicker from '../../src/components/DatePicker/CalendarPicker'; import CONST from '../../src/CONST'; import DateUtils from '../../src/libs/DateUtils'; @@ -73,7 +73,7 @@ describe('CalendarPicker', () => { fireEvent.press(getByTestId('prev-month-arrow')); - const prevMonth = new Date().getMonth() - 1; + const prevMonth = subMonths(new Date(), 1).getMonth(); expect(getByText(monthNames[prevMonth])).toBeTruthy(); });