From 68a36ae62a725eb4fc5ae4687397b297c5f3fe36 Mon Sep 17 00:00:00 2001 From: Pierre Michel Date: Fri, 21 Jul 2023 15:52:39 -0600 Subject: [PATCH 001/312] Add longPress support for TaskPreview Signed-off-by: Pierre Michel --- src/components/ReportActionItem/TaskPreview.js | 17 +++++++++++++++++ src/pages/home/report/ReportActionItem.js | 3 +++ 2 files changed, 20 insertions(+) diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 472c71298852..7f18a65e420e 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -22,6 +22,10 @@ import * as ReportUtils from '../../libs/ReportUtils'; import RenderHTML from '../RenderHTML'; import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback'; import personalDetailsPropType from '../../pages/personalDetailsPropType'; +import {showContextMenuForReport} from '../ShowContextMenuContext'; +import reportPropTypes from '../../pages/reportPropTypes'; +import refPropTypes from '../refPropTypes'; +import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; const propTypes = { /** All personal details asssociated with user */ @@ -49,6 +53,16 @@ const propTypes = { ownerAccountID: PropTypes.number, }), + /* Onyx Props */ + /** chatReport associated with taskReport */ + chatReport: reportPropTypes, + + /** Popover context menu anchor, used for showing context menu */ + contextMenuAnchor: refPropTypes, + + /** Callback for updating context menu active state, used for showing context menu */ + checkIfContextMenuActive: PropTypes.func, + ...withLocalizePropTypes, }; @@ -74,6 +88,9 @@ function TaskPreview(props) { Navigation.navigate(ROUTES.getReportRoute(props.taskReportID))} + onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + onLongPress={(event) => showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive)} style={[styles.flexRow, styles.justifyContentBetween]} accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={props.translate('task.task')} diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 0b67a728ba0e..b15e92e29b62 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -281,8 +281,11 @@ function ReportActionItem(props) { children = ( ); } else { From 0efba40908b3ea408f222edee2d6e9b23d855387 Mon Sep 17 00:00:00 2001 From: Pierre Michel Date: Fri, 21 Jul 2023 16:27:14 -0600 Subject: [PATCH 002/312] Add ControlSelection to imports Signed-off-by: Pierre Michel --- src/components/ReportActionItem/TaskPreview.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 7f18a65e420e..9d66d16b9b54 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -26,6 +26,7 @@ import {showContextMenuForReport} from '../ShowContextMenuContext'; import reportPropTypes from '../../pages/reportPropTypes'; import refPropTypes from '../refPropTypes'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; +import ControlSelection from '../../libs/ControlSelection'; const propTypes = { /** All personal details asssociated with user */ From ceaaf3359fdeb3569bf652059dc50f7a9a63ccc4 Mon Sep 17 00:00:00 2001 From: April Bekkala Date: Wed, 27 Sep 2023 22:33:14 -0500 Subject: [PATCH 003/312] Create Copilot Adding the Copilot page info --- .../account-settings/Copilot | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/articles/expensify-classic/account-settings/Copilot diff --git a/docs/articles/expensify-classic/account-settings/Copilot b/docs/articles/expensify-classic/account-settings/Copilot new file mode 100644 index 000000000000..dbd26af12d88 --- /dev/null +++ b/docs/articles/expensify-classic/account-settings/Copilot @@ -0,0 +1,78 @@ +--- +title: Copilot +description: Safely delegate tasks without sharing login information. +--- + + +# About + + +# How-to + + +# Deep Dive + + +# FAQ + From 23338eb4067c5420f6960657127223af6eed907b Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 3 Oct 2023 00:42:39 +0700 Subject: [PATCH 004/312] Fix receipt image is opened in transaction thread report --- src/components/Attachments/AttachmentCarousel/index.js | 7 +++++-- .../Attachments/AttachmentCarousel/index.native.js | 6 ++++-- .../ReportActionCompose/AttachmentPickerWithMenuItems.js | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js index 00b603cdd7d9..df6c812fde4e 100644 --- a/src/components/Attachments/AttachmentCarousel/index.js +++ b/src/components/Attachments/AttachmentCarousel/index.js @@ -37,17 +37,18 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, setDownl const [attachments, setAttachments] = useState([]); const [activeSource, setActiveSource] = useState(source); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); + const [isReceipt, setIsReceipt] = useState(false); const compareImage = useCallback( (attachment) => { - if (attachment.isReceipt) { + if (attachment.isReceipt && isReceipt) { const action = ReportActionsUtils.getParentReportAction(report); const transactionID = _.get(action, ['originalMessage', 'IOUTransactionID']); return attachment.transactionID === transactionID; } return attachment.source === source; }, - [source, report], + [source, report, isReceipt], ); useEffect(() => { @@ -86,10 +87,12 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, setDownl // to get the index of the current page const entry = _.first(viewableItems); if (!entry) { + setIsReceipt(false); setActiveSource(null); return; } + setIsReceipt(entry.item.isReceipt); setPage(entry.index); setActiveSource(entry.item.source); diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index bd12020341be..f2cdc111c666 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -27,17 +27,18 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose, const [activeSource, setActiveSource] = useState(source); const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); + const [isReceipt, setIsReceipt] = useState(false); const compareImage = useCallback( (attachment) => { - if (attachment.isReceipt) { + if (attachment.isReceipt && isReceipt) { const action = ReportActionsUtils.getParentReportAction(report); const transactionID = _.get(action, ['originalMessage', 'IOUTransactionID']); return attachment.transactionID === transactionID; } return attachment.source === source; }, - [source, report], + [source, report, isReceipt], ); useEffect(() => { @@ -76,6 +77,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose, const item = attachments[newPageIndex]; setPage(newPageIndex); + setIsReceipt(item.isReceipt); setActiveSource(item.source); onNavigate(item); diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index 6dd3355f4a53..10f29615c904 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -243,7 +243,7 @@ function AttachmentPickerWithMenuItems({ onAddActionPressed(); // Drop focus to avoid blue focus ring. - actionButtonRef.current.blur(); + // actionButtonRef.current.blur(); setMenuVisibility(!isMenuVisible); }} style={styles.composerSizeButton} From e48bd2f7a97333dbe779d49fa5f97d24a1fea76f Mon Sep 17 00:00:00 2001 From: April Bekkala Date: Mon, 2 Oct 2023 14:13:44 -0500 Subject: [PATCH 005/312] Rename Copilot to Copilot.md needed to add .md to the title --- .../expensify-classic/account-settings/{Copilot => Copilot.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/expensify-classic/account-settings/{Copilot => Copilot.md} (100%) diff --git a/docs/articles/expensify-classic/account-settings/Copilot b/docs/articles/expensify-classic/account-settings/Copilot.md similarity index 100% rename from docs/articles/expensify-classic/account-settings/Copilot rename to docs/articles/expensify-classic/account-settings/Copilot.md From 09f3a5f73b343a93282da0ab747c4db7be3a5148 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 3 Oct 2023 10:40:30 +0700 Subject: [PATCH 006/312] restore hard code --- .../report/ReportActionCompose/AttachmentPickerWithMenuItems.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index 10f29615c904..6dd3355f4a53 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -243,7 +243,7 @@ function AttachmentPickerWithMenuItems({ onAddActionPressed(); // Drop focus to avoid blue focus ring. - // actionButtonRef.current.blur(); + actionButtonRef.current.blur(); setMenuVisibility(!isMenuVisible); }} style={styles.composerSizeButton} From d21ae8d45a1a5563c6e959872633a0b961cbfac7 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 3 Oct 2023 17:49:39 +0800 Subject: [PATCH 007/312] Add accessibility translation for floating action button --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 81e1c22b0ccc..7b6cfc372c48 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -458,7 +458,7 @@ export default { buttonSearch: 'Buscar', buttonMySettings: 'Mi configuración', fabNewChat: 'Enviar mensaje', - fabNewChatExplained: 'Enviar mensaje', + fabNewChatExplained: 'Enviar mensaje (Acción flotante)', chatPinned: 'Chat fijado', draftedMessage: 'Mensaje borrador', listOfChatMessages: 'Lista de mensajes del chat', From b0fa108a9ef097ebc5358d8576c6173bc9cc16a0 Mon Sep 17 00:00:00 2001 From: April Bekkala Date: Wed, 4 Oct 2023 11:03:02 -0500 Subject: [PATCH 008/312] Update Copilot.md https://github.com/Expensify/App/pull/28361#pullrequestreview-1656620957 made necessary edit noted in above comment --- .../expensify-classic/account-settings/Copilot.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/articles/expensify-classic/account-settings/Copilot.md b/docs/articles/expensify-classic/account-settings/Copilot.md index dbd26af12d88..4fac402b7ced 100644 --- a/docs/articles/expensify-classic/account-settings/Copilot.md +++ b/docs/articles/expensify-classic/account-settings/Copilot.md @@ -2,15 +2,11 @@ title: Copilot description: Safely delegate tasks without sharing login information. --- - # About - # How-to - # Deep Dive - # FAQ - From 67125d17037f34d45c35d77862148cfd08995638 Mon Sep 17 00:00:00 2001 From: Pierre Michel Date: Wed, 4 Oct 2023 10:08:53 -0600 Subject: [PATCH 009/312] The merge was wrong Signed-off-by: Pierre Michel --- src/components/ReportActionItem/TaskPreview.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 79d016fa297f..6fbe2c175799 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -57,16 +57,17 @@ const propTypes = { ownerAccountID: PropTypes.number, }), - /* Onyx Props */ /** chatReport associated with taskReport */ chatReport: reportPropTypes, - + /** Popover context menu anchor, used for showing context menu */ contextMenuAnchor: refPropTypes, /** Callback for updating context menu active state, used for showing context menu */ checkIfContextMenuActive: PropTypes.func, - ...withLocalizePropTypes, + + /* Onyx Props */ + ...withLocalizePropTypes, ...withCurrentUserPersonalDetailsPropTypes, }; @@ -87,8 +88,7 @@ function TaskPreview(props) { : props.action.childStateNum === CONST.REPORT.STATE_NUM.SUBMITTED && props.action.childStatusNum === CONST.REPORT.STATUS.APPROVED; const taskTitle = props.taskReport.reportName || props.action.childReportName; const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(props.taskReport) || props.action.childManagerAccountID; - const taskAssignee = lodashGet(props.personalDetailsList, [taskAssigneeAccountID, 'login'], lodashGet(props.personalDetailsList, [taskAssigneeAccountID, 'displayName'], '')); - const htmlForTaskPreview = taskAssignee ? `@${taskAssignee} ${taskTitle}` : `${taskTitle}`; + const assigneeLogin = lodashGet(props.personalDetailsList, [taskAssigneeAccountID, 'login'], ''); const assigneeDisplayName = lodashGet(props.personalDetailsList, [taskAssigneeAccountID, 'displayName'], ''); const taskAssignee = assigneeDisplayName || LocalePhoneNumber.formatPhoneNumber(assigneeLogin); const htmlForTaskPreview = From eb4fe936253d86232d0e096ca4c6328471d95e96 Mon Sep 17 00:00:00 2001 From: Pierre Michel Date: Wed, 4 Oct 2023 11:09:07 -0600 Subject: [PATCH 010/312] We use chatReportID not chatReport Signed-off-by: Pierre Michel --- src/components/ReportActionItem/TaskPreview.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 6fbe2c175799..3635e8dbb24d 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -57,8 +57,8 @@ const propTypes = { ownerAccountID: PropTypes.number, }), - /** chatReport associated with taskReport */ - chatReport: reportPropTypes, + /** The chat report associated with taskReport */ + chatReportID: PropTypes.string.isRequired, /** Popover context menu anchor, used for showing context menu */ contextMenuAnchor: refPropTypes, From eb985defedccd75ff4d347cff2c7adb433e068d6 Mon Sep 17 00:00:00 2001 From: Pierre Michel Date: Wed, 4 Oct 2023 11:16:09 -0600 Subject: [PATCH 011/312] reportPropTypes is not used Signed-off-by: Pierre Michel --- src/components/ReportActionItem/TaskPreview.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 3635e8dbb24d..8c757b51790c 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -26,7 +26,6 @@ import personalDetailsPropType from '../../pages/personalDetailsPropType'; import * as Session from '../../libs/actions/Session'; import * as LocalePhoneNumber from '../../libs/LocalePhoneNumber'; import {showContextMenuForReport} from '../ShowContextMenuContext'; -import reportPropTypes from '../../pages/reportPropTypes'; import refPropTypes from '../refPropTypes'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; import ControlSelection from '../../libs/ControlSelection'; From 9e54973035bd998503c3bfaad497b1412f729a52 Mon Sep 17 00:00:00 2001 From: Sofie de Vreese <40040992+SofiedeVreese@users.noreply.github.com> Date: Tue, 10 Oct 2023 01:38:34 +0000 Subject: [PATCH 012/312] Update Auto-Reconciliation.md Updating article to add image placeholder per https://stackoverflowteams.com/c/expensify/questions/17454/17455 --- .../expensify-classic/expensify-card/Auto-Reconciliation.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md index 9de47d6e5beb..059877a18075 100644 --- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md +++ b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md @@ -44,10 +44,14 @@ Once Auto-Reconciliation is enabled, there are a few things that happen. Let’s **What happens**: When an Expensify Card is used to make purchases, the amount spent is automatically deducted from your company’s 'Settlement Account' (your business checking account). This deduction happens on a daily or monthly basis, depending on your chosen settlement frequency. Don't worry; this settlement account is pre-defined when you apply for the Expensify Card, and you can't accidentally change it. **Accounting treatment**: After your card balance is settled each day, we update your accounting system with a journal entry. This entry credits your bank account (referred to as the GL account) and debits the Expensify Card Clearing Account. To ensure accuracy, please make sure that the 'bank account' in your Expensify Card settings matches your real-life settlement account. You can easily verify this by navigating to **Settings > Account > Payments**, where you'll see 'Settlement Account' next to your business bank account. To keep track of settlement figures by date, use the Company Card Reconciliation Dashboard's Settlements tab: +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/image-name.png){:width="100%"} + ### Submitting, Approving, and Exporting Expenses **What happens**: Users submit their expenses on a report, which might occur after some time has passed since the initial purchase. Once the report is approved, it's then exported to your accounting software. **Accounting treatment**: When the report is exported, we create a journal entry in your accounting system. This entry credits the Clearing Account and debits the Liability Account for the purchase amount. The Liability Account functions as a bank account in your ledger, specifically for Expensify Card expenses: +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/image-name.png){:width="100%"} + # Deep Dive ## QuickBooks Online From c92ab2ef56ad8e714cdc0f67b73e1cb09829197d Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 11 Oct 2023 09:01:05 +0700 Subject: [PATCH 013/312] fix anonymous user can edit profile --- src/libs/Navigation/NavigationRoot.js | 6 ++++++ src/libs/actions/Session/index.js | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index 34a52adfeca9..d8f083b9c59b 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -12,6 +12,7 @@ import StatusBar from '../StatusBar'; import useCurrentReportID from '../../hooks/useCurrentReportID'; import useWindowDimensions from '../../hooks/useWindowDimensions'; import {SidebarNavigationContext} from '../../pages/home/sidebar/SidebarNavigationContext'; +import * as Session from '../actions/Session'; // https://reactnavigation.org/docs/themes const navigationTheme = { @@ -133,6 +134,11 @@ function NavigationRoot(props) { // Update the global navigation to show the correct selected menu items. globalNavigation.updateFromNavigationState(state); + + const route = Navigation.getActiveRoute(); + if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route)) { + Session.signOutAndRedirectToSignIn(); + } }; return ( diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 117a092c3875..30c5f3320e08 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -787,6 +787,20 @@ function waitForUserSignIn() { }); } +/** + * check if the route can be accessed by anonymous user + * + * @param {string} route + */ + +const canAccessRouteByAnonymousUser = (route) => { + const reportID = ReportUtils.getReportIDFromLink(route); + if (reportID) { + return true; + } + return false; +}; + export { beginSignIn, beginAppleSignIn, @@ -815,4 +829,5 @@ export { toggleTwoFactorAuth, validateTwoFactorAuth, waitForUserSignIn, + canAccessRouteByAnonymousUser, }; From fa0fc433829dc7e5749a7047ede3851cc3315ab9 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 11 Oct 2023 13:25:01 +0700 Subject: [PATCH 014/312] fix update util function --- src/libs/ReportUtils.js | 1 + src/libs/actions/Session/index.js | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index b5fc0bff6ec7..ffb34355845f 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3959,4 +3959,5 @@ export { getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, isReportDraft, + parseReportRouteParams }; diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 30c5f3320e08..b52f172f2efa 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -798,6 +798,19 @@ const canAccessRouteByAnonymousUser = (route) => { if (reportID) { return true; } + const parsedReportRouteParams = ReportUtils.parseReportRouteParams(route); + let routeRemovedReportId = route; + if (parsedReportRouteParams.reportID) { + routeRemovedReportId = route.replace(lodashGet(parsedReportRouteParams, 'reportID', ''), ':reportID'); + } + if (route.startsWith('/')) { + routeRemovedReportId = routeRemovedReportId.slice(1); + } + const routesCanAccessByAnonymousUser = [ROUTES.SIGN_IN_MODAL, ROUTES.REPORT_WITH_ID_DETAILS.route, ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.route]; + + if (_.contains(routesCanAccessByAnonymousUser, routeRemovedReportId)) { + return true; + } return false; }; From 78ba2a46d5bfb9f2fce3a22f2c786d9810cd3565 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 11 Oct 2023 14:26:11 +0700 Subject: [PATCH 015/312] fix lint issue --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index ffb34355845f..50df076ba043 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3959,5 +3959,5 @@ export { getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, isReportDraft, - parseReportRouteParams + parseReportRouteParams, }; From 709d48fbab1fce7dfc3936ad3b3a81c8b5a0e1d6 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 13 Oct 2023 13:42:14 +0700 Subject: [PATCH 016/312] fix dissmiss modal login when clicking on back button --- src/pages/signin/SignInModal.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/signin/SignInModal.js b/src/pages/signin/SignInModal.js index f1ce09def084..0ca8fa6838b2 100644 --- a/src/pages/signin/SignInModal.js +++ b/src/pages/signin/SignInModal.js @@ -24,7 +24,11 @@ function SignInModal() { shouldEnableMaxHeight testID={SignInModal.displayName} > - + { + Navigation.dismissModal(); + }} + /> ); From 0d08b325058d0a96f2bb543150d268f786f1a9e4 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 13 Oct 2023 13:45:17 +0700 Subject: [PATCH 017/312] fix refactor code --- src/pages/signin/SignInModal.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/signin/SignInModal.js b/src/pages/signin/SignInModal.js index 0ca8fa6838b2..98bd0692298c 100644 --- a/src/pages/signin/SignInModal.js +++ b/src/pages/signin/SignInModal.js @@ -24,11 +24,7 @@ function SignInModal() { shouldEnableMaxHeight testID={SignInModal.displayName} > - { - Navigation.dismissModal(); - }} - /> + ); From 2cadcf65de4bb5d60e9c304ebddee04effb87978 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 13 Oct 2023 14:36:51 +0200 Subject: [PATCH 018/312] [TS migration] Migrate 'withPolicy.js' HOC to TypeScript --- .eslintrc.js | 2 +- src/libs/getComponentDisplayName.ts | 2 +- .../{withPolicy.js => withPolicy.tsx} | 87 ++++++++----------- 3 files changed, 40 insertions(+), 51 deletions(-) rename src/pages/workspace/{withPolicy.js => withPolicy.tsx} (58%) diff --git a/.eslintrc.js b/.eslintrc.js index 75a74ed371c4..83e9479ce0c4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -116,7 +116,7 @@ module.exports = { }, { selector: ['parameter', 'method'], - format: ['camelCase'], + format: ['camelCase', 'PascalCase'], }, ], '@typescript-eslint/ban-types': [ diff --git a/src/libs/getComponentDisplayName.ts b/src/libs/getComponentDisplayName.ts index fd1bbcaea521..0bf52d543a84 100644 --- a/src/libs/getComponentDisplayName.ts +++ b/src/libs/getComponentDisplayName.ts @@ -1,6 +1,6 @@ import {ComponentType} from 'react'; /** Returns the display name of a component */ -export default function getComponentDisplayName(component: ComponentType): string { +export default function getComponentDisplayName(component: ComponentType): string { return component.displayName ?? component.name ?? 'Component'; } diff --git a/src/pages/workspace/withPolicy.js b/src/pages/workspace/withPolicy.tsx similarity index 58% rename from src/pages/workspace/withPolicy.js rename to src/pages/workspace/withPolicy.tsx index b1659ea2b7a6..94731b2dc650 100644 --- a/src/pages/workspace/withPolicy.js +++ b/src/pages/workspace/withPolicy.tsx @@ -1,21 +1,22 @@ -import _ from 'underscore'; -import lodashGet from 'lodash/get'; -import React from 'react'; +import React, {ComponentType, ForwardedRef, RefAttributes, forwardRef} from 'react'; import PropTypes from 'prop-types'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import {useNavigationState} from '@react-navigation/native'; import CONST from '../../CONST'; import getComponentDisplayName from '../../libs/getComponentDisplayName'; import * as Policy from '../../libs/actions/Policy'; import ONYXKEYS from '../../ONYXKEYS'; import policyMemberPropType from '../policyMemberPropType'; +import * as OnyxTypes from '../../types/onyx'; -/** - * @param {Object} route - * @returns {String} - */ -function getPolicyIDFromRoute(route) { - return lodashGet(route, 'params.policyID', ''); +type PolicyRoute = { + params?: { + policyID: string; + }; +}; + +function getPolicyIDFromRoute(route: PolicyRoute): string { + return route?.params?.policyID ?? 'N/A'; } const policyPropTypes = { @@ -28,10 +29,10 @@ const policyPropTypes = { name: PropTypes.string, /** The current user's role in the policy */ - role: PropTypes.oneOf(_.values(CONST.POLICY.ROLE)), + role: PropTypes.oneOf(Object.values(CONST.POLICY.ROLE)), /** The policy type */ - type: PropTypes.oneOf(_.values(CONST.POLICY.TYPE)), + type: PropTypes.oneOf(Object.values(CONST.POLICY.TYPE)), /** The email of the policy owner */ owner: PropTypes.string, @@ -61,66 +62,54 @@ const policyPropTypes = { policyMembers: PropTypes.objectOf(policyMemberPropType), }; -const policyDefaultProps = { - policy: {}, +const policyDefaultProps: WithPolicyOnyxProps = { + policy: {} as OnyxTypes.Policy, policyMembers: {}, }; +type WithPolicyOnyxProps = { + policy: OnyxEntry; + policyMembers: OnyxEntry; +}; + +type WithPolicyProps = WithPolicyOnyxProps & { + route: PolicyRoute; +}; /* * HOC for connecting a policy in Onyx corresponding to the policyID in route params */ -export default function (WrappedComponent) { - const propTypes = { - /** The HOC takes an optional ref as a prop and passes it as a ref to the wrapped component. - * That way, if a ref is passed to a component wrapped in the HOC, the ref is a reference to the wrapped component, not the HOC. */ - forwardedRef: PropTypes.func, - - ...policyPropTypes, - }; - - const defaultProps = { - forwardedRef: () => {}, - - ...policyDefaultProps, - }; - - function WithPolicy(props) { - const currentRoute = _.last(useNavigationState((state) => state.routes || [])); - const policyID = getPolicyIDFromRoute(currentRoute); - - if (_.isString(policyID) && !_.isEmpty(policyID)) { +export default function withPolicy( + WrappedComponent: ComponentType>, +): React.ComponentType> { + function WithPolicy(props: TProps, ref: ForwardedRef) { + const routes = useNavigationState((state) => state.routes || []); + const currentRoute = routes?.[routes.length - 1]; + const policyID = getPolicyIDFromRoute(currentRoute as PolicyRoute); + + if (typeof policyID === 'string' && policyID.length > 0) { Policy.updateLastAccessedWorkspace(policyID); } - const rest = _.omit(props, ['forwardedRef']); return ( ); } - WithPolicy.propTypes = propTypes; - WithPolicy.defaultProps = defaultProps; + WithPolicy.defaultProps = policyDefaultProps; WithPolicy.displayName = `withPolicy(${getComponentDisplayName(WrappedComponent)})`; - const withPolicy = React.forwardRef((props, ref) => ( - - )); - - return withOnyx({ + + return withOnyx({ policy: { key: (props) => `${ONYXKEYS.COLLECTION.POLICY}${getPolicyIDFromRoute(props.route)}`, }, policyMembers: { key: (props) => `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${getPolicyIDFromRoute(props.route)}`, }, - })(withPolicy); + })(forwardRef(WithPolicy)); } export {policyPropTypes, policyDefaultProps}; From 98f89ada062eec261414497e82626cd79bb8f783 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Oct 2023 14:02:53 +0200 Subject: [PATCH 019/312] [TS migration] Migrate 'SafeAreaConsumer.js' component to TypeScript --- ...feAreaConsumer.js => SafeAreaConsumer.tsx} | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) rename src/components/{SafeAreaConsumer.js => SafeAreaConsumer.tsx} (55%) diff --git a/src/components/SafeAreaConsumer.js b/src/components/SafeAreaConsumer.tsx similarity index 55% rename from src/components/SafeAreaConsumer.js rename to src/components/SafeAreaConsumer.tsx index 78d7426ba380..c2439e25ecd3 100644 --- a/src/components/SafeAreaConsumer.js +++ b/src/components/SafeAreaConsumer.tsx @@ -1,29 +1,34 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import {EdgeInsets, SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import {DimensionValue} from 'react-native'; import * as StyleUtils from '../styles/StyleUtils'; -const propTypes = { - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, +type ChildrenProps = { + paddingTop?: DimensionValue; + paddingBottom?: DimensionValue; + insets?: EdgeInsets; + safeAreaPaddingBottomStyle: { + paddingBottom?: DimensionValue; + }; +}; + +type SafeAreaConsumerProps = { + children: (props: ChildrenProps) => React.ReactNode; }; /** * This component is a light wrapper around the SafeAreaInsetsContext.Consumer. There are several places where we * may need not just the insets, but the computed styles so we save a few lines of code with this. - * - * @param {Object} props - * @returns {React.Component} */ -function SafeAreaConsumer(props) { +function SafeAreaConsumer({children}: SafeAreaConsumerProps) { return ( {(insets) => { - const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets); - return props.children({ + const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets ?? undefined); + return children({ paddingTop, paddingBottom, - insets, + insets: insets ?? undefined, safeAreaPaddingBottomStyle: {paddingBottom}, }); }} @@ -32,5 +37,5 @@ function SafeAreaConsumer(props) { } SafeAreaConsumer.displayName = 'SafeAreaConsumer'; -SafeAreaConsumer.propTypes = propTypes; + export default SafeAreaConsumer; From cb3f3d0bb0011e7531c90eee29dd4c0e5d05bc49 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 17 Oct 2023 16:17:44 +0200 Subject: [PATCH 020/312] add hasOutstandingChildRequest verification --- src/libs/ReportUtils.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 011907c2c88b..c97de623ad3a 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1253,6 +1253,11 @@ function isWaitingForIOUActionFromCurrentUser(report) { return true; } + // Child report that is awaiting for current user to Pay + if (report.hasOutstandingChildRequest && report.ownerAccountID === currentUserAccountID) { + return true; + } + return false; } From 872645c276342312772f7c73029e7faa95fd533f Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 18 Oct 2023 11:55:47 +0800 Subject: [PATCH 021/312] update number of lines when window width changes --- src/components/Composer/index.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index ad7a84cc1828..14974f217aab 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -11,7 +11,6 @@ import updateIsFullComposerAvailable from '../../libs/ComposerUtils/updateIsFull import * as ComposerUtils from '../../libs/ComposerUtils'; import * as Browser from '../../libs/Browser'; import * as StyleUtils from '../../styles/StyleUtils'; -import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions'; import compose from '../../libs/compose'; import styles from '../../styles/styles'; import Text from '../Text'; @@ -19,6 +18,7 @@ import isEnterWhileComposition from '../../libs/KeyboardShortcut/isEnterWhileCom import CONST from '../../CONST'; import withNavigation from '../withNavigation'; import ReportActionComposeFocusManager from '../../libs/ReportActionComposeFocusManager'; +import useWindowDimensions from '../../hooks/useWindowDimensions'; const propTypes = { /** Maximum number of lines in the text input */ @@ -87,8 +87,6 @@ const propTypes = { isComposerFullSize: PropTypes.bool, ...withLocalizePropTypes, - - ...windowDimensionsPropTypes, }; const defaultProps = { @@ -168,6 +166,7 @@ function Composer({ isComposerFullSize, ...props }) { + const {windowWidth} = useWindowDimensions(); const textRef = useRef(null); const textInput = useRef(null); const initialValue = defaultValue ? `${defaultValue}` : `${value || ''}`; @@ -366,7 +365,7 @@ function Composer({ setNumberOfLines(generalNumberOfLines); textInput.current.style.height = 'auto'; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, maxLines, numberOfLinesProp, onNumberOfLinesChange, isFullComposerAvailable, setIsFullComposerAvailable]); + }, [value, maxLines, numberOfLinesProp, onNumberOfLinesChange, isFullComposerAvailable, setIsFullComposerAvailable, windowWidth]); useEffect(() => { updateNumberOfLines(); @@ -491,7 +490,6 @@ Composer.defaultProps = defaultProps; export default compose( withLocalize, - withWindowDimensions, withNavigation, )( React.forwardRef((props, ref) => ( From eab87d28d3a3747f68f1096e2b68f03583799a64 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 18 Oct 2023 13:05:00 +0200 Subject: [PATCH 022/312] Adjust after internal review --- src/components/SafeAreaConsumer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SafeAreaConsumer.tsx b/src/components/SafeAreaConsumer.tsx index c2439e25ecd3..dec0964b34a9 100644 --- a/src/components/SafeAreaConsumer.tsx +++ b/src/components/SafeAreaConsumer.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {EdgeInsets, SafeAreaInsetsContext} from 'react-native-safe-area-context'; -import {DimensionValue} from 'react-native'; +import type {DimensionValue} from 'react-native'; import * as StyleUtils from '../styles/StyleUtils'; type ChildrenProps = { @@ -13,7 +13,7 @@ type ChildrenProps = { }; type SafeAreaConsumerProps = { - children: (props: ChildrenProps) => React.ReactNode; + children: React.FC; }; /** From 1ea93f7b779943e8a2dc3dd384f305d7b7a0d5c9 Mon Sep 17 00:00:00 2001 From: Roksana Zawilowska Date: Wed, 18 Oct 2023 15:13:41 +0200 Subject: [PATCH 023/312] Fix: 27456 green line --- src/pages/home/report/ReportActionsList.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index c673c06470f8..93668970edb7 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -288,9 +288,13 @@ function ReportActionsList({ if (!currentUnreadMarker) { const nextMessage = sortedReportActions[index + 1]; const isCurrentMessageUnread = isMessageUnread(reportAction, report.lastReadTime); - shouldDisplay = isCurrentMessageUnread && !isMessageUnread(nextMessage, report.lastReadTime); - if (!messageManuallyMarkedUnread) { - shouldDisplay = shouldDisplay && reportAction.actorAccountID !== Report.getCurrentUserAccountID(); + const isNextMessageRead = !isMessageUnread(nextMessage, report.lastReadTime); + const isWithinVisibleThreshold = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD ? reportAction.created < userActiveSince.current : true; + + shouldDisplay = isCurrentMessageUnread && isNextMessageRead && isWithinVisibleThreshold; + + if (shouldDisplay && !messageManuallyMarkedUnread) { + shouldDisplay = reportAction.actorAccountID !== Report.getCurrentUserAccountID(); } } else { shouldDisplay = reportAction.reportActionID === currentUnreadMarker; From ddba0ece2d3b77764cc3af4b3e86635ebbe85876 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Wed, 18 Oct 2023 19:07:30 +0200 Subject: [PATCH 024/312] adjusted and removed unused parts --- src/components/LHNOptionsList/OptionRowLHN.js | 3 +- src/libs/ReportUtils.js | 31 +++---------------- src/libs/SidebarUtils.js | 2 +- tests/unit/ReportUtilsTest.js | 20 ++++++------ 4 files changed, 17 insertions(+), 39 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index ba035c8b3baf..064c1c146163 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -116,8 +116,7 @@ function OptionRowLHN(props) { const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; const defaultSubscriptSize = optionItem.isExpenseRequest ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; - const shouldShowGreenDotIndicator = - !hasBrickError && (optionItem.isUnreadWithMention || optionItem.isWaitingForTaskCompleteFromAssignee || ReportUtils.isWaitingForIOUActionFromCurrentUser(optionItem)); + const shouldShowGreenDotIndicator = !hasBrickError && (optionItem.isUnreadWithMention || optionItem.isWaitingForTaskCompleteFromAssignee || ReportUtils.shouldShowGBR(optionItem)); /** * Show the ReportActionContextMenu modal popover. diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index c97de623ad3a..c8339810cb1f 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1213,12 +1213,12 @@ function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantR } /** - * Determines if a report has an IOU that is waiting for an action from the current user (either Pay or Add a credit bank account) + * Determines if a report child has an outstanding request that is waiting for an action from the current user (either Pay or Add a credit bank account) * * @param {Object} report (chatReport or iouReport) * @returns {boolean} */ -function isWaitingForIOUActionFromCurrentUser(report) { +function shouldShowGBR(report) { if (!report) { return false; } @@ -1227,34 +1227,13 @@ function isWaitingForIOUActionFromCurrentUser(report) { return false; } - const policy = getPolicy(report.policyID); - if (policy.type === CONST.POLICY.TYPE.CORPORATE) { - // If the report is already settled, there's no action required from any user. - if (isSettled(report.reportID)) { - return false; - } - - // Report is pending approval and the current user is the manager - if (isReportManager(report) && !isReportApproved(report)) { - return true; - } - - // Current user is an admin and the report has been approved but not settled yet - return policy.role === CONST.POLICY.ROLE.ADMIN && isReportApproved(report); - } - // Money request waiting for current user to add their credit bank account if (report.hasOutstandingIOU && report.ownerAccountID === currentUserAccountID && report.isWaitingOnBankAccount) { return true; } - // Money request waiting for current user to Pay (from expense or iou report) - if (report.hasOutstandingIOU && report.ownerAccountID && (report.ownerAccountID !== currentUserAccountID || currentUserAccountID === report.managerID)) { - return true; - } - // Child report that is awaiting for current user to Pay - if (report.hasOutstandingChildRequest && report.ownerAccountID === currentUserAccountID) { + if (report.hasOutstandingChildRequest) { return true; } @@ -3141,7 +3120,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, } // Include reports that are relevant to the user in any view mode. Criteria include having a draft, having an outstanding IOU, or being assigned to an open task. - if (report.hasDraft || isWaitingForIOUActionFromCurrentUser(report) || isWaitingForTaskCompleteFromAssignee(report)) { + if (report.hasDraft || shouldShowGBR(report) || isWaitingForTaskCompleteFromAssignee(report)) { return true; } const lastVisibleMessage = ReportActionsUtils.getLastVisibleMessage(report.reportID); @@ -3953,7 +3932,7 @@ export { isCurrentUserTheOnlyParticipant, hasAutomatedExpensifyAccountIDs, hasExpensifyGuidesEmails, - isWaitingForIOUActionFromCurrentUser, + shouldShowGBR, isIOUOwnedByCurrentUser, getMoneyRequestReimbursableTotal, getMoneyRequestSpendBreakdown, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index dd6db33902fb..bc915c4b16fa 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -179,7 +179,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p reportsToDisplay.forEach((report) => { if (report.isPinned) { pinnedReports.push(report); - } else if (ReportUtils.isWaitingForIOUActionFromCurrentUser(report)) { + } else if (ReportUtils.shouldShowGBR(report)) { outstandingIOUReports.push(report); } else if (report.hasDraft) { draftReports.push(report); diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index c6afde7d9161..10a1389d6cf9 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -246,9 +246,9 @@ describe('ReportUtils', () => { }); }); - describe('isWaitingForIOUActionFromCurrentUser', () => { + describe('shouldShowGBR', () => { it('returns false when there is no report', () => { - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser()).toBe(false); + expect(ReportUtils.shouldShowGBR()).toBe(false); }); it('returns false when the matched IOU report does not have an owner accountID', () => { const report = { @@ -256,7 +256,7 @@ describe('ReportUtils', () => { ownerAccountID: undefined, hasOutstandingIOU: true, }; - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); + expect(ReportUtils.shouldShowGBR(report)).toBe(false); }); it('returns false when the linked iou report has an oustanding IOU', () => { const report = { @@ -268,7 +268,7 @@ describe('ReportUtils', () => { ownerAccountID: 99, hasOutstandingIOU: true, }).then(() => { - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); + expect(ReportUtils.shouldShowGBR(report)).toBe(false); }); }); it('returns false when the report has no oustanding IOU but is waiting for a bank account and the logged user is the report owner', () => { @@ -278,7 +278,7 @@ describe('ReportUtils', () => { ownerAccountID: currentUserAccountID, isWaitingOnBankAccount: true, }; - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); + expect(ReportUtils.shouldShowGBR(report)).toBe(false); }); it('returns true when the report has oustanding IOU and is waiting for a bank account and the logged user is the report owner', () => { const report = { @@ -287,7 +287,7 @@ describe('ReportUtils', () => { ownerAccountID: currentUserAccountID, isWaitingOnBankAccount: true, }; - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(true); + expect(ReportUtils.shouldShowGBR(report)).toBe(true); }); it('returns false when the report has no oustanding IOU but is waiting for a bank account and the logged user is not the report owner', () => { const report = { @@ -296,16 +296,16 @@ describe('ReportUtils', () => { ownerAccountID: 97, isWaitingOnBankAccount: true, }; - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); + expect(ReportUtils.shouldShowGBR(report)).toBe(false); }); - it('returns true when the report has oustanding IOU', () => { + it('returns true when the report has oustanding child request', () => { const report = { ...LHNTestUtils.getFakeReport(), ownerAccountID: 99, - hasOutstandingIOU: true, + hasOutstandingChildRequest: true, isWaitingOnBankAccount: false, }; - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(true); + expect(ReportUtils.shouldShowGBR(report)).toBe(true); }); }); From 03af64861ae3e64fc41cfc4c19274b13f51f68af Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 19 Oct 2023 00:56:31 +0700 Subject: [PATCH 025/312] fix sign in modal appear for a second --- src/libs/Navigation/NavigationRoot.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index a920bfeeca9e..b39e5bbb0d5f 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -13,6 +13,8 @@ import useCurrentReportID from '../../hooks/useCurrentReportID'; import useWindowDimensions from '../../hooks/useWindowDimensions'; import {SidebarNavigationContext} from '../../pages/home/sidebar/SidebarNavigationContext'; import * as Session from '../actions/Session'; +import getCurrentUrl from './currentUrl'; +import ROUTES from '../../ROUTES'; // https://reactnavigation.org/docs/themes const navigationTheme = { @@ -102,7 +104,7 @@ function NavigationRoot(props) { const animateStatusBarBackgroundColor = () => { const currentRoute = navigationRef.getCurrentRoute(); - const currentScreenBackgroundColor = (currentRoute.params && currentRoute.params.backgroundColor) || themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG; + const currentScreenBackgroundColor = themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG; prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current; statusBarBackgroundColor.current = currentScreenBackgroundColor; @@ -136,7 +138,7 @@ function NavigationRoot(props) { globalNavigation.updateFromNavigationState(state); const route = Navigation.getActiveRoute(); - if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route)) { + if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route) && !getCurrentUrl().includes(ROUTES.SIGN_IN_MODAL)) { Session.signOutAndRedirectToSignIn(); } }; From 0ac3e98f814025b0df91c793c1352e94a218f190 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 19 Oct 2023 01:02:12 +0700 Subject: [PATCH 026/312] fix revert unrelated change --- src/libs/Navigation/NavigationRoot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index b39e5bbb0d5f..a22b6714a306 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -104,7 +104,7 @@ function NavigationRoot(props) { const animateStatusBarBackgroundColor = () => { const currentRoute = navigationRef.getCurrentRoute(); - const currentScreenBackgroundColor = themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG; + const currentScreenBackgroundColor = (currentRoute.params && currentRoute.params.backgroundColor) || themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG; prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current; statusBarBackgroundColor.current = currentScreenBackgroundColor; From 9b074dd4a52930ba1acffed0adeedc0832a84a6c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 19 Oct 2023 10:34:23 +0200 Subject: [PATCH 027/312] Add new onyx values --- src/ONYXKEYS.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ad8b60700e39..c761b540bc22 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -382,9 +382,11 @@ type OnyxValues = { // Collections [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; [ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy; + [ONYXKEYS.COLLECTION.POLICY_DRAFTS]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory; [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTag; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember; + [ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; From 55b66073e64fb1f28c62020cf2521107700f4cbc Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 19 Oct 2023 12:29:20 +0200 Subject: [PATCH 028/312] migrate KeyboardAvoidingView to TypeScript --- .../KeyboardAvoidingView/{index.ios.js => index.ios.tsx} | 4 ++-- .../KeyboardAvoidingView/{index.js => index.tsx} | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) rename src/components/KeyboardAvoidingView/{index.ios.js => index.ios.tsx} (60%) rename src/components/KeyboardAvoidingView/{index.js => index.tsx} (50%) diff --git a/src/components/KeyboardAvoidingView/index.ios.js b/src/components/KeyboardAvoidingView/index.ios.tsx similarity index 60% rename from src/components/KeyboardAvoidingView/index.ios.js rename to src/components/KeyboardAvoidingView/index.ios.tsx index c1ea8687f793..4ab04a52a4cf 100644 --- a/src/components/KeyboardAvoidingView/index.ios.js +++ b/src/components/KeyboardAvoidingView/index.ios.tsx @@ -2,9 +2,9 @@ * The KeyboardAvoidingView is only used on ios */ import React from 'react'; -import {KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native'; +import {KeyboardAvoidingView as KeyboardAvoidingViewComponent, KeyboardAvoidingViewProps} from 'react-native'; -function KeyboardAvoidingView(props) { +function KeyboardAvoidingView(props: KeyboardAvoidingViewProps): React.ReactNode { // eslint-disable-next-line react/jsx-props-no-spreading return ; } diff --git a/src/components/KeyboardAvoidingView/index.js b/src/components/KeyboardAvoidingView/index.tsx similarity index 50% rename from src/components/KeyboardAvoidingView/index.js rename to src/components/KeyboardAvoidingView/index.tsx index 3483b2d007ac..f86f73622515 100644 --- a/src/components/KeyboardAvoidingView/index.js +++ b/src/components/KeyboardAvoidingView/index.tsx @@ -2,14 +2,13 @@ * The KeyboardAvoidingView is only used on ios */ import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; +import {KeyboardAvoidingViewProps, View} from 'react-native'; -function KeyboardAvoidingView(props) { - const viewProps = _.omit(props, ['behavior', 'contentContainerStyle', 'enabled', 'keyboardVerticalOffset']); +function KeyboardAvoidingView(props: KeyboardAvoidingViewProps): React.ReactNode { + const {behavior, contentContainerStyle, enabled, keyboardVerticalOffset, ...rest} = props; return ( // eslint-disable-next-line react/jsx-props-no-spreading - + ); } From 134318a8d22a0e1e7accb7e286b8728ad4e53b2e Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 19 Oct 2023 14:55:40 +0200 Subject: [PATCH 029/312] remove component return type --- src/components/KeyboardAvoidingView/index.ios.tsx | 2 +- src/components/KeyboardAvoidingView/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/KeyboardAvoidingView/index.ios.tsx b/src/components/KeyboardAvoidingView/index.ios.tsx index 4ab04a52a4cf..076dcef3e158 100644 --- a/src/components/KeyboardAvoidingView/index.ios.tsx +++ b/src/components/KeyboardAvoidingView/index.ios.tsx @@ -4,7 +4,7 @@ import React from 'react'; import {KeyboardAvoidingView as KeyboardAvoidingViewComponent, KeyboardAvoidingViewProps} from 'react-native'; -function KeyboardAvoidingView(props: KeyboardAvoidingViewProps): React.ReactNode { +function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { // eslint-disable-next-line react/jsx-props-no-spreading return ; } diff --git a/src/components/KeyboardAvoidingView/index.tsx b/src/components/KeyboardAvoidingView/index.tsx index f86f73622515..4cff2a2399af 100644 --- a/src/components/KeyboardAvoidingView/index.tsx +++ b/src/components/KeyboardAvoidingView/index.tsx @@ -4,7 +4,7 @@ import React from 'react'; import {KeyboardAvoidingViewProps, View} from 'react-native'; -function KeyboardAvoidingView(props: KeyboardAvoidingViewProps): React.ReactNode { +function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { const {behavior, contentContainerStyle, enabled, keyboardVerticalOffset, ...rest} = props; return ( // eslint-disable-next-line react/jsx-props-no-spreading From bd2274c63dd1c23c8bb509949a44ab6e72d7e75f Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 19 Oct 2023 16:20:14 +0200 Subject: [PATCH 030/312] WIP --- src/components/FullscreenLoadingIndicator.js | 33 ------------------- src/components/FullscreenLoadingIndicator.tsx | 22 +++++++++++++ 2 files changed, 22 insertions(+), 33 deletions(-) delete mode 100644 src/components/FullscreenLoadingIndicator.js create mode 100644 src/components/FullscreenLoadingIndicator.tsx diff --git a/src/components/FullscreenLoadingIndicator.js b/src/components/FullscreenLoadingIndicator.js deleted file mode 100644 index 5c212b6dc29e..000000000000 --- a/src/components/FullscreenLoadingIndicator.js +++ /dev/null @@ -1,33 +0,0 @@ -import _ from 'underscore'; -import React from 'react'; -import {ActivityIndicator, StyleSheet, View} from 'react-native'; -import styles from '../styles/styles'; -import themeColors from '../styles/themes/default'; -import stylePropTypes from '../styles/stylePropTypes'; - -const propTypes = { - /** Additional style props */ - style: stylePropTypes, -}; - -const defaultProps = { - style: [], -}; - -function FullScreenLoadingIndicator(props) { - const additionalStyles = _.isArray(props.style) ? props.style : [props.style]; - return ( - - - - ); -} - -FullScreenLoadingIndicator.propTypes = propTypes; -FullScreenLoadingIndicator.defaultProps = defaultProps; -FullScreenLoadingIndicator.displayName = 'FullScreenLoadingIndicator'; - -export default FullScreenLoadingIndicator; diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx new file mode 100644 index 000000000000..3f5f62f533d7 --- /dev/null +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import {ActivityIndicator, StyleSheet, View} from 'react-native'; +import styles from '../styles/styles'; +import themeColors from '../styles/themes/default'; + +type FullScreenLoadingIndicatorProps = { + style: Record | Array> | (() => void); +}; + +function FullScreenLoadingIndicator({style = []}: FullScreenLoadingIndicatorProps) { + const additionalStyles = Array.isArray(style) ? style : [style]; + return ( + + + + ); +} + +export default FullScreenLoadingIndicator; From 9d1cd9d436023ac07fde031abc93a3f2eecb54d3 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Thu, 19 Oct 2023 16:35:10 +0200 Subject: [PATCH 031/312] remove moment from datepicker --- src/CONST.ts | 1 - .../DatePicker/datepickerPropTypes.js | 4 +- src/components/DatePicker/index.android.js | 10 ++--- src/components/DatePicker/index.ios.js | 10 ++--- src/components/DatePicker/index.js | 12 +++--- .../NewDatePicker/CalendarPicker/index.js | 40 +++++++++---------- src/components/NewDatePicker/index.js | 10 ++--- src/libs/DateUtils.ts | 36 +++++++++++++++++ tests/unit/CalendarPickerTest.js | 11 +---- 9 files changed, 81 insertions(+), 53 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index bc74cbe77717..6a52563d238b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -131,7 +131,6 @@ const CONST = { DESKTOP: `${ACTIVE_EXPENSIFY_URL}NewExpensify.dmg`, }, DATE: { - MOMENT_FORMAT_STRING: 'YYYY-MM-DD', SQL_DATE_TIME: 'YYYY-MM-DD HH:mm:ss', FNS_FORMAT_STRING: 'yyyy-MM-dd', LOCAL_TIME_FORMAT: 'h:mm a', diff --git a/src/components/DatePicker/datepickerPropTypes.js b/src/components/DatePicker/datepickerPropTypes.js index 8bd5d890c42c..f896023d386b 100644 --- a/src/components/DatePicker/datepickerPropTypes.js +++ b/src/components/DatePicker/datepickerPropTypes.js @@ -6,13 +6,13 @@ const propTypes = { ...fieldPropTypes, /** - * The datepicker supports any value that `moment` can parse. + * The datepicker supports any value that `new Date()` can parse. * `onInputChange` would always be called with a Date (or null) */ value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), /** - * The datepicker supports any defaultValue that `moment` can parse. + * The datepicker supports any defaultValue that `new Date()` can parse. * `onInputChange` would always be called with a Date (or null) */ defaultValue: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), diff --git a/src/components/DatePicker/index.android.js b/src/components/DatePicker/index.android.js index 5bdda580d357..24faf2b19745 100644 --- a/src/components/DatePicker/index.android.js +++ b/src/components/DatePicker/index.android.js @@ -1,7 +1,7 @@ import React from 'react'; import {Keyboard} from 'react-native'; import RNDatePicker from '@react-native-community/datetimepicker'; -import moment from 'moment'; +import {format} from 'date-fns'; import _ from 'underscore'; import TextInput from '../TextInput'; import CONST from '../../CONST'; @@ -28,8 +28,7 @@ class DatePicker extends React.Component { this.setState({isPickerVisible: false}); if (event.type === 'set') { - const asMoment = moment(selectedDate, true); - this.props.onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING)); + this.props.onInputChange(format(selectedDate, CONST.DATE.FNS_FORMAT_STRING)); } } @@ -39,7 +38,8 @@ class DatePicker extends React.Component { } render() { - const dateAsText = this.props.value || this.props.defaultValue ? moment(this.props.value || this.props.defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : ''; + const date = this.props.value || this.props.defaultValue; + const dateAsText = date ? format(new Date(date), CONST.DATE.FNS_FORMAT_STRING) : ''; return ( <> @@ -73,7 +73,7 @@ class DatePicker extends React.Component { /> {this.state.isPickerVisible && ( { setIsPickerVisible(false); - const asMoment = moment(selectedDate, true); - onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING)); + onInputChange(format(selectedDate, CONST.DATE.FNS_FORMAT_STRING)); }; /** @@ -77,7 +77,7 @@ function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLoca setSelectedDate(date); }; - const dateAsText = value || defaultValue ? moment(value || defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : ''; + const dateAsText = dateValue ? format(new Date(dateValue), CONST.DATE.FNS_FORMAT_STRING) : ''; return ( <> diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js index d14886fd1c59..e0672f847295 100644 --- a/src/components/DatePicker/index.js +++ b/src/components/DatePicker/index.js @@ -1,5 +1,5 @@ import React, {useEffect, useRef} from 'react'; -import moment from 'moment'; +import {format, isValid} from 'date-fns'; import _ from 'underscore'; import TextInput from '../TextInput'; import CONST from '../../CONST'; @@ -13,8 +13,8 @@ function DatePicker({maxDate, minDate, onInputChange, innerRef, label, value, pl useEffect(() => { // Adds nice native datepicker on web/desktop. Not possible to set this through props inputRef.current.setAttribute('type', 'date'); - inputRef.current.setAttribute('max', moment(maxDate).format(CONST.DATE.MOMENT_FORMAT_STRING)); - inputRef.current.setAttribute('min', moment(minDate).format(CONST.DATE.MOMENT_FORMAT_STRING)); + inputRef.current.setAttribute('max', format(new Date(maxDate), CONST.DATE.FNS_FORMAT_STRING)); + inputRef.current.setAttribute('min', format(new Date(minDate), CONST.DATE.FNS_FORMAT_STRING)); inputRef.current.classList.add('expensify-datepicker'); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -29,9 +29,9 @@ function DatePicker({maxDate, minDate, onInputChange, innerRef, label, value, pl return; } - const asMoment = moment(text, true); - if (asMoment.isValid()) { - onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING)); + const date = new Date(text); + if (isValid(date)) { + onInputChange(format(date, CONST.DATE.FNS_FORMAT_STRING)); } }; diff --git a/src/components/NewDatePicker/CalendarPicker/index.js b/src/components/NewDatePicker/CalendarPicker/index.js index d03c36997845..67b3ef3aa91a 100644 --- a/src/components/NewDatePicker/CalendarPicker/index.js +++ b/src/components/NewDatePicker/CalendarPicker/index.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import React from 'react'; import {View} from 'react-native'; -import moment from 'moment'; +import {setYear, format, getYear, subMonths, addMonths, startOfDay, endOfMonth, setDate, isSameDay} from 'date-fns'; import PropTypes from 'prop-types'; import Str from 'expensify-common/lib/str'; import Text from '../../Text'; @@ -11,6 +11,7 @@ import styles from '../../../styles/styles'; import generateMonthMatrix from './generateMonthMatrix'; import withLocalize, {withLocalizePropTypes} from '../../withLocalize'; import CONST from '../../../CONST'; +import DateUtils from '../../../libs/DateUtils'; import getButtonState from '../../../libs/getButtonState'; import * as StyleUtils from '../../../styles/StyleUtils'; import PressableWithFeedback from '../../Pressable/PressableWithFeedback'; @@ -34,8 +35,8 @@ const propTypes = { const defaultProps = { value: new Date(), - minDate: moment().year(CONST.CALENDAR_PICKER.MIN_YEAR).toDate(), - maxDate: moment().year(CONST.CALENDAR_PICKER.MAX_YEAR).toDate(), + minDate: setYear(new Date(), CONST.CALENDAR_PICKER.MIN_YEAR), + maxDate: setYear(new Date(), CONST.CALENDAR_PICKER.MAX_YEAR), onSelected: () => {}, }; @@ -46,16 +47,15 @@ class CalendarPicker extends React.PureComponent { if (props.minDate >= props.maxDate) { throw new Error('Minimum date cannot be greater than the maximum date.'); } - - let currentDateView = moment(props.value, CONST.DATE.MOMENT_FORMAT_STRING).toDate(); + let currentDateView = new Date(props.value); if (props.maxDate < currentDateView) { currentDateView = props.maxDate; } else if (props.minDate > currentDateView) { currentDateView = props.minDate; } - const minYear = moment(this.props.minDate).year(); - const maxYear = moment(this.props.maxDate).year(); + const minYear = getYear(new Date(this.props.minDate)); + const maxYear = getYear(new Date(this.props.maxDate)); this.state = { currentDateView, @@ -79,7 +79,7 @@ class CalendarPicker extends React.PureComponent { onYearSelected(year) { this.setState((prev) => { - const newCurrentDateView = moment(prev.currentDateView).set('year', year).toDate(); + const newCurrentDateView = setYear(new Date(prev.currentDateView), year); return { currentDateView: newCurrentDateView, @@ -99,9 +99,9 @@ class CalendarPicker extends React.PureComponent { onDayPressed(day) { this.setState( (prev) => ({ - currentDateView: moment(prev.currentDateView).set('date', day).toDate(), + currentDateView: setDate(new Date(prev.currentDateView), day), }), - () => this.props.onSelected(moment(this.state.currentDateView).format('YYYY-MM-DD')), + () => this.props.onSelected(format(new Date(this.state.currentDateView), CONST.DATE.FNS_FORMAT_STRING)), ); } @@ -109,24 +109,24 @@ class CalendarPicker extends React.PureComponent { * Handles the user pressing the previous month arrow of the calendar picker. */ moveToPrevMonth() { - this.setState((prev) => ({currentDateView: moment(prev.currentDateView).subtract(1, 'months').toDate()})); + this.setState((prev) => ({currentDateView: subMonths(new Date(prev.currentDateView), 1)})); } /** * Handles the user pressing the next month arrow of the calendar picker. */ moveToNextMonth() { - this.setState((prev) => ({currentDateView: moment(prev.currentDateView).add(1, 'months').toDate()})); + this.setState((prev) => ({currentDateView: addMonths(new Date(prev.currentDateView), 1)})); } render() { - const monthNames = _.map(moment.localeData(this.props.preferredLocale).months(), Str.recapitalize); - const daysOfWeek = _.map(moment.localeData(this.props.preferredLocale).weekdays(), (day) => day.toUpperCase()); + const monthNames = _.map(DateUtils.getMonthNames(this.props.preferredLocale), Str.recapitalize); + const daysOfWeek = _.map(DateUtils.getDaysOfWeek(this.props.preferredLocale), (day) => day.toUpperCase()); const currentMonthView = this.state.currentDateView.getMonth(); const currentYearView = this.state.currentDateView.getFullYear(); const calendarDaysMatrix = generateMonthMatrix(currentYearView, currentMonthView); - const hasAvailableDatesNextMonth = moment(this.props.maxDate).endOf('month').endOf('day') >= moment(this.state.currentDateView).add(1, 'months'); - const hasAvailableDatesPrevMonth = moment(this.props.minDate).startOf('month').startOf('day') <= moment(this.state.currentDateView).subtract(1, 'months'); + const hasAvailableDatesNextMonth = startOfDay(endOfMonth(new Date(this.props.maxDate))) > addMonths(new Date(this.state.currentDateView), 1); + const hasAvailableDatesPrevMonth = startOfDay(new Date(this.props.minDate)) < endOfMonth(subMonths(new Date(this.state.currentDateView), 1)); return ( @@ -201,11 +201,11 @@ class CalendarPicker extends React.PureComponent { style={styles.flexRow} > {_.map(week, (day, index) => { - const currentDate = moment([currentYearView, currentMonthView, day]); - const isBeforeMinDate = currentDate < moment(this.props.minDate).startOf('day'); - const isAfterMaxDate = currentDate > moment(this.props.maxDate).startOf('day'); + const currentDate = new Date(currentYearView, currentMonthView, day); + const isBeforeMinDate = currentDate < startOfDay(new Date(this.props.minDate)); + const isAfterMaxDate = currentDate > startOfDay(new Date(this.props.maxDate)); const isDisabled = !day || isBeforeMinDate || isAfterMaxDate; - const isSelected = moment(this.props.value).isSame(moment([currentYearView, currentMonthView, day]), 'day'); + const isSelected = isSameDay(new Date(this.props.value), new Date(currentYearView, currentMonthView, day)); return ( { return timezone; } +/** + * @returns [January, Fabruary, March, April, May, June, July, August, ...] + */ +function getMonthNames(preferredLocale: string): string[] { + if (preferredLocale) { + setLocale(preferredLocale); + } + const fullYear = new Date().getFullYear(); + const monthsArray = eachMonthOfInterval({ + start: new Date(fullYear, 0, 1), // January 1st of the current year + end: new Date(fullYear, 11, 31), // December 31st of the current year + }); + + // eslint-disable-next-line rulesdir/prefer-underscore-method + return monthsArray.map((monthDate) => format(monthDate, CONST.DATE.MONTH_FORMAT)); +} + +/** + * @returns [Monday, Thuesday, Wednesday, ...] + */ +function getDaysOfWeek(preferredLocale: string): string[] { + if (preferredLocale) { + setLocale(preferredLocale); + } + const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week + const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week + const daysOfWeek = eachDayOfInterval({start: startOfCurrentWeek, end: endOfCurrentWeek}); + + // eslint-disable-next-line rulesdir/prefer-underscore-method + return daysOfWeek.map((date) => format(date, 'eeee')); +} + // Used to throttle updates to the timezone when necessary let lastUpdatedTimezoneTime = new Date(); @@ -357,6 +391,8 @@ const DateUtils = { isToday, isTomorrow, isYesterday, + getMonthNames, + getDaysOfWeek, }; export default DateUtils; diff --git a/tests/unit/CalendarPickerTest.js b/tests/unit/CalendarPickerTest.js index 512a86a25e19..235dff45f631 100644 --- a/tests/unit/CalendarPickerTest.js +++ b/tests/unit/CalendarPickerTest.js @@ -1,17 +1,10 @@ import {render, fireEvent, within} from '@testing-library/react-native'; -import {format, eachMonthOfInterval, subYears, addYears} from 'date-fns'; +import {subYears, addYears} from 'date-fns'; import DateUtils from '../../src/libs/DateUtils'; import CalendarPicker from '../../src/components/NewDatePicker/CalendarPicker'; import CONST from '../../src/CONST'; -DateUtils.setLocale(CONST.LOCALES.EN); -const fullYear = new Date().getFullYear(); -const monthsArray = eachMonthOfInterval({ - start: new Date(fullYear, 0, 1), // January 1st of the current year - end: new Date(fullYear, 11, 31), // December 31st of the current year -}); -// eslint-disable-next-line rulesdir/prefer-underscore-method -const monthNames = monthsArray.map((monthDate) => format(monthDate, CONST.DATE.MONTH_FORMAT)); +const monthNames = DateUtils.getMonthNames(CONST.LOCALES.EN); jest.mock('@react-navigation/native', () => ({ useNavigation: () => ({navigate: jest.fn()}), From 8d4678fa56bb60d5c5cf9e4371ffe56f1e2d924b Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 19 Oct 2023 16:37:39 +0200 Subject: [PATCH 032/312] Drop func from possible types --- src/components/FullscreenLoadingIndicator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx index 3f5f62f533d7..92e59d0e2d1c 100644 --- a/src/components/FullscreenLoadingIndicator.tsx +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -4,7 +4,7 @@ import styles from '../styles/styles'; import themeColors from '../styles/themes/default'; type FullScreenLoadingIndicatorProps = { - style: Record | Array> | (() => void); + style: Record | Array>; }; function FullScreenLoadingIndicator({style = []}: FullScreenLoadingIndicatorProps) { From 7ae4259e4167e62ee71ea30ea2a70dcf633cddb7 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 19 Oct 2023 17:35:05 +0200 Subject: [PATCH 033/312] Adjust the code after review --- src/pages/workspace/withPolicy.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index f168061f5fea..23473ec726dc 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -62,13 +62,6 @@ const policyPropTypes = { policyMembers: PropTypes.objectOf(policyMemberPropType), }; -const policyDefaultProps: WithPolicyOnyxProps = { - policy: {} as OnyxTypes.Policy, - policyMembers: {}, - policyDraft: {} as OnyxTypes.Policy, - policyMembersDraft: {}, -}; - type WithPolicyOnyxProps = { policy: OnyxEntry; policyMembers: OnyxEntry; @@ -79,6 +72,14 @@ type WithPolicyOnyxProps = { type WithPolicyProps = WithPolicyOnyxProps & { route: PolicyRoute; }; + +const policyDefaultProps: WithPolicyOnyxProps = { + policy: {} as OnyxTypes.Policy, + policyMembers: {}, + policyDraft: {} as OnyxTypes.Policy, + policyMembersDraft: {}, +}; + /* * HOC for connecting a policy in Onyx corresponding to the policyID in route params */ @@ -90,7 +91,7 @@ export default function withPolicy( const currentRoute = routes?.[routes.length - 1]; const policyID = getPolicyIDFromRoute(currentRoute as PolicyRoute); - if (typeof policyID === 'string' && policyID.length > 0) { + if (policyID.length > 0) { Policy.updateLastAccessedWorkspace(policyID); } From 7ffe27e996defa5cfce388dbcec6f5c19935bf3a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 19 Oct 2023 18:45:19 +0200 Subject: [PATCH 034/312] Fix forwardRef error --- src/pages/workspace/withPolicy.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index 23473ec726dc..2e55230a0a33 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -104,7 +104,6 @@ export default function withPolicy( ); } - WithPolicy.defaultProps = policyDefaultProps; WithPolicy.displayName = `withPolicy(${getComponentDisplayName(WrappedComponent)})`; return withOnyx, WithPolicyOnyxProps>({ From ba7bd1714b873326c7bda6a75e29c33473100e40 Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Thu, 19 Oct 2023 15:45:01 -0400 Subject: [PATCH 035/312] Allow requesting money when expensify owns workspace --- src/libs/ReportUtils.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 1b03c3a1bdb2..3e0b8962bf37 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3515,15 +3515,18 @@ function getMoneyRequestOptions(report, reportParticipants) { const participants = _.filter(reportParticipants, (accountID) => currentUserPersonalDetails.accountID !== accountID); - // Verify if there is any of the expensify accounts amongst the participants in which case user cannot take IOU actions on such report - const hasExcludedIOUAccountIDs = lodashIntersection(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0; - const hasSingleParticipantInReport = participants.length === 1; - const hasMultipleParticipants = participants.length > 1; - - if (hasExcludedIOUAccountIDs) { + // We don't allow IOU actions if an Expensify account is a participant of the report, unless the policy that the report is on is owned by an Expensify account + const doParticipantsIncludeExpensifyAccounts = lodashIntersection(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0; + const policyID = lodashGet(report, 'policyID', ''); + const policyOwnerAccountID = lodashGet(allPolicies, `${ONYXKEYS.COLLECTION.POLICY}${policyID}.ownerAccountID`, 0); + const doExpensifyAccountsOwnPolicy = CONST.EXPENSIFY_ACCOUNT_IDS.includes(policyOwnerAccountID); + if (doParticipantsIncludeExpensifyAccounts && !doExpensifyAccountsOwnPolicy) { return []; } + const hasSingleParticipantInReport = participants.length === 1; + const hasMultipleParticipants = participants.length > 1; + // User created policy rooms and default rooms like #admins or #announce will always have the Split Bill option // unless there are no participants at all (e.g. #admins room for a policy with only 1 admin) // DM chats will have the Split Bill option only when there are at least 3 people in the chat. From ce653159b75e12465402d199712bd2e02e9f4a37 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 20 Oct 2023 14:30:33 +0700 Subject: [PATCH 036/312] fix error when open sign in modal --- .../HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js | 5 +++++ src/libs/Navigation/NavigationRoot.js | 8 -------- src/libs/actions/Report.js | 8 +++++++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js index 92a313cf1e0a..5b15d7014d1f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js @@ -15,6 +15,7 @@ import * as Url from '../../../libs/Url'; import ROUTES from '../../../ROUTES'; import tryResolveUrlFromApiRoot from '../../../libs/tryResolveUrlFromApiRoot'; import useEnvironment from '../../../hooks/useEnvironment'; +import * as Session from '../../../libs/actions/Session'; function AnchorRenderer(props) { const htmlAttribs = props.tnode.attributes; @@ -52,6 +53,10 @@ function AnchorRenderer(props) { // If we are handling a New Expensify link then we will assume this should be opened by the app internally. This ensures that the links are opened internally via react-navigation // instead of in a new tab or with a page refresh (which is the default behavior of an anchor tag) if (internalNewExpensifyPath && hasSameOrigin) { + if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(internalNewExpensifyPath)) { + Session.signOutAndRedirectToSignIn(); + return; + } Navigation.navigate(internalNewExpensifyPath); return; } diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index a22b6714a306..c7a3b14e4fb0 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -12,9 +12,6 @@ import StatusBar from '../StatusBar'; import useCurrentReportID from '../../hooks/useCurrentReportID'; import useWindowDimensions from '../../hooks/useWindowDimensions'; import {SidebarNavigationContext} from '../../pages/home/sidebar/SidebarNavigationContext'; -import * as Session from '../actions/Session'; -import getCurrentUrl from './currentUrl'; -import ROUTES from '../../ROUTES'; // https://reactnavigation.org/docs/themes const navigationTheme = { @@ -136,11 +133,6 @@ function NavigationRoot(props) { // Update the global navigation to show the correct selected menu items. globalNavigation.updateFromNavigationState(state); - - const route = Navigation.getActiveRoute(); - if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route) && !getCurrentUrl().includes(ROUTES.SIGN_IN_MODAL)) { - Session.signOutAndRedirectToSignIn(); - } }; return ( diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index af1b4a0ac1dd..d3e2f9c749d2 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1909,7 +1909,13 @@ function openReportFromDeepLink(url, isAuthenticated) { InteractionManager.runAfterInteractions(() => { Session.waitForUserSignIn().then(() => { if (route === ROUTES.CONCIERGE) { - navigateToConciergeChat(true); + navigateToConciergeChat(); + return; + } + if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route)) { + Navigation.isNavigationReady().then(() => { + Session.signOutAndRedirectToSignIn(); + }); return; } Navigation.navigate(route, CONST.NAVIGATION.TYPE.PUSH); From ced932a1d2c8af33881b72772bec64293b6a014a Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 20 Oct 2023 14:36:09 +0700 Subject: [PATCH 037/312] fix revert not related changes --- src/libs/actions/Report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index d3e2f9c749d2..51dcdc49847d 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1909,7 +1909,7 @@ function openReportFromDeepLink(url, isAuthenticated) { InteractionManager.runAfterInteractions(() => { Session.waitForUserSignIn().then(() => { if (route === ROUTES.CONCIERGE) { - navigateToConciergeChat(); + navigateToConciergeChat(true); return; } if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route)) { From dfddd3300c375d738e491fe71db1f2cb4d6afadb Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 20 Oct 2023 14:40:30 +0700 Subject: [PATCH 038/312] fix remove not related change --- src/libs/ReportUtils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 88261a0061ea..011907c2c88b 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4065,6 +4065,5 @@ export { getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, isReportDraft, - parseReportRouteParams, shouldUseFullTitleToDisplay, }; From 7ca29f856c41305baebae957112b9e7ce7089d02 Mon Sep 17 00:00:00 2001 From: sarious Date: Fri, 20 Oct 2023 13:07:45 +0400 Subject: [PATCH 039/312] Fix 17866: After pressing the back arrow to "Send/Request money" screen, the keyboard flashes --- src/components/ScreenWrapper/index.js | 6 ++- src/hooks/useInitialWindowDimensions/index.js | 50 +++++++++++++++++++ .../index.native.js | 50 +++++++++++++++++++ src/pages/iou/MoneyRequestSelectorPage.js | 2 + 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useInitialWindowDimensions/index.js create mode 100644 src/hooks/useInitialWindowDimensions/index.native.js diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index e2af40589a8a..ecb83c28759e 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -16,12 +16,14 @@ import toggleTestToolsModal from '../../libs/actions/TestTool'; import CustomDevMenu from '../CustomDevMenu'; import * as Browser from '../../libs/Browser'; import useWindowDimensions from '../../hooks/useWindowDimensions'; +import useInitialDimensions from '../../hooks/useInitialWindowDimensions'; import useKeyboardState from '../../hooks/useKeyboardState'; import useEnvironment from '../../hooks/useEnvironment'; import useNetwork from '../../hooks/useNetwork'; function ScreenWrapper({ shouldEnableMaxHeight, + shouldEnableMinHeight, includePaddingTop, keyboardAvoidingViewBehavior, includeSafeAreaPaddingBottom, @@ -37,12 +39,14 @@ function ScreenWrapper({ testID, }) { const {windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const {initialHeight} = useInitialDimensions(); const keyboardState = useKeyboardState(); const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); const navigation = useNavigation(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; + const minHeight = shouldEnableMinHeight ? initialHeight : undefined; const isKeyboardShown = lodashGet(keyboardState, 'isKeyboardShown', false); const panResponder = useRef( @@ -125,7 +129,7 @@ function ScreenWrapper({ {...keyboardDissmissPanResponder.panHandlers} > diff --git a/src/hooks/useInitialWindowDimensions/index.js b/src/hooks/useInitialWindowDimensions/index.js new file mode 100644 index 000000000000..ae498f45377f --- /dev/null +++ b/src/hooks/useInitialWindowDimensions/index.js @@ -0,0 +1,50 @@ +// eslint-disable-next-line no-restricted-imports +import {useState, useEffect} from 'react'; +import {Dimensions} from 'react-native'; + +export default function () { + const [dimensions, setDimensions] = useState(() => { + const window = Dimensions.get('window'); + const screen = Dimensions.get('screen'); + + return { + screenHeight: screen.height, + screenWidth: screen.width, + initialHeight: window.height, + initialWidth: window.width, + }; + }); + + useEffect(() => { + const onDimensionChange = (newDimensions) => { + const {window, screen} = newDimensions; + + setDimensions((oldState) => { + if (screen.width !== oldState.screenWidth || screen.height !== oldState.screenHeight || window.height > oldState.initialHeight) { + return { + initialHeight: window.height, + initialWidth: window.width, + screenHeight: screen.height, + screenWidth: screen.width, + }; + } + + return oldState; + }); + }; + + const dimensionsEventListener = Dimensions.addEventListener('change', onDimensionChange); + + return () => { + if (!dimensionsEventListener) { + return; + } + dimensionsEventListener.remove(); + }; + }, []); + + return { + initialWidth: dimensions.initialWidth, + initialHeight: dimensions.initialHeight, + }; +} diff --git a/src/hooks/useInitialWindowDimensions/index.native.js b/src/hooks/useInitialWindowDimensions/index.native.js new file mode 100644 index 000000000000..ae498f45377f --- /dev/null +++ b/src/hooks/useInitialWindowDimensions/index.native.js @@ -0,0 +1,50 @@ +// eslint-disable-next-line no-restricted-imports +import {useState, useEffect} from 'react'; +import {Dimensions} from 'react-native'; + +export default function () { + const [dimensions, setDimensions] = useState(() => { + const window = Dimensions.get('window'); + const screen = Dimensions.get('screen'); + + return { + screenHeight: screen.height, + screenWidth: screen.width, + initialHeight: window.height, + initialWidth: window.width, + }; + }); + + useEffect(() => { + const onDimensionChange = (newDimensions) => { + const {window, screen} = newDimensions; + + setDimensions((oldState) => { + if (screen.width !== oldState.screenWidth || screen.height !== oldState.screenHeight || window.height > oldState.initialHeight) { + return { + initialHeight: window.height, + initialWidth: window.width, + screenHeight: screen.height, + screenWidth: screen.width, + }; + } + + return oldState; + }); + }; + + const dimensionsEventListener = Dimensions.addEventListener('change', onDimensionChange); + + return () => { + if (!dimensionsEventListener) { + return; + } + dimensionsEventListener.remove(); + }; + }, []); + + return { + initialWidth: dimensions.initialWidth, + initialHeight: dimensions.initialHeight, + }; +} diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index 0786faa3841b..bcf75bd64056 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -22,6 +22,7 @@ import NewRequestAmountPage from './steps/NewRequestAmountPage'; import reportPropTypes from '../reportPropTypes'; import * as ReportUtils from '../../libs/ReportUtils'; import usePrevious from '../../hooks/usePrevious'; +import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; const propTypes = { /** React Navigation route */ @@ -85,6 +86,7 @@ function MoneyRequestSelectorPage(props) { From 4fca0496676475b9c3725bd455364e850442a830 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Fri, 20 Oct 2023 14:13:05 +0200 Subject: [PATCH 040/312] clean up show GBR --- src/components/LHNOptionsList/OptionRowLHN.js | 2 +- src/libs/ReportUtils.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 064c1c146163..ae9a6da40c5d 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -116,7 +116,7 @@ function OptionRowLHN(props) { const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; const defaultSubscriptSize = optionItem.isExpenseRequest ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; - const shouldShowGreenDotIndicator = !hasBrickError && (optionItem.isUnreadWithMention || optionItem.isWaitingForTaskCompleteFromAssignee || ReportUtils.shouldShowGBR(optionItem)); + const shouldShowGreenDotIndicator = !hasBrickError && ReportUtils.shouldShowGBR(optionItem); /** * Show the ReportActionContextMenu modal popover. diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index c8339810cb1f..a6578a320f39 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1227,6 +1227,14 @@ function shouldShowGBR(report) { return false; } + if (report.isUnreadWithMention) { + return true; + } + + if (report.isWaitingForTaskCompleteFromAssignee) { + return true; + } + // Money request waiting for current user to add their credit bank account if (report.hasOutstandingIOU && report.ownerAccountID === currentUserAccountID && report.isWaitingOnBankAccount) { return true; From 7058006baf3696aa5a0b19e6e0c7dd1c6b78786b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 20 Oct 2023 14:26:38 +0200 Subject: [PATCH 041/312] Improve PolicyRoute type --- src/pages/workspace/withPolicy.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index 2e55230a0a33..7bb9db9ad8e0 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -1,7 +1,7 @@ import React, {ComponentType, ForwardedRef, RefAttributes, forwardRef} from 'react'; import PropTypes from 'prop-types'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; -import {useNavigationState} from '@react-navigation/native'; +import {RouteProp, useNavigationState} from '@react-navigation/native'; import CONST from '../../CONST'; import getComponentDisplayName from '../../libs/getComponentDisplayName'; import * as Policy from '../../libs/actions/Policy'; @@ -9,11 +9,7 @@ import ONYXKEYS from '../../ONYXKEYS'; import policyMemberPropType from '../policyMemberPropType'; import * as OnyxTypes from '../../types/onyx'; -type PolicyRoute = { - params?: { - policyID: string; - }; -}; +type PolicyRoute = RouteProp<{params: {policyID: string}}>; function getPolicyIDFromRoute(route: PolicyRoute): string { return route?.params?.policyID ?? ''; From 4d7989493d72a2bc9ef756bb6eb00461415d31a2 Mon Sep 17 00:00:00 2001 From: AmjedNazzal Date: Fri, 20 Oct 2023 17:39:17 +0300 Subject: [PATCH 042/312] Issue28820 --- src/libs/Clipboard/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Clipboard/index.js b/src/libs/Clipboard/index.js index 6fbaa8eccd31..2f7b647d9eeb 100644 --- a/src/libs/Clipboard/index.js +++ b/src/libs/Clipboard/index.js @@ -62,7 +62,7 @@ function setHTMLSync(html, text) { if (isComposer) { firstAnchorChild.setSelectionRange(originalSelection.start, originalSelection.end, originalSelection.direction); - } else { + } else if (originalSelection.anchorNode && originalSelection.focusNode) { selection.setBaseAndExtent(originalSelection.anchorNode, originalSelection.anchorOffset, originalSelection.focusNode, originalSelection.focusOffset); } From bd92d7e6503bdf413bedfc8e216d74a814e92b68 Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Fri, 20 Oct 2023 12:12:55 -0400 Subject: [PATCH 043/312] Allow assigning task in workspace chat where expensify is owner --- .../report/ReportActionCompose/AttachmentPickerWithMenuItems.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index 6522bedc825a..d3be3f47bf2f 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -152,7 +152,7 @@ function AttachmentPickerWithMenuItems({ */ const taskOption = useMemo(() => { // We only prevent the task option from showing if it's a DM and the other user is an Expensify default email - if (!Permissions.canUseTasks(betas) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { + if (!Permissions.canUseTasks(betas) || (!ReportUtils.isPolicyExpenseChat(report) && ReportUtils.isExpensifyOnlyParticipantInReport(report))) { return []; } From 4117624cbce608726c21a203278098a3fb370f88 Mon Sep 17 00:00:00 2001 From: Sofie de Vreese <40040992+SofiedeVreese@users.noreply.github.com> Date: Sun, 22 Oct 2023 23:11:08 +0000 Subject: [PATCH 044/312] Update Auto-Reconciliation.md Updating image placeholders with actual image URLs --- .../expensify-card/Auto-Reconciliation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md index 059877a18075..824f01f688b3 100644 --- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md +++ b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md @@ -35,6 +35,8 @@ To set up your auto-reconciliation account with the Expensify Card, follow these 5. Head to the "Settings" tab. 6. Select the account in your accounting solution that you want to use for reconciliation. Make sure this account matches the settlement business bank account. +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/Auto-Reconciliaton_Image1.png){:width="100%"} + That's it! You've successfully set up your auto-reconciliation account. ## How does Auto-Reconciliation work @@ -44,13 +46,11 @@ Once Auto-Reconciliation is enabled, there are a few things that happen. Let’s **What happens**: When an Expensify Card is used to make purchases, the amount spent is automatically deducted from your company’s 'Settlement Account' (your business checking account). This deduction happens on a daily or monthly basis, depending on your chosen settlement frequency. Don't worry; this settlement account is pre-defined when you apply for the Expensify Card, and you can't accidentally change it. **Accounting treatment**: After your card balance is settled each day, we update your accounting system with a journal entry. This entry credits your bank account (referred to as the GL account) and debits the Expensify Card Clearing Account. To ensure accuracy, please make sure that the 'bank account' in your Expensify Card settings matches your real-life settlement account. You can easily verify this by navigating to **Settings > Account > Payments**, where you'll see 'Settlement Account' next to your business bank account. To keep track of settlement figures by date, use the Company Card Reconciliation Dashboard's Settlements tab: -![Insert alt text for accessibility here](https://help.expensify.com/assets/images/image-name.png){:width="100%"} +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/Auto-Reconciliation_Image2.png){:width="100%"} ### Submitting, Approving, and Exporting Expenses **What happens**: Users submit their expenses on a report, which might occur after some time has passed since the initial purchase. Once the report is approved, it's then exported to your accounting software. -**Accounting treatment**: When the report is exported, we create a journal entry in your accounting system. This entry credits the Clearing Account and debits the Liability Account for the purchase amount. The Liability Account functions as a bank account in your ledger, specifically for Expensify Card expenses: - -![Insert alt text for accessibility here](https://help.expensify.com/assets/images/image-name.png){:width="100%"} +**Accounting treatment**: When the report is exported, we create a journal entry in your accounting system. This entry credits the Clearing Account and debits the Liability Account for the purchase amount. The Liability Account functions as a bank account in your ledger, specifically for Expensify Card expenses. # Deep Dive ## QuickBooks Online From 1783d6ccbded3e5b423f878a93b6ff7014e3e5f6 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Mon, 23 Oct 2023 13:24:00 +0100 Subject: [PATCH 045/312] update text styles to better handle ellipsize on Android native --- src/pages/ProfilePage.js | 14 ++++++++------ src/pages/settings/InitialSettingsPage.js | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 8e0ed04ab94a..7f5801e8ddfe 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -187,12 +187,14 @@ function ProfilePage(props) { )} {Boolean(displayName) && ( - - {displayName} - + + + {displayName} + + )} {hasStatus && ( diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index d81c9d057174..32bb64f95420 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -356,14 +356,14 @@ function InitialSettingsPage(props) { {props.currentUserPersonalDetails.displayName ? props.currentUserPersonalDetails.displayName : props.formatPhoneNumber(props.session.email)} From 512c4d5b97d6689074000c42a80794c0b25611a8 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 23 Oct 2023 19:49:21 +0200 Subject: [PATCH 046/312] Restore displayName --- src/components/FullscreenLoadingIndicator.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx index 92e59d0e2d1c..4cd8002cf2c7 100644 --- a/src/components/FullscreenLoadingIndicator.tsx +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -19,4 +19,6 @@ function FullScreenLoadingIndicator({style = []}: FullScreenLoadingIndicatorProp ); } +FullScreenLoadingIndicator.displayName = 'FullScreenLoadingIndicator'; + export default FullScreenLoadingIndicator; From 7044bbb83da3685e1eb19a4fc4b2ba4c9f2841de Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 23 Oct 2023 19:54:07 +0200 Subject: [PATCH 047/312] Use ViewStyle --- src/components/FullscreenLoadingIndicator.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx index 4cd8002cf2c7..43a7c4180284 100644 --- a/src/components/FullscreenLoadingIndicator.tsx +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import {ActivityIndicator, StyleSheet, View} from 'react-native'; +import {ActivityIndicator, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'; import styles from '../styles/styles'; import themeColors from '../styles/themes/default'; type FullScreenLoadingIndicatorProps = { - style: Record | Array>; + style: StyleProp; }; function FullScreenLoadingIndicator({style = []}: FullScreenLoadingIndicatorProps) { From 1088ada601544648d36bbdbbce33b409d8d1a6d2 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 23 Oct 2023 21:45:59 +0200 Subject: [PATCH 048/312] Drop not needed casting --- src/components/FullscreenLoadingIndicator.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx index 43a7c4180284..dc2a08e3ac5e 100644 --- a/src/components/FullscreenLoadingIndicator.tsx +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -8,9 +8,8 @@ type FullScreenLoadingIndicatorProps = { }; function FullScreenLoadingIndicator({style = []}: FullScreenLoadingIndicatorProps) { - const additionalStyles = Array.isArray(style) ? style : [style]; return ( - + Date: Tue, 24 Oct 2023 02:39:35 +0200 Subject: [PATCH 049/312] tests --- src/pages/home/sidebar/SidebarLinksData.js | 1 + src/types/onyx/Report.ts | 3 +++ tests/unit/SidebarOrderTest.js | 28 +++++++++++----------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 394f6c5ddc5a..2875b61256af 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -143,6 +143,7 @@ const chatReportSelector = (report) => total: report.total, nonReimbursableTotal: report.nonReimbursableTotal, hasOutstandingIOU: report.hasOutstandingIOU, + hasOutstandingChildRequest: report.hasOutstandingChildRequest, isWaitingOnBankAccount: report.isWaitingOnBankAccount, statusNum: report.statusNum, stateNum: report.stateNum, diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 8587cf9b7cd5..c8b3b0752959 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -9,6 +9,9 @@ type Report = { /** Whether there is an outstanding amount in IOU */ hasOutstandingIOU?: boolean; + /** Whether child has an outstanding request */ + hasOutstandingChildRequest?: boolean; + /** List of icons for report participants */ icons?: OnyxCommon.Icon[]; diff --git a/tests/unit/SidebarOrderTest.js b/tests/unit/SidebarOrderTest.js index 4a693d679b86..64587466c942 100644 --- a/tests/unit/SidebarOrderTest.js +++ b/tests/unit/SidebarOrderTest.js @@ -385,7 +385,7 @@ describe('Sidebar', () => { }; const report3 = { ...LHNTestUtils.getFakeReport([5, 6], 1), - hasOutstandingIOU: false, + hasOutstandingChildRequest: false, // This has to be added after the IOU report is generated iouReportID: null, @@ -395,7 +395,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 2, - hasOutstandingIOU: true, + hasOutstandingChildRequest: true, total: 10000, currency: 'USD', chatReportID: report3.reportID, @@ -404,7 +404,7 @@ describe('Sidebar', () => { const currentReportId = report2.reportID; const currentlyLoggedInUserAccountID = 9; LHNTestUtils.getDefaultRenderedSidebarLinks(currentReportId); - + console.log('iouReport :>> ', iouReport); return ( waitForBatchedUpdates() // When Onyx is updated with the data and the sidebar re-renders @@ -431,7 +431,7 @@ describe('Sidebar', () => { expect(screen.queryAllByTestId('Pin Icon')).toHaveLength(1); expect(screen.queryAllByTestId('Pencil Icon')).toHaveLength(1); expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('One, Two'); - expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Email Two owes $100.00'); + expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Email Two paid $100.00'); expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Three, Four'); }) ); @@ -733,7 +733,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 2, - hasOutstandingIOU: true, + hasOutstandingChildRequest: true, total: 10000, currency: 'USD', chatReportID: report3.reportID, @@ -743,7 +743,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 3, - hasOutstandingIOU: true, + hasOutstandingChildRequest: true, total: 10000, currency: 'USD', chatReportID: report3.reportID, @@ -753,7 +753,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 4, - hasOutstandingIOU: true, + hasOutstandingChildRequest: true, total: 100000, currency: 'USD', chatReportID: report3.reportID, @@ -763,7 +763,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 5, - hasOutstandingIOU: true, + hasOutstandingChildRequest: true, total: 10000, currency: 'USD', chatReportID: report3.reportID, @@ -773,7 +773,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 6, - hasOutstandingIOU: true, + hasOutstandingChildRequest: true, total: 10000, currency: 'USD', chatReportID: report3.reportID, @@ -814,11 +814,11 @@ describe('Sidebar', () => { const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNames = screen.queryAllByLabelText(hintText); expect(displayNames).toHaveLength(5); - expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('Email Four owes $1,000.00'); - expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Email Five owes $100.00'); - expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Email Six owes $100.00'); - expect(lodashGet(displayNames, [3, 'props', 'children'])).toBe('Email Three owes $100.00'); - expect(lodashGet(displayNames, [4, 'props', 'children'])).toBe('Email Two owes $100.00'); + expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('Email Four paid $1,000.00'); + expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Email Five paid $100.00'); + expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Email Six paid $100.00'); + expect(lodashGet(displayNames, [3, 'props', 'children'])).toBe('Email Three paid $100.00'); + expect(lodashGet(displayNames, [4, 'props', 'children'])).toBe('Email Two paid $100.00'); }) ); }); From f4cb0093f0981445a1bebe2709f00b84cd831ce7 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 24 Oct 2023 02:47:08 +0200 Subject: [PATCH 050/312] fix lint --- tests/unit/SidebarOrderTest.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/SidebarOrderTest.js b/tests/unit/SidebarOrderTest.js index 64587466c942..a2b82dc1cb58 100644 --- a/tests/unit/SidebarOrderTest.js +++ b/tests/unit/SidebarOrderTest.js @@ -404,7 +404,6 @@ describe('Sidebar', () => { const currentReportId = report2.reportID; const currentlyLoggedInUserAccountID = 9; LHNTestUtils.getDefaultRenderedSidebarLinks(currentReportId); - console.log('iouReport :>> ', iouReport); return ( waitForBatchedUpdates() // When Onyx is updated with the data and the sidebar re-renders From 8413ffde609d1f04b52ddd63d681800be922bb2a Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 24 Oct 2023 10:55:59 +0700 Subject: [PATCH 051/312] fix: regression 29888 --- src/components/ReportActionItem/MoneyRequestPreview.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 43500c731728..06e374306ae8 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -263,7 +263,9 @@ function MoneyRequestPreview(props) { ) : ( - {getPreviewHeaderText() + (isSettled ? ` • ${getSettledMessage()}` : '')} + + {getPreviewHeaderText() + (isSettled ? ` • ${getSettledMessage()}` : '')} + {hasFieldErrors && ( Date: Tue, 24 Oct 2023 13:21:50 +0200 Subject: [PATCH 052/312] draft option row left indent --- src/components/OptionRow.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index e3ea3acfc2ee..114b728f62d6 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -109,9 +109,20 @@ function OptionRow(props) { setIsDisabled(props.isDisabled); }, [props.isDisabled]); + const text = lodashGet(props.option, 'text', ''); + const fullTitle = props.isMultilineSupported ? text.trimStart() : text; + const indentsLength = text.length - fullTitle.length; + const paddingLeft = Math.floor(indentsLength / CONST.INDENTS.length) * styles.ml3.marginLeft; const textStyle = props.optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = props.boldStyle || props.option.boldStyle ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; - const displayNameStyle = StyleUtils.combineStyles(styles.optionDisplayName, textUnreadStyle, props.style, styles.pre, isDisabled ? styles.optionRowDisabled : {}); + const displayNameStyle = StyleUtils.combineStyles( + styles.optionDisplayName, + textUnreadStyle, + props.style, + styles.pre, + isDisabled ? styles.optionRowDisabled : {}, + props.isMultilineSupported ? {paddingLeft} : {}, + ); const alternateTextStyle = StyleUtils.combineStyles( textStyle, styles.optionAlternateText, @@ -204,7 +215,7 @@ function OptionRow(props) { Date: Tue, 24 Oct 2023 13:24:18 +0200 Subject: [PATCH 053/312] tests fix --- tests/unit/ReportUtilsTest.js | 1 + tests/unit/SidebarOrderTest.js | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index 0c888b80b51b..81249214a4e8 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -302,6 +302,7 @@ describe('ReportUtils', () => { const report = { ...LHNTestUtils.getFakeReport(), ownerAccountID: 99, + hasOutstandingIOU: true, hasOutstandingChildRequest: true, isWaitingOnBankAccount: false, }; diff --git a/tests/unit/SidebarOrderTest.js b/tests/unit/SidebarOrderTest.js index a2b82dc1cb58..ea6fe49befc8 100644 --- a/tests/unit/SidebarOrderTest.js +++ b/tests/unit/SidebarOrderTest.js @@ -395,6 +395,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 2, + hasOutstandingIOU: true, hasOutstandingChildRequest: true, total: 10000, currency: 'USD', @@ -430,7 +431,7 @@ describe('Sidebar', () => { expect(screen.queryAllByTestId('Pin Icon')).toHaveLength(1); expect(screen.queryAllByTestId('Pencil Icon')).toHaveLength(1); expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('One, Two'); - expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Email Two paid $100.00'); + expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Email Two owes $100.00'); expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Three, Four'); }) ); @@ -732,6 +733,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 2, + hasOutstandingIOU: true, hasOutstandingChildRequest: true, total: 10000, currency: 'USD', @@ -742,6 +744,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 3, + hasOutstandingIOU: true, hasOutstandingChildRequest: true, total: 10000, currency: 'USD', @@ -752,6 +755,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 4, + hasOutstandingIOU: true, hasOutstandingChildRequest: true, total: 100000, currency: 'USD', @@ -762,6 +766,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 5, + hasOutstandingIOU: true, hasOutstandingChildRequest: true, total: 10000, currency: 'USD', @@ -772,6 +777,7 @@ describe('Sidebar', () => { type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, managerID: 6, + hasOutstandingIOU: true, hasOutstandingChildRequest: true, total: 10000, currency: 'USD', @@ -813,11 +819,11 @@ describe('Sidebar', () => { const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNames = screen.queryAllByLabelText(hintText); expect(displayNames).toHaveLength(5); - expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('Email Four paid $1,000.00'); - expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Email Five paid $100.00'); - expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Email Six paid $100.00'); - expect(lodashGet(displayNames, [3, 'props', 'children'])).toBe('Email Three paid $100.00'); - expect(lodashGet(displayNames, [4, 'props', 'children'])).toBe('Email Two paid $100.00'); + expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('Email Four owes $1,000.00'); + expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Email Five owes $100.00'); + expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Email Six owes $100.00'); + expect(lodashGet(displayNames, [3, 'props', 'children'])).toBe('Email Three owes $100.00'); + expect(lodashGet(displayNames, [4, 'props', 'children'])).toBe('Email Two owes $100.00'); }) ); }); From dd5d90cebaaf17a7fbb6c17176198df774059a19 Mon Sep 17 00:00:00 2001 From: Sonia Liapounova Date: Tue, 24 Oct 2023 04:32:10 -0700 Subject: [PATCH 054/312] Create Fringe-Benefits.md --- .../Fringe-Benefits.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md new file mode 100644 index 000000000000..d76b6e413840 --- /dev/null +++ b/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md @@ -0,0 +1,43 @@ +--- +title: Fringe Benefits +description: How to track your Fringe Benefits +--- +# Overview +If you’re looking to track and report expense data to calculate Fringe Benefits Tax (FBT), you can use Expensify’s special workflow that allows you to capture extra information and use a template to export to a spreadsheet. + +# How to set up Fringe Benefit Tax + +## Add Attendee Count Tags +First, you’ll need to add these exact two tags to your Workspace: +1) Number of Internal Attendees +2) Number of External Attendees + +These tags must be named exactly as written above, ensuring there are no extra spaces at the beginning or at the end. You’ll need to set the tags to be numbers 00 - 10 or whatever number you wish to go up to (up to the maximum number of attendees you would expect at any one time), one tag per number i.e. “01”, “02”, “03” etc. These tags can be added in addition to those that are pulled in from your accounting solution. Follow these [instructions](https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Tags#gsc.tab=0) to add tags. + +## Add Payroll Code +Go to **Settings > Workspaces > Group > _Workspace Name_ > Categories** and within the categories you wish to track FBT against, select **Edit Category** and add the code “TAG”: + +## Enable Workflow +Once you’ve added the two tags (Internal Attendees and External Attendees) and added the payroll code “TAG” to FBT categories, you can now contact Expensify at concierge@expensify.com and send through a request to enable the FBT workflow. Please send through the following request: +>“Can you please add the custom workflow/DEW named FRINGE_BENEFIT_TAX to my company workspace named ?” +Once the FBT workflow is enabled, it will require anything with the code “TAG” to include the two attendee count tags in order to be submitted. + + +# For Users +Once these steps are completed, users who create expenses coded with any category that has the payroll code “TAG” (e.g. Entertainment Expenses) but don’t add the internal and external attendee counts, will not be able to submit their expenses. +# For Admins +You are now able to create and run a report, which shows all expenses under these categories and also shows the number of internal and external attendees. Because we don’t presume to know all of the data points you wish to capture, you’ll need to create a Custom CSV export. +Here’s an example of the Excel formulas to use to report on attendees: +- `{expense:tag:ntag-1}` outputs the first tag the user chooses. +- `{expense:tag:ntag-3}` outputs the third tag the user chooses. + +Your expenses may have multiple levels of coding, i.e.: +- GL Code (Category) +- Department (Tag 1) +- Location (Tag 2) +- Number of Internal Attendees (Tag 3) +- Number of External Attendees (Tag 4) + +In the above case, you’ll want to use `{expense:tag:ntag-3}` and `{expense:tag:ntag-4}` as formulas to report on the number of internal and external attendees. + +Our article on [Custom Templates](https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates#gsc.tab=0) shows you how to create a custom CSV. From 8098acf53f512e8ffcec8fbeaf7865a689c3885b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 24 Oct 2023 15:20:50 +0200 Subject: [PATCH 055/312] pass sectionHeaderStyle to OptionsList --- src/components/OptionsList/BaseOptionsList.js | 3 ++- src/components/OptionsList/optionsListPropTypes.js | 5 +++++ src/components/OptionsSelector/BaseOptionsSelector.js | 1 + src/components/OptionsSelector/optionsSelectorPropTypes.js | 5 +++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index 91fd77dbea30..f633156b6ed7 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -51,6 +51,7 @@ function BaseOptionsList({ showTitleTooltip, optionHoveredStyle, contentContainerStyles, + sectionHeaderStyle, showScrollIndicator, listContainerStyles, shouldDisableRowInnerPadding, @@ -233,7 +234,7 @@ function BaseOptionsList({ // We do this so that we can reference the height in `getItemLayout` – // we need to know the heights of all list items up-front in order to synchronously compute the layout of any given list item. // So be aware that if you adjust the content of the section header (for example, change the font size), you may need to adjust this explicit height as well. - + {title} ); diff --git a/src/components/OptionsList/optionsListPropTypes.js b/src/components/OptionsList/optionsListPropTypes.js index caabf39a41bb..5dc309e2b20c 100644 --- a/src/components/OptionsList/optionsListPropTypes.js +++ b/src/components/OptionsList/optionsListPropTypes.js @@ -14,6 +14,10 @@ const propTypes = { /** Extra styles for the section list container */ contentContainerStyles: PropTypes.arrayOf(PropTypes.object), + /** Style for section headers */ + // eslint-disable-next-line react/forbid-prop-types + sectionHeaderStyle: PropTypes.object, + /** Sections for the section list */ sections: PropTypes.arrayOf( PropTypes.shape({ @@ -101,6 +105,7 @@ const propTypes = { const defaultProps = { optionHoveredStyle: undefined, contentContainerStyles: [], + sectionHeaderStyle: undefined, listContainerStyles: [styles.flex1], sections: [], focusedIndex: 0, diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 0125fc8e178e..7c09edf64b0d 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -427,6 +427,7 @@ class BaseOptionsSelector extends Component { } }} contentContainerStyles={[safeAreaPaddingBottomStyle, ...this.props.contentContainerStyles]} + sectionHeaderStyle={this.props.sectionHeaderStyle} listContainerStyles={this.props.listContainerStyles} listStyles={this.props.listStyles} isLoading={!this.props.shouldShowOptions} diff --git a/src/components/OptionsSelector/optionsSelectorPropTypes.js b/src/components/OptionsSelector/optionsSelectorPropTypes.js index bfef8ca3a925..b2e422f0a0d9 100644 --- a/src/components/OptionsSelector/optionsSelectorPropTypes.js +++ b/src/components/OptionsSelector/optionsSelectorPropTypes.js @@ -108,6 +108,10 @@ const propTypes = { /** Hover style for options in the OptionsList */ optionHoveredStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + /** Style for section headers */ + // eslint-disable-next-line react/forbid-prop-types + sectionHeaderStyle: PropTypes.object, + /** Whether to show options list */ shouldShowOptions: PropTypes.bool, @@ -159,6 +163,7 @@ const defaultProps = { shouldTextInputAppearBelowOptions: false, footerContent: undefined, optionHoveredStyle: styles.hoveredComponentBG, + sectionHeaderStyle: undefined, shouldShowOptions: true, disableArrowKeysActions: false, isDisabled: false, From d5f4e0e47cf15849ab69c2bdd644f3f2168bc8e3 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 24 Oct 2023 15:21:04 +0200 Subject: [PATCH 056/312] Apply sectionHeaderStyle for category and tag pickers --- src/components/CategoryPicker/index.js | 1 + src/components/TagPicker/index.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/CategoryPicker/index.js b/src/components/CategoryPicker/index.js index e7f68e7011fc..a2e97ce70d28 100644 --- a/src/components/CategoryPicker/index.js +++ b/src/components/CategoryPicker/index.js @@ -69,6 +69,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC return ( Date: Tue, 24 Oct 2023 17:14:36 +0200 Subject: [PATCH 057/312] add a section header for empty title --- src/components/OptionsList/BaseOptionsList.js | 4 ++++ src/libs/OptionsListUtils.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index f633156b6ed7..2a71fb189ee4 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -228,6 +228,10 @@ function BaseOptionsList({ * @return {Component} */ const renderSectionHeader = ({section: {title, shouldShow}}) => { + if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) { + return ; + } + if (title && shouldShow && !hideSectionHeaders) { return ( // Note: The `optionsListSectionHeader` style provides an explicit height to section headers. diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index e909f0d86453..5391e5f4c234 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -756,7 +756,7 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt categorySections.push({ // "Search" section title: '', - shouldShow: false, + shouldShow: true, indexOffset, data: getCategoryOptionTree(searchCategories, true), }); @@ -790,7 +790,7 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt categorySections.push({ // "Selected" section title: '', - shouldShow: false, + shouldShow: true, indexOffset, data: getCategoryOptionTree(selectedOptions, true), }); From 0ca4eb83f513ac0d419f504257028ed97e9f5725 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 24 Oct 2023 17:22:05 +0200 Subject: [PATCH 058/312] add a section header for empty title --- src/libs/OptionsListUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index c61d7368b88d..eca0519a8011 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -962,7 +962,7 @@ function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInput tagSections.push({ // "Search" section title: '', - shouldShow: false, + shouldShow: true, indexOffset, data: getTagsOptions(searchTags), }); @@ -1004,7 +1004,7 @@ function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInput tagSections.push({ // "Selected" section title: '', - shouldShow: false, + shouldShow: true, indexOffset, data: getTagsOptions(selectedTagOptions), }); From 127bff691099a5cebea9994c9582b01b96e35bbd Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 24 Oct 2023 17:24:57 +0200 Subject: [PATCH 059/312] fix tests --- tests/unit/OptionsListUtilsTest.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 7a9fbb558455..006f8a82febd 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -729,7 +729,7 @@ describe('OptionsListUtils', () => { const smallSearchResultList = [ { title: '', - shouldShow: false, + shouldShow: true, indexOffset: 0, data: [ { @@ -752,7 +752,7 @@ describe('OptionsListUtils', () => { const smallWrongSearchResultList = [ { title: '', - shouldShow: false, + shouldShow: true, indexOffset: 0, data: [], }, @@ -818,7 +818,7 @@ describe('OptionsListUtils', () => { const largeResultList = [ { title: '', - shouldShow: false, + shouldShow: true, indexOffset: 0, data: [ { @@ -932,7 +932,7 @@ describe('OptionsListUtils', () => { const largeSearchResultList = [ { title: '', - shouldShow: false, + shouldShow: true, indexOffset: 0, data: [ { @@ -962,7 +962,7 @@ describe('OptionsListUtils', () => { const largeWrongSearchResultList = [ { title: '', - shouldShow: false, + shouldShow: true, indexOffset: 0, data: [], }, @@ -1105,7 +1105,7 @@ describe('OptionsListUtils', () => { const smallSearchResultList = [ { title: '', - shouldShow: false, + shouldShow: true, indexOffset: 0, data: [ { @@ -1121,7 +1121,7 @@ describe('OptionsListUtils', () => { const smallWrongSearchResultList = [ { title: '', - shouldShow: false, + shouldShow: true, indexOffset: 0, data: [], }, @@ -1175,7 +1175,7 @@ describe('OptionsListUtils', () => { const largeResultList = [ { title: '', - shouldShow: false, + shouldShow: true, indexOffset: 0, data: [ { @@ -1261,7 +1261,7 @@ describe('OptionsListUtils', () => { const largeSearchResultList = [ { title: '', - shouldShow: false, + shouldShow: true, indexOffset: 0, data: [ { @@ -1284,7 +1284,7 @@ describe('OptionsListUtils', () => { const largeWrongSearchResultList = [ { title: '', - shouldShow: false, + shouldShow: true, indexOffset: 0, data: [], }, From 87f60149c8b3540a06d9b51f67e6f77110b42b2f Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Tue, 24 Oct 2023 18:25:05 +0200 Subject: [PATCH 060/312] limit report routes in customStackNavigator --- .../createCustomStackNavigator/index.js | 36 ++++++++++- .../index.native.js | 60 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js index 58be3d2af3da..bfc56cf171c0 100644 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js @@ -1,9 +1,10 @@ -import React, {useRef} from 'react'; +import React, {useRef, useMemo} from 'react'; import PropTypes from 'prop-types'; import {useNavigationBuilder, createNavigatorFactory} from '@react-navigation/native'; import {StackView} from '@react-navigation/stack'; import CustomRouter from './CustomRouter'; import useWindowDimensions from '../../../../hooks/useWindowDimensions'; +import NAVIGATORS from '../../../../NAVIGATORS'; const propTypes = { /* Determines if the navigator should render the StackView (narrow) or ThreePaneView (wide) */ @@ -25,6 +26,24 @@ const defaultProps = { screenOptions: undefined, }; +function splitRoutes(routes) { + const reportRoutes = []; + const rhpRoutes = []; + const otherRoutes = []; + + routes.forEach((route) => { + if (route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR) { + reportRoutes.push(route); + } else if (route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + rhpRoutes.push(route); + } else { + otherRoutes.push(route); + } + }); + + return {reportRoutes, rhpRoutes, otherRoutes}; +} + function ResponsiveStackNavigator(props) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -40,12 +59,25 @@ function ResponsiveStackNavigator(props) { getIsSmallScreenWidth: () => isSmallScreenWidthRef.current, }); + const stateToRender = useMemo(() => { + const {reportRoutes, rhpRoutes, otherRoutes} = splitRoutes(state.routes); + + // Remove all report routes except the last 3. This will improve performance. + const limitedReportRoutes = reportRoutes.slice(-3); + + return { + ...state, + index: otherRoutes.length + limitedReportRoutes.length + rhpRoutes.length - 1, + routes: [...otherRoutes, ...limitedReportRoutes, ...rhpRoutes], + }; + }, [state]); + return ( diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js new file mode 100644 index 000000000000..58be3d2af3da --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js @@ -0,0 +1,60 @@ +import React, {useRef} from 'react'; +import PropTypes from 'prop-types'; +import {useNavigationBuilder, createNavigatorFactory} from '@react-navigation/native'; +import {StackView} from '@react-navigation/stack'; +import CustomRouter from './CustomRouter'; +import useWindowDimensions from '../../../../hooks/useWindowDimensions'; + +const propTypes = { + /* Determines if the navigator should render the StackView (narrow) or ThreePaneView (wide) */ + isSmallScreenWidth: PropTypes.bool.isRequired, + + /* Children for the useNavigationBuilder hook */ + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, + + /* initialRouteName for this navigator */ + initialRouteName: PropTypes.oneOf([PropTypes.string, PropTypes.undefined]), + + /* Screen options defined for this navigator */ + // eslint-disable-next-line react/forbid-prop-types + screenOptions: PropTypes.object, +}; + +const defaultProps = { + initialRouteName: undefined, + screenOptions: undefined, +}; + +function ResponsiveStackNavigator(props) { + const {isSmallScreenWidth} = useWindowDimensions(); + + const isSmallScreenWidthRef = useRef(isSmallScreenWidth); + + isSmallScreenWidthRef.current = isSmallScreenWidth; + + const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder(CustomRouter, { + children: props.children, + screenOptions: props.screenOptions, + initialRouteName: props.initialRouteName, + // Options for useNavigationBuilder won't update on prop change, so we need to pass a getter for the router to have the current state of isSmallScreenWidth. + getIsSmallScreenWidth: () => isSmallScreenWidthRef.current, + }); + + return ( + + + + ); +} + +ResponsiveStackNavigator.defaultProps = defaultProps; +ResponsiveStackNavigator.propTypes = propTypes; +ResponsiveStackNavigator.displayName = 'ResponsiveStackNavigator'; + +export default createNavigatorFactory(ResponsiveStackNavigator); From f7d19008dfa0ca6f68986396776ed57a533c5fba Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Oct 2023 19:15:19 +0200 Subject: [PATCH 061/312] update options on transition end --- src/pages/SearchPage.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 272fb30de858..f0796ffb7000 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -65,15 +65,13 @@ class SearchPage extends Component { this.searchRendered = this.searchRendered.bind(this); this.selectReport = this.selectReport.bind(this); this.onChangeText = this.onChangeText.bind(this); + this.updateOptions = this.updateOptions.bind(this); this.debouncedUpdateOptions = _.debounce(this.updateOptions.bind(this), 75); - - const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions(props.reports, props.personalDetails, '', props.betas); - this.state = { searchValue: '', - recentReports, - personalDetails, - userToInvite, + recentReports: {}, + personalDetails: {}, + userToInvite: {}, }; } @@ -186,6 +184,7 @@ class SearchPage extends Component { {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( <> From 0e4411cff508bdad6b54f855e843156699d98981 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Oct 2023 19:15:20 +0200 Subject: [PATCH 062/312] remove timeout on search focus --- src/components/OptionsSelector/BaseOptionsSelector.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 0125fc8e178e..5e37db057db5 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -75,9 +75,7 @@ class BaseOptionsSelector extends Component { this.subscribeToKeyboardShortcut(); if (this.props.isFocused && this.props.autoFocus && this.textInput) { - this.focusTimeout = setTimeout(() => { - this.textInput.focus(); - }, CONST.ANIMATED_TRANSITION); + this.textInput.focus(); } this.scrollToIndex(this.props.selectedOptions.length ? 0 : this.state.focusedIndex, false); @@ -139,10 +137,6 @@ class BaseOptionsSelector extends Component { } componentWillUnmount() { - if (this.focusTimeout) { - clearTimeout(this.focusTimeout); - } - this.unSubscribeFromKeyboardShortcut(); } From 62e087b9eca0d8608f01b3a0cecb1aea8d06ff4d Mon Sep 17 00:00:00 2001 From: VH Date: Wed, 25 Oct 2023 05:54:20 +0700 Subject: [PATCH 063/312] Always show confirmation button --- .../MoneyRequestParticipantsSelector.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 7e88ebe7db48..bc6e5f02ff5e 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -248,7 +248,6 @@ function MoneyRequestParticipantsSelector({ // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); - const shouldShowConfirmButton = !(participants.length > 1 && hasPolicyExpenseChatParticipant); const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND; return ( @@ -266,7 +265,6 @@ function MoneyRequestParticipantsSelector({ ref={forwardedRef} headerMessage={headerMessage} boldStyle - shouldShowConfirmButton={shouldShowConfirmButton && isAllowedToSplit} confirmButtonText={translate('iou.addToSplit')} onConfirmSelection={navigateToSplit} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} @@ -274,6 +272,7 @@ function MoneyRequestParticipantsSelector({ shouldShowOptions={isOptionsDataReady} shouldPreventDefaultFocusOnSelectRow={!Browser.isMobile()} shouldDelayFocus + footerContent /> ); From 40b36e0a15029bab343047d0c5e0a0752ead876e Mon Sep 17 00:00:00 2001 From: VH Date: Wed, 25 Oct 2023 06:23:18 +0700 Subject: [PATCH 064/312] Display warning message when split bill with multiple participants including a workspace --- .../MoneyRequestParticipantsSelector.js | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index bc6e5f02ff5e..62548fb2854c 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -16,6 +16,8 @@ import CONST from '../../../../CONST'; import personalDetailsPropType from '../../../personalDetailsPropType'; import reportPropTypes from '../../../reportPropTypes'; import refPropTypes from '../../../../components/refPropTypes'; +import Button from '../../../../components/Button'; +import FormHelpMessage from '../../../../components/FormHelpMessage'; const propTypes = { /** Beta features list */ @@ -248,8 +250,27 @@ function MoneyRequestParticipantsSelector({ // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); + const shouldShowErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND; + const footerContent = + {shouldShowErrorMessage && ( + + )} +