From e70b5cbaba845c3990b4176b941e8dbc6747d476 Mon Sep 17 00:00:00 2001 From: ahmedGaber93 Date: Mon, 26 Jun 2023 18:42:00 +0200 Subject: [PATCH 001/214] allow Request money 'Description' to accept multiline --- src/components/MenuItem.js | 5 +++-- src/components/MoneyRequestHeader.js | 1 + src/pages/iou/MoneyRequestDescriptionPage.js | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index eae6069783f3..89e5d0e73201 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -67,6 +67,7 @@ const defaultProps = { hoverAndPressStyle: [], furtherDetails: '', furtherDetailsIcon: undefined, + numberOfLines: 1, }; function MenuItem(props) { @@ -80,7 +81,7 @@ function MenuItem(props) { props.shouldShowBasicTitle ? undefined : styles.textStrong, props.shouldShowHeaderTitle ? styles.textHeadlineH1 : undefined, props.interactive && props.disabled ? {...styles.disabledText, ...styles.userSelectNone} : undefined, - styles.pre, + props.numberOfLines === 1 && styles.pre, styles.ltr, isDeleted ? styles.offlineFeedback.deleted : undefined, ], @@ -192,7 +193,7 @@ function MenuItem(props) { {Boolean(props.title) && ( {convertToLTR(props.title)} diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index a0e0fc3b70ad..b857bfa342a4 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -196,6 +196,7 @@ function MoneyRequestHeader(props) { description={props.translate('common.description')} title={transactionDescription} disabled={isSettled} + numberOfLines={6} // shouldShowRightIcon={!isSettled} // onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.DESCRIPTION))} /> diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index 1c9af254a162..a7edc509d239 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -112,6 +112,9 @@ class MoneyRequestDescriptionPage extends Component { defaultValue={this.props.iou.comment} label={this.props.translate('moneyRequestConfirmationList.whatsItFor')} ref={(el) => (this.descriptionInputRef = el)} + autoGrowHeight + containerStyles={[styles.autoGrowHeightMultilineInput]} + textAlignVertical="top" /> From 8d96ea11aa0cf970af16c04d9ae4b1700fcc4aa0 Mon Sep 17 00:00:00 2001 From: ahmedGaber93 Date: Tue, 27 Jun 2023 10:53:07 +0200 Subject: [PATCH 002/214] add prop type numberOfLines for menuItemPropTypes.js --- src/components/menuItemPropTypes.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js index 3909f446c907..881f5626d59c 100644 --- a/src/components/menuItemPropTypes.js +++ b/src/components/menuItemPropTypes.js @@ -119,6 +119,9 @@ const propTypes = { /** An icon to display under the main item */ furtherDetailsIcon: PropTypes.oneOfType([PropTypes.elementType, PropTypes.string]), + + /** Number of lines for the title */ + numberOfLines: PropTypes.number, }; export default propTypes; From c99bec96f3a9f7cc99ef1f91e88c90e4145ca9f3 Mon Sep 17 00:00:00 2001 From: ahmedGaber93 Date: Tue, 27 Jun 2023 21:49:22 +0200 Subject: [PATCH 003/214] allow Request money 'Description' to accept multiline - remove header changes --- src/components/MenuItem.js | 5 ++--- src/components/MoneyRequestHeader.js | 1 - src/components/menuItemPropTypes.js | 3 --- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index 89e5d0e73201..eae6069783f3 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -67,7 +67,6 @@ const defaultProps = { hoverAndPressStyle: [], furtherDetails: '', furtherDetailsIcon: undefined, - numberOfLines: 1, }; function MenuItem(props) { @@ -81,7 +80,7 @@ function MenuItem(props) { props.shouldShowBasicTitle ? undefined : styles.textStrong, props.shouldShowHeaderTitle ? styles.textHeadlineH1 : undefined, props.interactive && props.disabled ? {...styles.disabledText, ...styles.userSelectNone} : undefined, - props.numberOfLines === 1 && styles.pre, + styles.pre, styles.ltr, isDeleted ? styles.offlineFeedback.deleted : undefined, ], @@ -193,7 +192,7 @@ function MenuItem(props) { {Boolean(props.title) && ( {convertToLTR(props.title)} diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index b857bfa342a4..a0e0fc3b70ad 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -196,7 +196,6 @@ function MoneyRequestHeader(props) { description={props.translate('common.description')} title={transactionDescription} disabled={isSettled} - numberOfLines={6} // shouldShowRightIcon={!isSettled} // onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.DESCRIPTION))} /> diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js index 881f5626d59c..3909f446c907 100644 --- a/src/components/menuItemPropTypes.js +++ b/src/components/menuItemPropTypes.js @@ -119,9 +119,6 @@ const propTypes = { /** An icon to display under the main item */ furtherDetailsIcon: PropTypes.oneOfType([PropTypes.elementType, PropTypes.string]), - - /** Number of lines for the title */ - numberOfLines: PropTypes.number, }; export default propTypes; From 015b933129c6050c1477fe7f2edc6bb04cd6d737 Mon Sep 17 00:00:00 2001 From: ahmedGaber93 Date: Thu, 29 Jun 2023 09:36:18 +0200 Subject: [PATCH 004/214] fix Request money 'Description' auto focus && submit on enter --- src/pages/iou/MoneyRequestDescriptionPage.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index a7edc509d239..969e1c9aa9a5 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -16,6 +16,7 @@ import ROUTES from '../../ROUTES'; import compose from '../../libs/compose'; import * as IOU from '../../libs/actions/IOU'; import optionPropTypes from '../../components/optionPropTypes'; +import focusAndUpdateMultilineInputRange from '../../libs/focusAndUpdateMultilineInputRange'; const propTypes = { ...withLocalizePropTypes, @@ -92,7 +93,7 @@ class MoneyRequestDescriptionPage extends Component { this.descriptionInputRef && this.descriptionInputRef.focus()} + onEntryTransitionEnd={() => focusAndUpdateMultilineInputRange(this.descriptionInputRef)} > From e3c7cc65608224d86e7a3b4b89020c9163c14faf Mon Sep 17 00:00:00 2001 From: ahmedGaber93 Date: Fri, 7 Jul 2023 18:37:45 +0200 Subject: [PATCH 005/214] request money description max lines --- src/components/MoneyRequestConfirmationList.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 512018706bcb..7e3b87c06f6d 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -306,6 +306,7 @@ function MoneyRequestConfirmationList(props) { onPress={() => Navigation.navigate(ROUTES.getMoneyRequestDescriptionRoute(props.iouType, props.reportID))} style={[styles.moneyRequestMenuItem, styles.mb2]} disabled={didConfirm || props.isReadOnly} + numberOfLinesTitle={2} /> ); From 32dda58af5d85ad8cb3bbc97d511ed7072780d59 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 27 Jul 2023 10:43:07 +0200 Subject: [PATCH 006/214] update REPORT_WITH_ID --- src/ROUTES.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ROUTES.js b/src/ROUTES.js index 215a9a6384c3..8984e8fe0a63 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -62,7 +62,7 @@ export default { NEW_CHAT: 'new/chat', NEW_TASK, REPORT, - REPORT_WITH_ID: 'r/:reportID?', + REPORT_WITH_ID: 'r/:reportID/:reportActionID?', EDIT_REQUEST: 'r/:threadReportID/edit/:field', getEditRequestRoute: (threadReportID, field) => `r/${threadReportID}/edit/${field}`, getReportRoute: (reportID) => `r/${reportID}`, From 69249e402e36f83c053645d658a564e973f9ee08 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 28 Jul 2023 12:08:49 +0200 Subject: [PATCH 007/214] add reportActionID to prop types --- src/pages/home/ReportScreen.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 3535bf8fa8e2..5bec229de011 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -46,6 +46,8 @@ const propTypes = { params: PropTypes.shape({ /** The ID of the report this screen should display */ reportID: PropTypes.string, + /** The reportActionID to scroll to */ + reportActionID: PropTypes.string, }).isRequired, }).isRequired, From a6b973c7127d5d4745277ae098de149befcdc8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 2 Aug 2023 11:53:59 +0200 Subject: [PATCH 008/214] fixed undraggable desktop window bug --- src/components/Pressable/GenericPressable/index.js | 4 +++- web/index.html | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Pressable/GenericPressable/index.js b/src/components/Pressable/GenericPressable/index.js index 859552e10cf3..34924d1791ec 100644 --- a/src/components/Pressable/GenericPressable/index.js +++ b/src/components/Pressable/GenericPressable/index.js @@ -14,7 +14,9 @@ const WebGenericPressable = forwardRef((props, ref) => ( aria-label={props.accessibilityLabel} aria-labelledby={props.accessibilityLabelledBy} aria-valuenow={props.accessibilityValue} - nativeID={props.nativeID || 'no-drag-area'} + nativeID={props.nativeID} + dataSet={{id: 'pressable'}} + className="ASDASD" /> )); diff --git a/web/index.html b/web/index.html index d207fa54b97a..8507b990848b 100644 --- a/web/index.html +++ b/web/index.html @@ -41,7 +41,7 @@ #drag-area { -webkit-app-region: drag; } - #no-drag-area { + #no-drag-area, #drag-area [class*="touchAction"] { -webkit-app-region: no-drag; } input::placeholder { From 514662619e0e9c8a65cc5a48b0b15e24c634a39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Sosna?= <43684335+SosenWiosen@users.noreply.github.com> Date: Thu, 3 Aug 2023 13:13:53 +0200 Subject: [PATCH 009/214] migrate ReportScreen.js --- src/pages/home/ReportScreen.js | 487 ++++++++++++++++----------------- 1 file changed, 243 insertions(+), 244 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 94fe5e2beb00..1488c65cd413 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useRef, useState, useEffect, useMemo, useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import {View} from 'react-native'; @@ -114,56 +114,165 @@ function getReportID(route) { return String(lodashGet(route, 'params.reportID', null)); } -// Keep a reference to the list view height so we can use it when a new ReportScreen component mounts -let reportActionsListViewHeight = 0; +function ReportScreen({ + betas, + route, + report, + reportActions, + accountManagerReportID, + personalDetails, + policies, + translate, + network, + isSmallScreenWidth, + isSidebarLoaded, + viewportOffsetTop, + isComposerFullSize, + errors, +}) { + const reportActionsListViewHeightRef = useRef(0); + const firstRenderRef = useRef(true); + const flatListRef = useRef(); + const reactionListRef = useRef(); + const prevReport = useRef(); + + const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(reportActionsListViewHeightRef.current); + const [isBannerVisible, setIsBannerVisible] = useState(true); + const [isReportRemoved, setIsReportRemoved] = useState(false); + + const reportID = getReportID(route); + const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); + const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; + + // There are no reportActions at all to display and we are still in the process of loading the next set of actions. + const isLoadingInitialReportActions = _.isEmpty(reportActions) && report.isLoadingReportActions; + + const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); + + const isLoading = !reportID || !isSidebarLoaded || _.isEmpty(personalDetails) || firstRenderRef.current; + firstRenderRef.current = false; + + const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(parentReportAction); + const isSingleTransactionView = ReportActionsUtils.isTransactionThread(parentReportAction); + + const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + + const isTopMostReportId = Navigation.getTopmostReportId() === getReportID(route); + + let headerView = ( + Navigation.goBack(ROUTES.HOME, false, true)} + personalDetails={personalDetails} + report={report} + /> + ); + + if (isSingleTransactionView && !isDeletedParentAction) { + headerView = ( + + ); + } -class ReportScreen extends React.Component { - gestureStartListener = null; + if (ReportUtils.isMoneyRequestReport(report)) { + headerView = ( + + ); + } - constructor(props) { - super(props); + /** + * When false the ReportActionsView will completely unmount and we will show a loader until it returns true. + * + * @returns {Boolean} + */ + const isReportReadyForDisplay = useMemo(() => { + const reportIDFromPath = getReportID(route); - this.onSubmitComment = this.onSubmitComment.bind(this); - this.chatWithAccountManager = this.chatWithAccountManager.bind(this); - this.dismissBanner = this.dismissBanner.bind(this); + // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely + const isTransitioning = report && report.reportID !== reportIDFromPath; + return reportIDFromPath !== '' && report.reportID && !isTransitioning; + }, [route, report]); - this.state = { - skeletonViewContainerHeight: reportActionsListViewHeight, - isBannerVisible: true, - isReportRemoved: false, - }; - this.firstRenderRef = React.createRef(); - this.firstRenderRef.current = reportActionsListViewHeight === 0; + const fetchReportIfNeeded = useCallback(() => { + const reportIDFromPath = getReportID(route); - this.flatListRef = React.createRef(); - this.reactionListRef = React.createRef(); - } + // Report ID will be empty when the reports collection is empty. + // This could happen when we are loading the collection for the first time after logging in. + if (!reportIDFromPath) { + return; + } + + // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that + // is not stored locally yet. If props.report.reportID exists, then the report has been stored locally and nothing more needs to be done. + // If it doesn't exist, then we fetch the report from the API. + if (report.reportID && report.reportID === getReportID(route)) { + return; + } + + Report.openReport(reportIDFromPath); + }, [report.reportID, route]); - componentDidMount() { - this.unsubscribeVisibilityListener = Visibility.onVisibilityChange(() => { - const isTopMostReportId = Navigation.getTopmostReportId() === getReportID(this.props.route); + const dismissBanner = useCallback(() => { + setIsBannerVisible(false); + }, []); + const chatWithAccountManager = useCallback(() => { + Navigation.navigate(ROUTES.getReportRoute(accountManagerReportID)); + }, [accountManagerReportID]); + + /** + * @param {String} text + */ + const onSubmitComment = useCallback( + (text) => { + Report.addComment(getReportID(route), text); + }, + [route], + ); + useEffect(() => { + const unsubscribeVisibilityListener = Visibility.onVisibilityChange(() => { // If the report is not fully visible (AKA on small screen devices and LHR is open) or the report is optimistic (AKA not yet created) // we don't need to call openReport - if (!getIsReportFullyVisible(isTopMostReportId) || this.props.report.isOptimisticReport) { + if (!getIsReportFullyVisible(isTopMostReportId) || report.isOptimisticReport) { return; } - Report.openReport(this.props.report.reportID); + Report.openReport(report.reportID); }); - this.fetchReportIfNeeded(); + fetchReportIfNeeded(); ComposerActions.setShouldShowComposeInput(true); - } - componentDidUpdate(prevProps) { + return () => { + if (!unsubscribeVisibilityListener) { + return; + } + unsubscribeVisibilityListener(); + }; + }, []); + + useEffect(() => { // If composer should be hidden, hide emoji picker as well - if (ReportUtils.shouldHideComposer(this.props.report, this.props.errors)) { + if (ReportUtils.shouldHideComposer(report, errors)) { EmojiPickerAction.hideEmojiPicker(true); } - const onyxReportID = this.props.report.reportID; - const prevOnyxReportID = prevProps.report.reportID; - const routeReportID = getReportID(this.props.route); + const onyxReportID = report.reportID; + const prevOnyxReportID = prevReport.reportID; + + const routeReportID = getReportID(route); // navigate to concierge when the room removed from another device (e.g. user leaving a room) // the report will not really null when removed, it will have defaultProps properties and values @@ -172,14 +281,14 @@ class ReportScreen extends React.Component { prevOnyxReportID === routeReportID && !onyxReportID && // non-optimistic case - (_.isEqual(this.props.report, defaultProps.report) || + (_.isEqual(report, defaultProps.report) || // optimistic case - (prevProps.report.statusNum === CONST.REPORT.STATUS.OPEN && this.props.report.statusNum === CONST.REPORT.STATUS.CLOSED)) + (prevReport.statusNum === CONST.REPORT.STATUS.OPEN && report.statusNum === CONST.REPORT.STATUS.CLOSED)) ) { Navigation.goBack(); Report.navigateToConciergeChat(); // isReportRemoved will prevent showing when navigating - this.setState({isReportRemoved: true}); + setIsReportRemoved(true); return; } @@ -191,226 +300,116 @@ class ReportScreen extends React.Component { return; } - this.fetchReportIfNeeded(); + fetchReportIfNeeded(); ComposerActions.setShouldShowComposeInput(true); - } - - componentWillUnmount() { - if (!this.unsubscribeVisibilityListener) { - return; - } - this.unsubscribeVisibilityListener(); - } - - /** - * @param {String} text - */ - onSubmitComment(text) { - Report.addComment(getReportID(this.props.route), text); - } - - /** - * When false the ReportActionsView will completely unmount and we will show a loader until it returns true. - * - * @returns {Boolean} - */ - isReportReadyForDisplay() { - const reportIDFromPath = getReportID(this.props.route); - - // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely - const isTransitioning = this.props.report && this.props.report.reportID !== reportIDFromPath; - return reportIDFromPath !== '' && this.props.report.reportID && !isTransitioning; - } - - fetchReportIfNeeded() { - const reportIDFromPath = getReportID(this.props.route); - - // Report ID will be empty when the reports collection is empty. - // This could happen when we are loading the collection for the first time after logging in. - if (!reportIDFromPath) { - return; - } - - // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that - // is not stored locally yet. If props.report.reportID exists, then the report has been stored locally and nothing more needs to be done. - // If it doesn't exist, then we fetch the report from the API. - if (this.props.report.reportID && this.props.report.reportID === getReportID(this.props.route)) { - return; - } - - Report.openReport(reportIDFromPath); - } - - dismissBanner() { - this.setState({isBannerVisible: false}); - } - - chatWithAccountManager() { - Navigation.navigate(ROUTES.getReportRoute(this.props.accountManagerReportID)); - } - - render() { - const reportID = getReportID(this.props.route); - const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(this.props.report); - const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: this.props.viewportOffsetTop}]; - - // There are no reportActions at all to display and we are still in the process of loading the next set of actions. - const isLoadingInitialReportActions = _.isEmpty(this.props.reportActions) && this.props.report.isLoadingReportActions; - - const shouldHideReport = !ReportUtils.canAccessReport(this.props.report, this.props.policies, this.props.betas); - - const isLoading = !reportID || !this.props.isSidebarLoaded || _.isEmpty(this.props.personalDetails) || this.firstRenderRef.current; - this.firstRenderRef.current = false; - - const parentReportAction = ReportActionsUtils.getParentReportAction(this.props.report); - const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(parentReportAction); - const isSingleTransactionView = ReportUtils.isMoneyRequest(this.props.report); - - const policy = this.props.policies[`${ONYXKEYS.COLLECTION.POLICY}${this.props.report.policyID}`]; - - const isTopMostReportId = Navigation.getTopmostReportId() === getReportID(this.props.route); - - let headerView = ( - Navigation.goBack(ROUTES.HOME, false, true)} - personalDetails={this.props.personalDetails} - report={this.props.report} - /> - ); - - if (isSingleTransactionView && !isDeletedParentAction) { - headerView = ( - - ); - } - - if (ReportUtils.isMoneyRequestReport(this.props.report)) { - headerView = ( - - ); - } - - return ( - + - - - - {headerView} - {ReportUtils.isTaskReport(this.props.report) && this.props.isSmallScreenWidth && ReportUtils.isOpenTaskReport(this.props.report) && ( - - - - - + {headerView} + {ReportUtils.isTaskReport(report) && isSmallScreenWidth && ReportUtils.isOpenTaskReport(report) && ( + + + + - )} - - {Boolean(this.props.accountManagerReportID) && ReportUtils.isConciergeChatReport(this.props.report) && this.state.isBannerVisible && ( - + )} - - { - // Rounding this value for comparison because they can look like this: 411.9999694824219 - const skeletonViewContainerHeight = Math.round(event.nativeEvent.layout.height); - - // Only set state when the height changes to avoid unnecessary renders - if (reportActionsListViewHeight === skeletonViewContainerHeight) return; - - // The height can be 0 if the component unmounts - we are not interested in this value and want to know how much space it - // takes up so we can set the skeleton view container height. - if (skeletonViewContainerHeight === 0) { - return; - } - reportActionsListViewHeight = skeletonViewContainerHeight; - this.setState({skeletonViewContainerHeight}); - }} - > - {this.isReportReadyForDisplay() && !isLoadingInitialReportActions && !isLoading && ( - - )} + + {Boolean(accountManagerReportID) && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( + + )} + + { + // Rounding this value for comparison because they can look like this: 411.9999694824219 + const newSkeletonViewContainerHeight = Math.round(event.nativeEvent.layout.height); + + // Only set state when the height changes to avoid unnecessary renders + if (reportActionsListViewHeightRef.current === newSkeletonViewContainerHeight) return; + + // The height can be 0 if the component unmounts - we are not interested in this value and want to know how much space it + // takes up so we can set the skeleton view container height. + if (newSkeletonViewContainerHeight === 0) { + return; + } + reportActionsListViewHeightRef.current = newSkeletonViewContainerHeight; + setSkeletonViewContainerHeight(newSkeletonViewContainerHeight); + }} + > + {isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading && ( + + )} - {/* Note: The report should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then + {/* Note: The report should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} - {(!this.isReportReadyForDisplay() || isLoadingInitialReportActions || isLoading) && ( - - )} - - {this.isReportReadyForDisplay() && ( - <> - - - )} - - {!this.isReportReadyForDisplay() && ( + {(!isReportReadyForDisplay || isLoadingInitialReportActions || isLoading) && } + + {isReportReadyForDisplay && ( + <> - )} - - - - - - ); - } + + )} + + {!isReportReadyForDisplay && ( + + )} + + + + + + ); } ReportScreen.propTypes = propTypes; From faa1e27aa1a5016ffbcf7fbd276de50968ce2fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Sosna?= <43684335+SosenWiosen@users.noreply.github.com> Date: Thu, 3 Aug 2023 17:27:52 +0200 Subject: [PATCH 010/214] fixes and delete unnecessary code. --- src/pages/home/ReportScreen.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 1488c65cd413..6b8f05a0a7e7 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -130,13 +130,12 @@ function ReportScreen({ isComposerFullSize, errors, }) { - const reportActionsListViewHeightRef = useRef(0); const firstRenderRef = useRef(true); const flatListRef = useRef(); const reactionListRef = useRef(); - const prevReport = useRef(); + const prevReportRef = useRef(); - const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(reportActionsListViewHeightRef.current); + const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(0); const [isBannerVisible, setIsBannerVisible] = useState(true); const [isReportRemoved, setIsReportRemoved] = useState(false); @@ -154,7 +153,7 @@ function ReportScreen({ const parentReportAction = ReportActionsUtils.getParentReportAction(report); const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(parentReportAction); - const isSingleTransactionView = ReportActionsUtils.isTransactionThread(parentReportAction); + const isSingleTransactionView = ReportUtils.isMoneyRequest(report); const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; @@ -262,6 +261,8 @@ function ReportScreen({ } unsubscribeVisibilityListener(); }; + // disabling the warning, as it expects to use exhaustive deps, even though we want this useEffect to run only on the first render. + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { @@ -270,7 +271,7 @@ function ReportScreen({ EmojiPickerAction.hideEmojiPicker(true); } const onyxReportID = report.reportID; - const prevOnyxReportID = prevReport.reportID; + const prevOnyxReportID = prevReportRef.current.reportID; const routeReportID = getReportID(route); @@ -283,7 +284,7 @@ function ReportScreen({ // non-optimistic case (_.isEqual(report, defaultProps.report) || // optimistic case - (prevReport.statusNum === CONST.REPORT.STATUS.OPEN && report.statusNum === CONST.REPORT.STATUS.CLOSED)) + (prevReportRef.current.statusNum === CONST.REPORT.STATUS.OPEN && report.statusNum === CONST.REPORT.STATUS.CLOSED)) ) { Navigation.goBack(); Report.navigateToConciergeChat(); @@ -302,7 +303,7 @@ function ReportScreen({ fetchReportIfNeeded(); ComposerActions.setShouldShowComposeInput(true); - prevReport.current = report; + prevReportRef.current = report; }, [route, report, errors, fetchReportIfNeeded]); return ( @@ -357,15 +358,11 @@ function ReportScreen({ // Rounding this value for comparison because they can look like this: 411.9999694824219 const newSkeletonViewContainerHeight = Math.round(event.nativeEvent.layout.height); - // Only set state when the height changes to avoid unnecessary renders - if (reportActionsListViewHeightRef.current === newSkeletonViewContainerHeight) return; - // The height can be 0 if the component unmounts - we are not interested in this value and want to know how much space it // takes up so we can set the skeleton view container height. if (newSkeletonViewContainerHeight === 0) { return; } - reportActionsListViewHeightRef.current = newSkeletonViewContainerHeight; setSkeletonViewContainerHeight(newSkeletonViewContainerHeight); }} > From 5715e91708a08f7bfc59871665ef10868f610f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Sosna?= <43684335+SosenWiosen@users.noreply.github.com> Date: Thu, 3 Aug 2023 18:07:42 +0200 Subject: [PATCH 011/214] fix prevReportRef.current being undefined --- src/pages/home/ReportScreen.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 6b8f05a0a7e7..d11b4b8485fd 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -149,7 +149,6 @@ function ReportScreen({ const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); const isLoading = !reportID || !isSidebarLoaded || _.isEmpty(personalDetails) || firstRenderRef.current; - firstRenderRef.current = false; const parentReportAction = ReportActionsUtils.getParentReportAction(report); const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(parentReportAction); @@ -241,6 +240,7 @@ function ReportScreen({ }, [route], ); + useEffect(() => { const unsubscribeVisibilityListener = Visibility.onVisibilityChange(() => { // If the report is not fully visible (AKA on small screen devices and LHR is open) or the report is optimistic (AKA not yet created) @@ -254,6 +254,7 @@ function ReportScreen({ fetchReportIfNeeded(); ComposerActions.setShouldShowComposeInput(true); + prevReportRef.current = report; return () => { if (!unsubscribeVisibilityListener) { @@ -261,13 +262,19 @@ function ReportScreen({ } unsubscribeVisibilityListener(); }; - // disabling the warning, as it expects to use exhaustive deps, even though we want this useEffect to run only on the first render. + // I'm disabling the warning, as it expects to use exhaustive deps, even though we want this useEffect to run only on the first render. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { - // If composer should be hidden, hide emoji picker as well + // We don't want this effect to run on the first render. + if (firstRenderRef.current) { + firstRenderRef.current = false; + return; + } + if (ReportUtils.shouldHideComposer(report, errors)) { + // If composer should be hidden, hide emoji picker as well EmojiPickerAction.hideEmojiPicker(true); } const onyxReportID = report.reportID; From 653099be1456b56c49155d302b2abf6272652a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Fri, 4 Aug 2023 10:28:16 +0200 Subject: [PATCH 012/214] removed unnecessary changes + fixed ThreePaneView --- src/components/Pressable/GenericPressable/index.js | 2 -- .../createResponsiveStackNavigator/ThreePaneView.js | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Pressable/GenericPressable/index.js b/src/components/Pressable/GenericPressable/index.js index 34924d1791ec..ec5976a6db0b 100644 --- a/src/components/Pressable/GenericPressable/index.js +++ b/src/components/Pressable/GenericPressable/index.js @@ -15,8 +15,6 @@ const WebGenericPressable = forwardRef((props, ref) => ( aria-labelledby={props.accessibilityLabelledBy} aria-valuenow={props.accessibilityValue} nativeID={props.nativeID} - dataSet={{id: 'pressable'}} - className="ASDASD" /> )); diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/ThreePaneView.js b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/ThreePaneView.js index 2f9a899191bf..5b92a1304c26 100644 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/ThreePaneView.js +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/ThreePaneView.js @@ -71,6 +71,7 @@ function ThreePaneView(props) { onPress={() => props.navigation.goBack()} accessibilityLabel={translate('common.close')} accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + nativeID="no-drag-area" /> {props.descriptors[route.key].render()} From 96b35655f18ffe5085cab7ea0d65748f9bde4396 Mon Sep 17 00:00:00 2001 From: tienifr Date: Sat, 5 Aug 2023 02:46:01 +0700 Subject: [PATCH 013/214] fix: app opens all option when press quickly --- src/hooks/useSingleExecution.js | 35 +++++++++++++++++ src/hooks/useWaitForNavigate.js | 32 ++++++++++++++++ src/pages/settings/InitialSettingsPage.js | 46 +++++++++++++---------- 3 files changed, 93 insertions(+), 20 deletions(-) create mode 100644 src/hooks/useSingleExecution.js create mode 100644 src/hooks/useWaitForNavigate.js diff --git a/src/hooks/useSingleExecution.js b/src/hooks/useSingleExecution.js new file mode 100644 index 000000000000..22c2907c9420 --- /dev/null +++ b/src/hooks/useSingleExecution.js @@ -0,0 +1,35 @@ +import {InteractionManager} from 'react-native'; +import {useCallback, useState} from 'react'; + +/** + * With any action passed in, it will only allow 1 such action to occur at a time. + * + * @returns {Object} + */ +export default function useSingleExecution() { + const [isExecuting, setIsExecuting] = useState(false); + + const singleExecution = useCallback( + (action) => () => { + if (isExecuting) { + return; + } + + setIsExecuting(true); + + const execution = action(); + InteractionManager.runAfterInteractions(() => { + if (!(execution instanceof Promise)) { + setIsExecuting(false); + return; + } + execution.finally(() => { + setIsExecuting(false); + }); + }); + }, + [isExecuting], + ); + + return {isExecuting, singleExecution}; +} diff --git a/src/hooks/useWaitForNavigate.js b/src/hooks/useWaitForNavigate.js new file mode 100644 index 000000000000..fae62599bfc6 --- /dev/null +++ b/src/hooks/useWaitForNavigate.js @@ -0,0 +1,32 @@ +import {useEffect, useRef} from 'react'; +import {useNavigation} from '@react-navigation/native'; + +/** + * Returns a promise that resolves when navigation finishes. + * + * @returns {function} + */ +export default function useWaitForNavigate() { + const navigation = useNavigation(); + const resolvePromises = useRef([]); + + useEffect(() => { + const unsubscribeBlur = navigation.addListener('blur', () => { + resolvePromises.current.forEach((resolve) => { + resolve(); + }); + resolvePromises.current = []; + }); + + return () => { + unsubscribeBlur(); + }; + }, [navigation]); + + return (navigate) => () => { + navigate(); + return new Promise((resolve) => { + resolvePromises.current.push(resolve); + }); + }; +} diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 825da39244d8..dc869564b728 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -41,6 +41,8 @@ import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions' import * as CurrencyUtils from '../../libs/CurrencyUtils'; import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import useLocalize from '../../hooks/useLocalize'; +import useSingleExecution from '../../hooks/useSingleExecution'; +import useWaitForNavigate from '../../hooks/useWaitForNavigate'; const propTypes = { /* Onyx Props */ @@ -129,6 +131,8 @@ const defaultProps = { }; function InitialSettingsPage(props) { + const {isExecuting, singleExecution} = useSingleExecution(); + const waitForNavigate = useWaitForNavigate(); const popoverAnchor = useRef(null); const {translate} = useLocalize(); @@ -190,16 +194,16 @@ function InitialSettingsPage(props) { { translationKey: 'common.shareCode', icon: Expensicons.QrCode, - action: () => { + action: waitForNavigate(() => { Navigation.navigate(ROUTES.SETTINGS_SHARE_CODE); - }, + }), }, { translationKey: 'common.workspaces', icon: Expensicons.Building, - action: () => { + action: waitForNavigate(() => { Navigation.navigate(ROUTES.SETTINGS_WORKSPACES); - }, + }), floatRightAvatars: policiesAvatars, shouldStackHorizontally: true, avatarSize: CONST.AVATAR_SIZE.SMALLER, @@ -208,31 +212,31 @@ function InitialSettingsPage(props) { { translationKey: 'common.profile', icon: Expensicons.Profile, - action: () => { + action: waitForNavigate(() => { Navigation.navigate(ROUTES.SETTINGS_PROFILE); - }, + }), brickRoadIndicator: profileBrickRoadIndicator, }, { translationKey: 'common.preferences', icon: Expensicons.Gear, - action: () => { + action: waitForNavigate(() => { Navigation.navigate(ROUTES.SETTINGS_PREFERENCES); - }, + }), }, { translationKey: 'initialSettingsPage.security', icon: Expensicons.Lock, - action: () => { + action: waitForNavigate(() => { Navigation.navigate(ROUTES.SETTINGS_SECURITY); - }, + }), }, { translationKey: 'common.payments', icon: Expensicons.Wallet, - action: () => { + action: waitForNavigate(() => { Navigation.navigate(ROUTES.SETTINGS_PAYMENTS); - }, + }), brickRoadIndicator: PaymentMethods.hasPaymentMethodError(props.bankAccountList, paymentCardList) || !_.isEmpty(props.userWallet.errors) || !_.isEmpty(props.walletTerms.errors) ? 'error' @@ -241,9 +245,9 @@ function InitialSettingsPage(props) { { translationKey: 'initialSettingsPage.help', icon: Expensicons.QuestionMark, - action: () => { + action: waitForNavigate(() => { Link.openExternalLink(CONST.NEWHELP_URL); - }, + }), shouldShowRightIcon: true, iconRight: Expensicons.NewWindow, link: CONST.NEWHELP_URL, @@ -251,16 +255,16 @@ function InitialSettingsPage(props) { { translationKey: 'initialSettingsPage.about', icon: Expensicons.Info, - action: () => { + action: waitForNavigate(() => { Navigation.navigate(ROUTES.SETTINGS_ABOUT); - }, + }), }, { translationKey: 'initialSettingsPage.signOut', icon: Expensicons.Exit, - action: () => { + action: waitForNavigate(() => { signOut(false); - }, + }), }, ]; }, [ @@ -275,6 +279,7 @@ function InitialSettingsPage(props) { props.userWallet.errors, props.walletTerms.errors, signOut, + waitForNavigate, ]); const getMenuItems = useMemo(() => { @@ -297,7 +302,8 @@ function InitialSettingsPage(props) { title={keyTitle} icon={item.icon} iconType={item.iconType} - onPress={item.action} + disabled={isExecuting} + onPress={singleExecution(item.action)} iconStyles={item.iconStyles} shouldShowRightIcon iconRight={item.iconRight} @@ -317,7 +323,7 @@ function InitialSettingsPage(props) { })} ); - }, [getDefaultMenuItems, props.betas, props.userWallet.currentBalance, translate]); + }, [getDefaultMenuItems, props.betas, props.userWallet.currentBalance, translate, isExecuting, singleExecution]); // On the very first sign in or after clearing storage these // details will not be present on the first render so we'll just From 7bc848b9af084b0efd442c576897652e73a637d1 Mon Sep 17 00:00:00 2001 From: tienifr Date: Sat, 5 Aug 2023 03:05:12 +0700 Subject: [PATCH 014/214] useWaitForNavigation only when navigating by react-navigation --- src/components/Pressable/PressableWithFeedback.js | 15 +++------------ ...WaitForNavigate.js => useWaitForNavigation.js} | 3 ++- src/pages/settings/InitialSettingsPage.js | 12 ++++++------ 3 files changed, 11 insertions(+), 19 deletions(-) rename src/hooks/{useWaitForNavigate.js => useWaitForNavigation.js} (88%) diff --git a/src/components/Pressable/PressableWithFeedback.js b/src/components/Pressable/PressableWithFeedback.js index 0f33a3685b1c..d31c42ebfa20 100644 --- a/src/components/Pressable/PressableWithFeedback.js +++ b/src/components/Pressable/PressableWithFeedback.js @@ -1,11 +1,11 @@ import React, {forwardRef, useState} from 'react'; import _ from 'underscore'; import propTypes from 'prop-types'; -import {InteractionManager} from 'react-native'; import GenericPressable from './GenericPressable'; import GenericPressablePropTypes from './GenericPressable/PropTypes'; import OpacityView from '../OpacityView'; import variables from '../../styles/variables'; +import useSingleExecution from '../../hooks/useSingleExecution'; const omittedProps = ['wrapperStyle']; @@ -39,7 +39,7 @@ const PressableWithFeedbackDefaultProps = { const PressableWithFeedback = forwardRef((props, ref) => { const propsWithoutWrapperStyles = _.omit(props, omittedProps); - const [isExecuting, setIsExecuting] = useState(false); + const {isExecuting, singleExecution} = useSingleExecution(); const [isPressed, setIsPressed] = useState(false); const [isHovered, setIsHovered] = useState(false); const isDisabled = props.disabled || isExecuting; @@ -73,17 +73,8 @@ const PressableWithFeedback = forwardRef((props, ref) => { if (props.onPressOut) props.onPressOut(); }} onPress={(e) => { - setIsExecuting(true); const onPress = props.onPress(e); - InteractionManager.runAfterInteractions(() => { - if (!(onPress instanceof Promise)) { - setIsExecuting(false); - return; - } - onPress.finally(() => { - setIsExecuting(false); - }); - }); + singleExecution(onPress); }} > {(state) => (_.isFunction(props.children) ? props.children(state) : props.children)} diff --git a/src/hooks/useWaitForNavigate.js b/src/hooks/useWaitForNavigation.js similarity index 88% rename from src/hooks/useWaitForNavigate.js rename to src/hooks/useWaitForNavigation.js index fae62599bfc6..00f4405dff12 100644 --- a/src/hooks/useWaitForNavigate.js +++ b/src/hooks/useWaitForNavigation.js @@ -3,10 +3,11 @@ import {useNavigation} from '@react-navigation/native'; /** * Returns a promise that resolves when navigation finishes. + * Only use when navigating by react-navigation * * @returns {function} */ -export default function useWaitForNavigate() { +export default function useWaitForNavigation() { const navigation = useNavigation(); const resolvePromises = useRef([]); diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index dc869564b728..07d235a25227 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -42,7 +42,7 @@ import * as CurrencyUtils from '../../libs/CurrencyUtils'; import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import useLocalize from '../../hooks/useLocalize'; import useSingleExecution from '../../hooks/useSingleExecution'; -import useWaitForNavigate from '../../hooks/useWaitForNavigate'; +import useWaitForNavigation from '../../hooks/useWaitForNavigation'; const propTypes = { /* Onyx Props */ @@ -132,7 +132,7 @@ const defaultProps = { function InitialSettingsPage(props) { const {isExecuting, singleExecution} = useSingleExecution(); - const waitForNavigate = useWaitForNavigate(); + const waitForNavigate = useWaitForNavigation(); const popoverAnchor = useRef(null); const {translate} = useLocalize(); @@ -245,9 +245,9 @@ function InitialSettingsPage(props) { { translationKey: 'initialSettingsPage.help', icon: Expensicons.QuestionMark, - action: waitForNavigate(() => { + action: () => { Link.openExternalLink(CONST.NEWHELP_URL); - }), + }, shouldShowRightIcon: true, iconRight: Expensicons.NewWindow, link: CONST.NEWHELP_URL, @@ -262,9 +262,9 @@ function InitialSettingsPage(props) { { translationKey: 'initialSettingsPage.signOut', icon: Expensicons.Exit, - action: waitForNavigate(() => { + action: () => { signOut(false); - }), + }, }, ]; }, [ From e1c03197369c48c97de67376459f4a21b5d2e828 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 7 Aug 2023 19:38:31 +0100 Subject: [PATCH 015/214] Add approveMoneyRequest action --- src/libs/ReportActionsUtils.js | 7 +++++ src/libs/actions/IOU.js | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 3cd5621e5e32..8391648df4a0 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -202,6 +202,12 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal .value(); } +function getReportMostRecentIOUTransactionID(reportID) { + const reportActions = _.filter(allReportActions[reportID], (action) => lodashGet(action, 'originalMessage.type') === CONST.IOU.REPORT_ACTION_TYPE.CREATE); + const sortedReportActions = getSortedReportActions(reportActions); + return _.last(sortedReportActions).originalMessage.IOUTransactionID; +} + /** * Finds most recent IOU request action ID. * @@ -566,6 +572,7 @@ export { getLastVisibleAction, getLastVisibleMessage, getMostRecentIOURequestActionID, + getReportMostRecentIOUTransactionID, extractLinksFromMessageHtml, isCreatedAction, isDeletedAction, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 45fcd35eb839..de753e7016ae 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1431,6 +1431,59 @@ function sendMoneyViaPaypal(report, amount, currency, comment, managerID, recipi asyncOpenURL(Promise.resolve(), buildPayPalPaymentUrl(amount, recipient.payPalMeAddress, currency)); } +function approveMoneyRequest(expenseReport) { + const IOUTransactionID = ReportActionsUtils.getReportMostRecentIOUTransactionID(expenseReport.reportID); + const optimisticIOUReportAction = ReportUtils.buildOptimisticApprovedReportAction(expenseReport.total, expenseReport.currency, expenseReport.reportID, IOUTransactionID); + + const optimisticReportActionsData = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [optimisticIOUReportAction.reportActionID]: { + ...optimisticIOUReportAction, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }; + const optimisticIOUReportData = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + value: { + ...expenseReport, + lastMessageText: optimisticIOUReportAction.message[0].text, + lastMessageHtml: optimisticIOUReportAction.message[0].html, + statusNum: CONST.REPORT.STATUS.APPROVED, + }, + }; + const optimisticData = [optimisticIOUReportData, optimisticReportActionsData]; + + const successData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [optimisticIOUReportAction.reportActionID]: { + pendingAction: null, + }, + }, + }, + ]; + + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [expenseReport.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), + }, + }, + }, + ]; + + API.write('ApproveMoneyRequest', {reportID: expenseReport.reportID, approvedReportActionID: optimisticIOUReportAction.reportActionID}, {optimisticData, successData, failureData}); +} + /** * @param {String} paymentType * @param {Object} chatReport From b1e77abbc8eb7c558bc6953bb4b48076d1c9a543 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Mon, 7 Aug 2023 19:46:23 +0100 Subject: [PATCH 016/214] Add logic for showing the approve/settlement button --- src/components/MoneyReportHeader.js | 41 ++++++++++++++++++++++++++++- src/libs/ReportUtils.js | 10 ++++--- src/libs/SidebarUtils.js | 2 +- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 71ac5e02467b..c2a70bd23fa6 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -58,7 +58,28 @@ function MoneyReportHeader(props) { const isPayer = Policy.isAdminOfFreePolicy([policy]) || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(props.session, 'accountID', null) === moneyRequestReport.managerID); const reportTotal = ReportUtils.getMoneyRequestTotal(props.report); - const shouldShowSettlementButton = !isSettled && isPayer && !moneyRequestReport.isWaitingOnBankAccount && reportTotal !== 0; + const shouldShowSettlementButton = useMemo(() => { + if (ReportUtils.getPolicyType(moneyRequestReport, props.policies) === CONST.POLICY.TYPE.CORPORATE) { + return Policy.isAdminOfControlPolicy([policy]) && ReportUtils.isReportApproved(moneyRequestReport) && !ReportUtils.isSettled(moneyRequestReport.reportID); + } + + return ( + (Policy.isAdminOfFreePolicy([policy]) || + (ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(props.session, 'accountID', null) === moneyRequestReport.managerID)) && + !ReportUtils.isSettled(moneyRequestReport) + ); + }, [moneyRequestReport, policy, props.policies, props.session]); + const shouldShowApprovedButton = useMemo(() => { + if (ReportUtils.getPolicyType(moneyRequestReport) !== CONST.POLICY.TYPE.CORPORATE) { + return false; + } + + return ( + ReportUtils.isReportManager(moneyRequestReport) && + lodashGet(moneyRequestReport, 'stateNum') === CONST.REPORT.STATE_NUM.PROCESSING && + lodashGet(moneyRequestReport, 'statusNum') === CONST.REPORT.STATUS.SUBMITTED + ); + }, [moneyRequestReport]); const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport); const shouldShowPaypal = Boolean(lodashGet(props.personalDetails, [moneyRequestReport.managerID, 'payPalMeAddress'])); const formattedAmount = CurrencyUtils.convertToDisplayString(reportTotal, props.report.currency); @@ -93,6 +114,15 @@ function MoneyReportHeader(props) { /> )} + {shouldShowApprovedButton && !props.isSmallScreenWidth && ( + +