From cbdc5304a71b21f278ecbb8c24c0f438a6e32541 Mon Sep 17 00:00:00 2001 From: daledah Date: Thu, 12 Sep 2024 14:29:22 +0700 Subject: [PATCH 01/55] fix: Error when selecting distance rate that no longer has tax rate --- src/libs/actions/TaxRate.ts | 48 +++++++++++++++++++ .../PolicyDistanceRateDetailsPage.tsx | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 514b73915633..c8a2962d7dc4 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -286,6 +286,10 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { const policyTaxRates = policy?.taxRates?.taxes; const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault; const firstTaxID = Object.keys(policyTaxRates ?? {}).sort((a, b) => a.localeCompare(b))[0]; + const customUnits = policy?.customUnits ?? {}; + const ratesToUpdate = Object.values(customUnits?.[Object.keys(customUnits)[0]]?.rates).filter( + (rate) => !!rate.attributes?.taxRateExternalID && taxesToDelete.includes(rate.attributes?.taxRateExternalID), + ); if (!policyTaxRates) { console.debug('Policy or tax rates not found'); @@ -294,6 +298,35 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { const isForeignTaxRemoved = foreignTaxDefault && taxesToDelete.includes(foreignTaxDefault); + const optimisticRates: Record = {}; + const successRates: Record = {}; + const failureRates: Record = {}; + + ratesToUpdate.forEach((rate) => { + const rateID = rate.customUnitRateID ?? ''; + optimisticRates[rateID] = { + ...rate, + attributes: { + ...rate?.attributes, + taxRateExternalID: undefined, + taxClaimablePercentage: undefined, + }, + pendingFields: { + taxRateExternalID: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + taxClaimablePercentage: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }; + successRates[rateID] = {...rate, pendingFields: {taxRateExternalID: null}}; + failureRates[rateID] = { + ...rate, + pendingFields: {taxRateExternalID: null, taxClaimablePercentage: null}, + errorFields: { + taxRateExternalID: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + taxClaimablePercentage: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }; + }); + const onyxData: OnyxData = { optimisticData: [ { @@ -308,6 +341,11 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, + customUnits: policy.customUnits && { + [Object.keys(policy.customUnits)[0]]: { + rates: optimisticRates, + }, + }, }, }, ], @@ -323,6 +361,11 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, + customUnits: policy.customUnits && { + [Object.keys(policy.customUnits)[0]]: { + rates: successRates, + }, + }, }, }, ], @@ -341,6 +384,11 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, + customUnits: policy.customUnits && { + [Object.keys(policy.customUnits)[0]]: { + rates: failureRates, + }, + }, }, }, ], diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx index c4d033351b37..982e4799d781 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx @@ -155,7 +155,7 @@ function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetail )} - {isDistanceTrackTaxEnabled && isPolicyTrackTaxEnabled && ( + {isDistanceTrackTaxEnabled && !!taxRate && isPolicyTrackTaxEnabled && ( Date: Thu, 12 Sep 2024 14:59:11 +0700 Subject: [PATCH 02/55] fix: undefined --- src/libs/actions/TaxRate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index c8a2962d7dc4..c9073bd4a668 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -287,7 +287,7 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault; const firstTaxID = Object.keys(policyTaxRates ?? {}).sort((a, b) => a.localeCompare(b))[0]; const customUnits = policy?.customUnits ?? {}; - const ratesToUpdate = Object.values(customUnits?.[Object.keys(customUnits)[0]]?.rates).filter( + const ratesToUpdate = Object.values(customUnits?.[Object.keys(customUnits)[0]]?.rates ?? {}).filter( (rate) => !!rate.attributes?.taxRateExternalID && taxesToDelete.includes(rate.attributes?.taxRateExternalID), ); From af0bbb283d1ef35d5a9aa73888b77da4f86da908 Mon Sep 17 00:00:00 2001 From: daledah Date: Wed, 18 Sep 2024 12:07:26 +0700 Subject: [PATCH 03/55] fix: change undefined to null in onyx data --- src/libs/actions/TaxRate.ts | 4 ++-- src/types/onyx/Policy.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index c9073bd4a668..bbc63b60367b 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -308,8 +308,8 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { ...rate, attributes: { ...rate?.attributes, - taxRateExternalID: undefined, - taxClaimablePercentage: undefined, + taxRateExternalID: null, + taxClaimablePercentage: null, }, pendingFields: { taxRateExternalID: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index f2604d723f05..c1c5e78126a0 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -11,10 +11,10 @@ type Unit = 'mi' | 'km'; /** Tax rate attributes of the policy distance rate */ type TaxRateAttributes = { /** Percentage of the tax that can be reclaimable */ - taxClaimablePercentage?: number; + taxClaimablePercentage?: number | null; /** External ID associated to this tax rate */ - taxRateExternalID?: string; + taxRateExternalID?: string | null; }; /** Model of policy distance rate */ From 6c5127e5fbd264f8dc9f10623ca2a72f2b2d7e17 Mon Sep 17 00:00:00 2001 From: daledah Date: Thu, 19 Sep 2024 14:19:12 +0700 Subject: [PATCH 04/55] fix: add missing key, refactor withOnyx --- src/libs/actions/TaxRate.ts | 2 +- .../PolicyDistanceRateDetailsPage.tsx | 20 +++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index bbc63b60367b..439e5131508f 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -316,7 +316,7 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { taxClaimablePercentage: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, }; - successRates[rateID] = {...rate, pendingFields: {taxRateExternalID: null}}; + successRates[rateID] = {...rate, pendingFields: {taxRateExternalID: null, taxClaimablePercentage: null}}; failureRates[rateID] = { ...rate, pendingFields: {taxRateExternalID: null, taxClaimablePercentage: null}, diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx index 982e4799d781..0b7d925f2ee2 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx @@ -1,8 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useState} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -26,22 +25,17 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; import type {Rate, TaxRateAttributes} from '@src/types/onyx/Policy'; -type PolicyDistanceRateDetailsPageOnyxProps = { - /** Policy details */ - policy: OnyxEntry; -}; +type PolicyDistanceRateDetailsPageProps = StackScreenProps; -type PolicyDistanceRateDetailsPageProps = PolicyDistanceRateDetailsPageOnyxProps & StackScreenProps; - -function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetailsPageProps) { +function PolicyDistanceRateDetailsPage({route}: PolicyDistanceRateDetailsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const policyID = route.params.policyID; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`); const rateID = route.params.rateID; const customUnits = policy?.customUnits ?? {}; const customUnit = customUnits[Object.keys(customUnits)[0]]; @@ -209,8 +203,4 @@ function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetail PolicyDistanceRateDetailsPage.displayName = 'PolicyDistanceRateDetailsPage'; -export default withOnyx({ - policy: { - key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`, - }, -})(PolicyDistanceRateDetailsPage); +export default PolicyDistanceRateDetailsPage; From a456b625745e23313b777d32bb37c18f9d12c49b Mon Sep 17 00:00:00 2001 From: daledah Date: Thu, 19 Sep 2024 23:47:28 +0700 Subject: [PATCH 05/55] refactor: onyx keys, types --- src/libs/actions/TaxRate.ts | 6 +++--- src/types/onyx/Policy.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 439e5131508f..bf0087d26738 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -1,4 +1,4 @@ -import type {OnyxCollection} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {FormOnyxValues} from '@components/Form/types'; import * as API from '@libs/API'; @@ -298,7 +298,7 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { const isForeignTaxRemoved = foreignTaxDefault && taxesToDelete.includes(foreignTaxDefault); - const optimisticRates: Record = {}; + const optimisticRates: Record> = {}; const successRates: Record = {}; const failureRates: Record = {}; @@ -318,7 +318,7 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { }; successRates[rateID] = {...rate, pendingFields: {taxRateExternalID: null, taxClaimablePercentage: null}}; failureRates[rateID] = { - ...rate, + attributes: {...rate?.attributes}, pendingFields: {taxRateExternalID: null, taxClaimablePercentage: null}, errorFields: { taxRateExternalID: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 26095aec818b..95a9907526e4 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -11,10 +11,10 @@ type Unit = 'mi' | 'km'; /** Tax rate attributes of the policy distance rate */ type TaxRateAttributes = { /** Percentage of the tax that can be reclaimable */ - taxClaimablePercentage?: number | null; + taxClaimablePercentage?: number; /** External ID associated to this tax rate */ - taxRateExternalID?: string | null; + taxRateExternalID?: string; }; /** Model of policy distance rate */ From 4d94b1113a3bdea0bb2f7b3f901574c5cb7acf74 Mon Sep 17 00:00:00 2001 From: daledah Date: Fri, 20 Sep 2024 16:34:55 +0700 Subject: [PATCH 06/55] fix: clean onyx data --- src/libs/actions/TaxRate.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index bf0087d26738..81f3dbbb3ab4 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -305,9 +305,7 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { ratesToUpdate.forEach((rate) => { const rateID = rate.customUnitRateID ?? ''; optimisticRates[rateID] = { - ...rate, attributes: { - ...rate?.attributes, taxRateExternalID: null, taxClaimablePercentage: null, }, @@ -316,7 +314,7 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { taxClaimablePercentage: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, }; - successRates[rateID] = {...rate, pendingFields: {taxRateExternalID: null, taxClaimablePercentage: null}}; + successRates[rateID] = {pendingFields: {taxRateExternalID: null, taxClaimablePercentage: null}}; failureRates[rateID] = { attributes: {...rate?.attributes}, pendingFields: {taxRateExternalID: null, taxClaimablePercentage: null}, From 8da97f8f49acc09d34b202f64acbeb36c1d205ee Mon Sep 17 00:00:00 2001 From: HezekielT Date: Sun, 29 Sep 2024 12:48:56 +0300 Subject: [PATCH 07/55] add isUploading to language and use it in DefaultAttachmentView --- .../AttachmentView/DefaultAttachmentView/index.tsx | 7 +++++-- src/languages/en.ts | 1 + src/languages/es.ts | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx index ee594f66aabc..e6ac9f9f21c7 100644 --- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -24,9 +24,12 @@ type DefaultAttachmentViewProps = { containerStyles?: StyleProp; icon?: IconAsset; + + /** Flag indicating if the attachment is being uploaded. */ + isUploading?: boolean; }; -function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon}: DefaultAttachmentViewProps) { +function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon, isUploading}: DefaultAttachmentViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -53,7 +56,7 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa )} {shouldShowLoadingSpinnerIcon && ( - + Date: Sun, 29 Sep 2024 13:07:57 +0300 Subject: [PATCH 08/55] pass isUploading and use it show loading spinner. --- .../BaseAnchorForAttachmentsOnly.tsx | 3 ++- src/components/Attachments/AttachmentView/index.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 1f1844bf20db..9ef0ab97f436 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -43,7 +43,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive, isDisabled}) => ( { if (isDownloading || isOffline || !sourceID) { return; @@ -69,6 +69,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow shouldShowDownloadIcon={!!sourceID && !isOffline} shouldShowLoadingSpinnerIcon={isDownloading} isUsedAsChatAttachment + isUploading={!sourceID} /> )} diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 6d14a41741a6..03669d2fd9aa 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -77,6 +77,10 @@ type AttachmentViewProps = AttachmentViewOnyxProps & /* Flag indicating whether the attachment has been uploaded. */ isUploaded?: boolean; + + /** Flag indicating if the attachment is being uploaded. */ + isUploading?: boolean; + }; function AttachmentView({ @@ -101,6 +105,7 @@ function AttachmentView({ duration, isUsedAsChatAttachment, isUploaded = true, + isUploading = false, }: AttachmentViewProps) { const {translate} = useLocalize(); const {updateCurrentlyPlayingURL} = usePlaybackContext(); @@ -288,8 +293,9 @@ function AttachmentView({ ); } From fcf13c9bae431e19745b6820d04225c9ffa3dbd1 Mon Sep 17 00:00:00 2001 From: HezekielT Date: Mon, 30 Sep 2024 03:58:30 +0300 Subject: [PATCH 09/55] replace withOnyx by useOnyx --- .../BaseAnchorForAttachmentsOnly.tsx | 23 +++++-------------- .../Attachments/AttachmentView/index.tsx | 22 ++++++------------ 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 9ef0ab97f436..f0e3305165db 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import AttachmentView from '@components/Attachments/AttachmentView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext'; @@ -13,16 +13,10 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as Download from '@userActions/Download'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Download as OnyxDownload} from '@src/types/onyx'; import type AnchorForAttachmentsOnlyProps from './types'; -type BaseAnchorForAttachmentsOnlyOnyxProps = { - /** If a file download is happening */ - download: OnyxEntry; -}; -type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps & - BaseAnchorForAttachmentsOnlyOnyxProps & { +type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps & { /** Press in handler for the link */ onPressIn?: () => void; @@ -30,10 +24,12 @@ type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps & onPressOut?: () => void; }; -function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', download, onPressIn, onPressOut}: BaseAnchorForAttachmentsOnlyProps) { +function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onPressIn, onPressOut}: BaseAnchorForAttachmentsOnlyProps) { const sourceURLWithAuth = addEncryptedAuthTokenToURL(source); const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; + const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`) + const {isOffline} = useNetwork(); const styles = useThemeStyles(); @@ -79,11 +75,4 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow BaseAnchorForAttachmentsOnly.displayName = 'BaseAnchorForAttachmentsOnly'; -export default withOnyx({ - download: { - key: ({source}) => { - const sourceID = (source?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; - return `${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`; - }, - }, -})(BaseAnchorForAttachmentsOnly); +export default BaseAnchorForAttachmentsOnly; diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 03669d2fd9aa..505bb812f659 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -3,7 +3,7 @@ import React, {memo, useContext, useEffect, useState} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import DistanceEReceipt from '@components/DistanceEReceipt'; @@ -31,12 +31,7 @@ import AttachmentViewVideo from './AttachmentViewVideo'; import DefaultAttachmentView from './DefaultAttachmentView'; import HighResolutionInfo from './HighResolutionInfo'; -type AttachmentViewOnyxProps = { - transaction: OnyxEntry; -}; - -type AttachmentViewProps = AttachmentViewOnyxProps & - Attachment & { +type AttachmentViewProps = Attachment & { /** Whether this view is the active screen */ isFocused?: boolean; @@ -99,7 +94,7 @@ function AttachmentView({ isWorkspaceAvatar, maybeIcon, fallbackSource, - transaction, + transactionID = '-1', reportActionID, isHovered, duration, @@ -110,6 +105,9 @@ function AttachmentView({ const {translate} = useLocalize(); const {updateCurrentlyPlayingURL} = usePlaybackContext(); const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); + + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -302,12 +300,6 @@ function AttachmentView({ AttachmentView.displayName = 'AttachmentView'; -export default memo( - withOnyx({ - transaction: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - }, - })(AttachmentView), -); +export default memo(AttachmentView); export type {AttachmentViewProps}; From 87aa484d1449c0d92e27de932e68fb8065f65822 Mon Sep 17 00:00:00 2001 From: HezekielT Date: Mon, 30 Sep 2024 04:00:27 +0300 Subject: [PATCH 10/55] run prettier --- .../BaseAnchorForAttachmentsOnly.tsx | 13 ++-- .../Attachments/AttachmentView/index.tsx | 63 +++++++++---------- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index f0e3305165db..5f2ab6a761ee 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -15,20 +15,19 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type AnchorForAttachmentsOnlyProps from './types'; - type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps & { - /** Press in handler for the link */ - onPressIn?: () => void; + /** Press in handler for the link */ + onPressIn?: () => void; - /** Press out handler for the link */ - onPressOut?: () => void; - }; + /** Press out handler for the link */ + onPressOut?: () => void; +}; function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onPressIn, onPressOut}: BaseAnchorForAttachmentsOnlyProps) { const sourceURLWithAuth = addEncryptedAuthTokenToURL(source); const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; - const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`) + const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`); const {isOffline} = useNetwork(); const styles = useThemeStyles(); diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 505bb812f659..41294807397b 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -32,51 +32,50 @@ import DefaultAttachmentView from './DefaultAttachmentView'; import HighResolutionInfo from './HighResolutionInfo'; type AttachmentViewProps = Attachment & { - /** Whether this view is the active screen */ - isFocused?: boolean; + /** Whether this view is the active screen */ + isFocused?: boolean; - /** Function for handle on press */ - onPress?: (e?: GestureResponderEvent | KeyboardEvent) => void; + /** Function for handle on press */ + onPress?: (e?: GestureResponderEvent | KeyboardEvent) => void; - isUsedInAttachmentModal?: boolean; + isUsedInAttachmentModal?: boolean; - /** Flag to show/hide download icon */ - shouldShowDownloadIcon?: boolean; + /** Flag to show/hide download icon */ + shouldShowDownloadIcon?: boolean; - /** Flag to show the loading indicator */ - shouldShowLoadingSpinnerIcon?: boolean; + /** Flag to show the loading indicator */ + shouldShowLoadingSpinnerIcon?: boolean; - /** Notify parent that the UI should be modified to accommodate keyboard */ - onToggleKeyboard?: (shouldFadeOut: boolean) => void; + /** Notify parent that the UI should be modified to accommodate keyboard */ + onToggleKeyboard?: (shouldFadeOut: boolean) => void; - /** A callback when the PDF fails to load */ - onPDFLoadError?: () => void; + /** A callback when the PDF fails to load */ + onPDFLoadError?: () => void; - /** Extra styles to pass to View wrapper */ - containerStyles?: StyleProp; + /** Extra styles to pass to View wrapper */ + containerStyles?: StyleProp; - /** Denotes whether it is a workspace avatar or not */ - isWorkspaceAvatar?: boolean; + /** Denotes whether it is a workspace avatar or not */ + isWorkspaceAvatar?: boolean; - /** Denotes whether it is an icon (ex: SVG) */ - maybeIcon?: boolean; + /** Denotes whether it is an icon (ex: SVG) */ + maybeIcon?: boolean; - /** Fallback source to use in case of error */ - fallbackSource?: AttachmentSource; + /** Fallback source to use in case of error */ + fallbackSource?: AttachmentSource; - /* Whether it is hovered or not */ - isHovered?: boolean; + /* Whether it is hovered or not */ + isHovered?: boolean; - /** Whether the attachment is used as a chat attachment */ - isUsedAsChatAttachment?: boolean; + /** Whether the attachment is used as a chat attachment */ + isUsedAsChatAttachment?: boolean; - /* Flag indicating whether the attachment has been uploaded. */ - isUploaded?: boolean; + /* Flag indicating whether the attachment has been uploaded. */ + isUploaded?: boolean; - /** Flag indicating if the attachment is being uploaded. */ - isUploading?: boolean; - - }; + /** Flag indicating if the attachment is being uploaded. */ + isUploading?: boolean; +}; function AttachmentView({ source, @@ -107,7 +106,7 @@ function AttachmentView({ const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); - + const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); From 33360aa772daa1fc721dae875790c1c010ddd159 Mon Sep 17 00:00:00 2001 From: HezekielT Date: Mon, 30 Sep 2024 04:07:31 +0300 Subject: [PATCH 11/55] remove unused imports --- src/components/Attachments/AttachmentView/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 41294807397b..6c6d08f2c306 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -2,7 +2,6 @@ import {Str} from 'expensify-common'; import React, {memo, useContext, useEffect, useState} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; @@ -24,7 +23,6 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import type {ColorValue} from '@styles/utils/types'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Transaction} from '@src/types/onyx'; import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; import AttachmentViewVideo from './AttachmentViewVideo'; From 86d532abfd12fc12f021524c906a5aeb51eab404 Mon Sep 17 00:00:00 2001 From: HezekielT Date: Mon, 30 Sep 2024 04:07:45 +0300 Subject: [PATCH 12/55] remove unused imports --- src/components/Attachments/AttachmentView/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 6c6d08f2c306..132f0affc231 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -288,6 +288,7 @@ function AttachmentView({ Date: Mon, 30 Sep 2024 04:09:55 +0300 Subject: [PATCH 13/55] remove unused imports --- .../AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 5f2ab6a761ee..443a553d4689 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import AttachmentView from '@components/Attachments/AttachmentView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; From c95e5e9225b48e110357406a601a8881153693ed Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Thu, 3 Oct 2024 18:37:07 +0800 Subject: [PATCH 14/55] [P2P Distance] Rate currency doesn't match expense currency --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 66c773ab9534..332b9d214de8 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -213,7 +213,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const hasRoute = TransactionUtils.hasRoute(transactionBackup ?? transaction, isDistanceRequest); const rateID = TransactionUtils.getRateID(transaction) ?? '-1'; - const currency = policy ? policy.outputCurrency : PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; + const currency = transactionCurrency ?? CONST.CURRENCY.USD; const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(currency) : distanceRates[rateID] ?? {}; const {unit} = mileageRate; From af3f047a6660480880218f9a627f57d0d436cdc1 Mon Sep 17 00:00:00 2001 From: daledah Date: Fri, 4 Oct 2024 01:19:12 +0700 Subject: [PATCH 15/55] fix: lint --- src/libs/actions/TaxRate.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index d3b2fdfc9e4e..0061497be073 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -289,7 +289,8 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { .sort((a, b) => a.localeCompare(b)) .at(0); const customUnits = policy?.customUnits ?? {}; - const ratesToUpdate = Object.values(customUnits?.[Object.keys(customUnits)[0]]?.rates ?? {}).filter( + const customUnitID = Object.keys(customUnits).at(0) ?? '-1'; + const ratesToUpdate = Object.values(customUnits?.[customUnitID]?.rates ?? {}).filter( (rate) => !!rate.attributes?.taxRateExternalID && taxesToDelete.includes(rate.attributes?.taxRateExternalID), ); @@ -341,8 +342,8 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, - customUnits: policy.customUnits && { - [Object.keys(policy.customUnits)[0]]: { + customUnits: customUnits && { + [customUnitID]: { rates: optimisticRates, }, }, @@ -361,8 +362,8 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, - customUnits: policy.customUnits && { - [Object.keys(policy.customUnits)[0]]: { + customUnits: customUnits && { + [customUnitID]: { rates: successRates, }, }, @@ -384,8 +385,8 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, - customUnits: policy.customUnits && { - [Object.keys(policy.customUnits)[0]]: { + customUnits: customUnits && { + [customUnitID]: { rates: failureRates, }, }, From 830ac47fc05e52a975b7a4a1316f12cec622adf7 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Mon, 30 Sep 2024 16:00:08 +0200 Subject: [PATCH 16/55] Remove old chat finder functionality --- src/SCREENS.ts | 1 - src/libs/E2E/reactNativeLaunchingTest.ts | 1 - .../Navigators/LeftModalNavigator.tsx | 5 - .../ChatFinderPage/ChatFinderPageFooter.tsx | 11 - src/pages/ChatFinderPage/index.tsx | 209 ---------------- tests/e2e/config.ts | 4 - tests/perf-test/ChatFinderPage.perf-test.tsx | 232 ------------------ 7 files changed, 463 deletions(-) delete mode 100644 src/pages/ChatFinderPage/ChatFinderPageFooter.tsx delete mode 100644 src/pages/ChatFinderPage/index.tsx delete mode 100644 tests/perf-test/ChatFinderPage.perf-test.tsx diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9a94d612dc80..8d363e6317c1 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -141,7 +141,6 @@ const SCREENS = { ROOT: 'SaveTheWorld_Root', }, LEFT_MODAL: { - CHAT_FINDER: 'ChatFinder', WORKSPACE_SWITCHER: 'WorkspaceSwitcher', }, RIGHT_MODAL: { diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index f952998f0aad..21cb0336c5fb 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -32,7 +32,6 @@ if (!appInstanceId) { // import your test here, define its name and config first in e2e/config.js const tests: Tests = { [E2EConfig.TEST_NAMES.AppStartTime]: require('./tests/appStartTimeTest.e2e').default, - [E2EConfig.TEST_NAMES.OpenChatFinderPage]: require('./tests/openChatFinderPageTest.e2e').default, [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, [E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default, diff --git a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx index 077bdce94545..50439c19845e 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx @@ -14,7 +14,6 @@ import Overlay from './Overlay'; type LeftModalNavigatorProps = StackScreenProps; -const loadChatFinder = () => require('../../../../pages/ChatFinderPage').default; const loadWorkspaceSwitcherPage = () => require('../../../../pages/WorkspaceSwitcherPage').default; const Stack = createStackNavigator(); @@ -37,10 +36,6 @@ function LeftModalNavigator({navigation}: LeftModalNavigatorProps) { screenOptions={screenOptions} id={NAVIGATORS.LEFT_MODAL_NAVIGATOR} > - ; -} - -ChatFinderPageFooter.displayName = 'ChatFinderPageFooter'; - -export default ChatFinderPageFooter; diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx deleted file mode 100644 index aabf881a8bed..000000000000 --- a/src/pages/ChatFinderPage/index.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import type {StackScreenProps} from '@react-navigation/stack'; -import isEmpty from 'lodash/isEmpty'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useOptionsList} from '@components/OptionListContextProvider'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import UserListItem from '@components/SelectionList/UserListItem'; -import useCancelSearchOnModalClose from '@hooks/useCancelSearchOnModalClose'; -import useDebouncedState from '@hooks/useDebouncedState'; -import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import Navigation from '@libs/Navigation/Navigation'; -import type {RootStackParamList} from '@libs/Navigation/types'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import Performance from '@libs/Performance'; -import type {OptionData} from '@libs/ReportUtils'; -import * as Report from '@userActions/Report'; -import Timing from '@userActions/Timing'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; -import ChatFinderPageFooter from './ChatFinderPageFooter'; - -type ChatFinderPageOnyxProps = { - /** Beta features list */ - betas: OnyxEntry; - - /** Whether or not we are searching for reports on the server */ - isSearchingForReports: OnyxEntry; -}; - -type ChatFinderPageProps = ChatFinderPageOnyxProps & StackScreenProps; - -type ChatFinderPageSectionItem = { - data: OptionData[]; - shouldShow: boolean; -}; - -type ChatFinderPageSectionList = ChatFinderPageSectionItem[]; - -const setPerformanceTimersEnd = () => { - Timing.end(CONST.TIMING.CHAT_FINDER_RENDER); - Performance.markEnd(CONST.TIMING.CHAT_FINDER_RENDER); -}; - -const ChatFinderPageFooterInstance = ; - -function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPageProps) { - const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); - const {translate} = useLocalize(); - const {isOffline} = useNetwork(); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: isScreenTransitionEnd, - }); - - const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; - - const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - const [, debouncedSearchValueInServer, setSearchValueInServer] = useDebouncedState('', 500); - const updateSearchValue = useCallback( - (value: string) => { - setSearchValue(value); - setSearchValueInServer(value); - }, - [setSearchValue, setSearchValueInServer], - ); - useCancelSearchOnModalClose(); - - useEffect(() => { - Report.searchInServer(debouncedSearchValueInServer.trim()); - }, [debouncedSearchValueInServer]); - - const searchOptions = useMemo(() => { - if (!areOptionsInitialized || !isScreenTransitionEnd) { - return { - recentReports: [], - personalDetails: [], - userToInvite: null, - currentUserOption: null, - categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], - headerMessage: '', - }; - } - const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); - const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, !!optionList.userToInvite, ''); - return {...optionList, headerMessage: header}; - }, [areOptionsInitialized, betas, isScreenTransitionEnd, options]); - - const filteredOptions = useMemo(() => { - if (debouncedSearchValue.trim() === '') { - return { - recentReports: [], - personalDetails: [], - userToInvite: null, - headerMessage: '', - }; - } - - Timing.start(CONST.TIMING.SEARCH_FILTER_OPTIONS); - const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true}); - Timing.end(CONST.TIMING.SEARCH_FILTER_OPTIONS); - - const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length + Number(!!newOptions.userToInvite) > 0, false, debouncedSearchValue); - return { - recentReports: newOptions.recentReports, - personalDetails: newOptions.personalDetails, - userToInvite: newOptions.userToInvite, - headerMessage: header, - }; - }, [debouncedSearchValue, searchOptions]); - - const {recentReports, personalDetails: localPersonalDetails, userToInvite, headerMessage} = debouncedSearchValue.trim() !== '' ? filteredOptions : searchOptions; - - const sections = useMemo((): ChatFinderPageSectionList => { - const newSections: ChatFinderPageSectionList = []; - - if (recentReports?.length > 0) { - newSections.push({ - data: recentReports, - shouldShow: true, - }); - } - - if (localPersonalDetails.length > 0) { - newSections.push({ - data: localPersonalDetails, - shouldShow: true, - }); - } - - if (!isEmpty(userToInvite)) { - newSections.push({ - data: [userToInvite], - shouldShow: true, - }); - } - - return newSections; - }, [localPersonalDetails, recentReports, userToInvite]); - - const selectReport = (option: OptionData) => { - if (!option) { - return; - } - - if (option.reportID) { - Navigation.closeAndNavigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID)); - } else { - Report.navigateToAndOpenReport(option.login ? [option.login] : []); - } - }; - - const handleScreenTransitionEnd = () => { - setIsScreenTransitionEnd(true); - }; - - const {isDismissed} = useDismissedReferralBanners({referralContentType: CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}); - - return ( - - - - sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} - ListItem={UserListItem} - textInputValue={searchValue} - textInputLabel={translate('selectionList.nameEmailOrPhoneNumber')} - textInputHint={offlineMessage} - onChangeText={updateSearchValue} - headerMessage={headerMessage} - onLayout={setPerformanceTimersEnd} - onSelectRow={selectReport} - shouldSingleExecuteRowSelect - showLoadingPlaceholder={!areOptionsInitialized || !isScreenTransitionEnd} - footerContent={!isDismissed && ChatFinderPageFooterInstance} - isLoadingNewOptions={!!isSearchingForReports} - shouldDelayFocus={false} - /> - - ); -} - -ChatFinderPage.displayName = 'ChatFinderPage'; - -export default withOnyx({ - betas: { - key: ONYXKEYS.BETAS, - }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, -})(ChatFinderPage); diff --git a/tests/e2e/config.ts b/tests/e2e/config.ts index bdb24bee0bc5..8b14fb8de7a0 100644 --- a/tests/e2e/config.ts +++ b/tests/e2e/config.ts @@ -4,7 +4,6 @@ const OUTPUT_DIR = process.env.WORKING_DIRECTORY || './tests/e2e/results'; // add your test name here … const TEST_NAMES = { AppStartTime: 'App start time', - OpenChatFinderPage: 'Open chat finder page TTI', ReportTyping: 'Report typing', ChatOpening: 'Chat opening', Linking: 'Linking', @@ -73,9 +72,6 @@ export default { name: TEST_NAMES.AppStartTime, // ... any additional config you might need }, - [TEST_NAMES.OpenChatFinderPage]: { - name: TEST_NAMES.OpenChatFinderPage, - }, [TEST_NAMES.ReportTyping]: { name: TEST_NAMES.ReportTyping, reportScreen: { diff --git a/tests/perf-test/ChatFinderPage.perf-test.tsx b/tests/perf-test/ChatFinderPage.perf-test.tsx deleted file mode 100644 index 4346977a1cd0..000000000000 --- a/tests/perf-test/ChatFinderPage.perf-test.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import type * as NativeNavigation from '@react-navigation/native'; -import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; -import {fireEvent, screen} from '@testing-library/react-native'; -import React, {useMemo} from 'react'; -import type {ComponentType} from 'react'; -import Onyx from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {measurePerformance} from 'reassure'; -import {LocaleContextProvider} from '@components/LocaleContextProvider'; -import OptionListContextProvider, {OptionsListContext} from '@components/OptionListContextProvider'; -import {KeyboardStateProvider} from '@components/withKeyboardState'; -import type {WithNavigationFocusProps} from '@components/withNavigationFocus'; -import type {RootStackParamList} from '@libs/Navigation/types'; -import {createOptionList} from '@libs/OptionsListUtils'; -import ChatFinderPage from '@pages/ChatFinderPage'; -import ComposeProviders from '@src/components/ComposeProviders'; -import OnyxProvider from '@src/components/OnyxProvider'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; -import type {Beta, PersonalDetails, Report} from '@src/types/onyx'; -import createCollection from '../utils/collections/createCollection'; -import createPersonalDetails from '../utils/collections/personalDetails'; -import createRandomReport from '../utils/collections/reports'; -import createAddListenerMock from '../utils/createAddListenerMock'; -import * as TestHelper from '../utils/TestHelper'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; - -jest.mock('lodash/debounce', () => - jest.fn((fn: Record) => { - // eslint-disable-next-line no-param-reassign - fn.cancel = jest.fn(); - return fn; - }), -); - -jest.mock('@src/libs/Log'); - -jest.mock('@src/libs/API', () => ({ - write: jest.fn(), - makeRequestWithSideEffects: jest.fn(), - read: jest.fn(), -})); - -jest.mock('@src/libs/Navigation/Navigation', () => ({ - dismissModalWithReport: jest.fn(), - getTopmostReportId: jest.fn(), - isNavigationReady: jest.fn(() => Promise.resolve()), - isDisplayedInModal: jest.fn(() => false), -})); - -jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); - return { - ...actualNav, - useFocusEffect: jest.fn(), - useIsFocused: () => true, - useRoute: () => jest.fn(), - useNavigation: () => ({ - navigate: jest.fn(), - addListener: () => jest.fn(), - }), - createNavigationContainerRef: () => ({ - addListener: () => jest.fn(), - removeListener: () => jest.fn(), - isReady: () => jest.fn(), - getCurrentRoute: () => jest.fn(), - getState: () => jest.fn(), - }), - }; -}); - -jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType) => { - function WithNavigationFocus(props: WithNavigationFocusProps) { - return ( - - ); - } - - WithNavigationFocus.displayName = 'WithNavigationFocus'; - - return WithNavigationFocus; -}); -// mock of useDismissedReferralBanners -jest.mock('../../src/hooks/useDismissedReferralBanners', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - __esModule: true, - default: jest.fn(() => ({ - isDismissed: false, - setAsDismissed: () => {}, - })), -})); - -const getMockedReports = (length = 100) => - createCollection( - (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, - (index) => createRandomReport(index), - length, - ); - -const getMockedPersonalDetails = (length = 100) => - createCollection( - (item) => item.accountID, - (index) => createPersonalDetails(index), - length, - ); - -const mockedReports = getMockedReports(600); -const mockedBetas = Object.values(CONST.BETAS); -const mockedPersonalDetails = getMockedPersonalDetails(100); -const mockedOptions = createOptionList(mockedPersonalDetails, mockedReports); - -beforeAll(() => - Onyx.init({ - keys: ONYXKEYS, - safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT], - }), -); - -// Initialize the network key for OfflineWithFeedback -beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock(); - wrapOnyxWithWaitForBatchedUpdates(Onyx); - Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); -}); - -// Clear out Onyx after each test so that each test starts with a clean state -afterEach(() => { - Onyx.clear(); -}); - -type ChatFinderPageProps = StackScreenProps & { - betas?: OnyxEntry; - reports?: OnyxCollection; - isSearchingForReports?: OnyxEntry; -}; - -function ChatFinderPageWrapper(args: ChatFinderPageProps) { - return ( - - - - - - ); -} - -function ChatFinderPageWithCachedOptions(args: ChatFinderPageProps) { - return ( - - ({options: mockedOptions, initializeOptions: () => {}, areOptionsInitialized: true}), [])}> - - - - ); -} - -test('[ChatFinderPage] should render list with cached options', async () => { - const {addListener} = createAddListenerMock(); - - const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); - }; - - const navigation = {addListener} as unknown as StackNavigationProp; - - return waitForBatchedUpdates() - .then(() => - Onyx.multiSet({ - ...mockedReports, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, - [ONYXKEYS.BETAS]: mockedBetas, - [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, - }), - ) - .then(() => - measurePerformance( - , - {scenario}, - ), - ); -}); - -test('[ChatFinderPage] should interact when text input changes', async () => { - const {addListener} = createAddListenerMock(); - - const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); - - const input = screen.getByTestId('selection-list-text-input'); - fireEvent.changeText(input, 'Email Four'); - fireEvent.changeText(input, 'Report'); - fireEvent.changeText(input, 'Email Five'); - }; - - const navigation = {addListener} as unknown as StackNavigationProp; - - return waitForBatchedUpdates() - .then(() => - Onyx.multiSet({ - ...mockedReports, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, - [ONYXKEYS.BETAS]: mockedBetas, - [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, - }), - ) - .then(() => - measurePerformance( - , - {scenario}, - ), - ); -}); From 4052f9a4db738cce4afbe8573a33f838db2afe59 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 2 Oct 2024 13:06:17 +0200 Subject: [PATCH 17/55] Use SearchRouter instead of Chat finder and migrate e2e tests --- src/CONST.ts | 2 +- src/ROUTES.ts | 1 - .../Search/SearchRouter/SearchButton.tsx | 12 +- src/libs/E2E/reactNativeLaunchingTest.ts | 2 + ...est.e2e.ts => openSearchRouterTest.e2e.ts} | 17 +- .../Navigation/AppNavigator/AuthScreens.tsx | 10 +- .../createCustomBottomTabNavigator/TopBar.tsx | 33 +-- src/libs/Navigation/linkingConfig/config.ts | 1 - src/libs/Navigation/types.ts | 1 - src/pages/Search/SearchPageBottomTab.tsx | 7 +- .../SidebarScreen/BaseSidebarScreen.tsx | 5 + tests/e2e/config.ts | 4 + tests/perf-test/ChatFinderPage.perf-test.tsx | 217 ++++++++++++++++++ 13 files changed, 259 insertions(+), 53 deletions(-) rename src/libs/E2E/tests/{openChatFinderPageTest.e2e.ts => openSearchRouterTest.e2e.ts} (79%) create mode 100644 tests/perf-test/ChatFinderPage.perf-test.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 9a6bf21db303..0b130ae7564b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1084,7 +1084,7 @@ const CONST = { }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', - CHAT_FINDER_RENDER: 'search_render', + SEARCH_ROUTER_OPEN: 'search_router_render', CHAT_RENDER: 'chat_render', OPEN_REPORT: 'open_report', HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9c429dd3e909..d7e6b37a60fd 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -74,7 +74,6 @@ const ROUTES = { route: 'flag/:reportID/:reportActionID', getRoute: (reportID: string, reportActionID: string, backTo?: string) => getUrlWithBackToParam(`flag/${reportID}/${reportActionID}` as const, backTo), }, - CHAT_FINDER: 'chat-finder', PROFILE: { route: 'a/:accountID', getRoute: (accountID?: string | number, backTo?: string, login?: string) => { diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index 05693ad5ea22..53660ee1f6c2 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -5,7 +5,11 @@ import {PressableWithoutFeedback} from '@components/Pressable'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Performance from '@libs/Performance'; import Permissions from '@libs/Permissions'; +import * as Session from '@userActions/Session'; +import Timing from '@userActions/Timing'; +import CONST from '@src/CONST'; import {useSearchRouterContext} from './SearchRouterContext'; function SearchButton() { @@ -22,9 +26,13 @@ function SearchButton() { { + onPress={Session.checkIfActionIsAllowed(() => { + // Todo [Search] add finishing for this timing in + Timing.start(CONST.TIMING.SEARCH_ROUTER_OPEN); + Performance.markStart(CONST.TIMING.SEARCH_ROUTER_OPEN); + openSearchRouter(); - }} + })} > ('./tests/appStartTimeTest.e2e').default, + // Todo [Search] rename + [E2EConfig.TEST_NAMES.OpenSearchRouter]: require('./tests/openSearchRouterTest.e2e').default, [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, [E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default, diff --git a/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts similarity index 79% rename from src/libs/E2E/tests/openChatFinderPageTest.e2e.ts rename to src/libs/E2E/tests/openSearchRouterTest.e2e.ts index 2c2f2eda4efe..6ce1891a63d4 100644 --- a/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts +++ b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts @@ -3,14 +3,12 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve'; -import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; const test = () => { // check for login (if already logged in the action will simply resolve) - console.debug('[E2E] Logging in for chat finder'); + console.debug('[E2E] Logging in for new search router'); E2ELogin().then((neededLogin: boolean): Promise | undefined => { if (neededLogin) { @@ -20,7 +18,7 @@ const test = () => { ); } - console.debug('[E2E] Logged in, getting chat finder metrics and submitting them…'); + console.debug('[E2E] Logged in, getting search router metrics and submitting them…'); const [openSearchPagePromise, openSearchPageResolve] = getPromiseWithResolve(); const [loadSearchOptionsPromise, loadSearchOptionsResolve] = getPromiseWithResolve(); @@ -32,19 +30,12 @@ const test = () => { }); Performance.subscribeToMeasurements((entry) => { - if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { - console.debug(`[E2E] Sidebar loaded, navigating to chat finder route…`); - Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); - Navigation.navigate(ROUTES.CHAT_FINDER); - return; - } - console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`); - if (entry.name === CONST.TIMING.CHAT_FINDER_RENDER) { + if (entry.name === CONST.TIMING.SEARCH_ROUTER_OPEN) { E2EClient.submitTestResults({ branch: Config.E2E_BRANCH, - name: 'Open Chat Finder Page TTI', + name: 'Open Search Router TTI', metric: entry.duration, unit: 'ms', }) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index f5f35fd21025..62be199ee183 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -8,6 +8,7 @@ import ComposeProviders from '@components/ComposeProviders'; import OptionsListContextProvider from '@components/OptionListContextProvider'; import {SearchContextProvider} from '@components/Search/SearchContext'; import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; +import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useOnboardingFlowRouter from '@hooks/useOnboardingFlow'; import usePermissions from '@hooks/usePermissions'; @@ -233,6 +234,8 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const screenOptions = getRootNavigatorScreenOptions(shouldUseNarrowLayout, styles, StyleUtils); const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); + const {openSearchRouter} = useSearchRouterContext(); + const onboardingModalScreenOptions = useMemo(() => screenOptions.onboardingModalNavigator(onboardingIsMediumOrLargerScreenWidth), [screenOptions, onboardingIsMediumOrLargerScreenWidth]); const onboardingScreenOptions = useMemo( () => getOnboardingModalScreenOptions(shouldUseNarrowLayout, styles, StyleUtils, onboardingIsMediumOrLargerScreenWidth), @@ -241,6 +244,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const modal = useRef({}); const [didPusherInit, setDidPusherInit] = useState(false); const {isOnboardingCompleted} = useOnboardingFlowRouter(); + let initialReportID: string | undefined; const isInitialRender = useRef(true); if (isInitialRender.current) { @@ -363,13 +367,15 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie ); // Listen for the key K being pressed so that focus can be given to - // the chat switcher, or new group chat + // Search Router, or new group chat // based on the key modifiers pressed and the operating system const unsubscribeSearchShortcut = KeyboardShortcut.subscribe( searchShortcutConfig.shortcutKey, () => { Modal.close( - Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.CHAT_FINDER)), + Session.checkIfActionIsAllowed(() => { + openSearchRouter(); + }), true, true, ); diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx index 4684eb9637be..8967486165f8 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx @@ -2,32 +2,25 @@ import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Breadcrumbs from '@components/Breadcrumbs'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; import SearchButton from '@components/Search/SearchRouter/SearchButton'; import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import Performance from '@libs/Performance'; import * as SearchUtils from '@libs/SearchUtils'; import SignInButton from '@pages/home/sidebar/SignInButton'; import * as Session from '@userActions/Session'; -import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; isCustomSearchQuery?: boolean; shouldDisplaySearchRouter?: boolean}; +type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; shouldDisplayCancelSearch?: boolean}; -function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, isCustomSearchQuery = false, shouldDisplaySearchRouter = false}: TopBarProps) { +function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false}: TopBarProps) { const styles = useThemeStyles(); - const theme = useTheme(); const {translate} = useLocalize(); const policy = usePolicy(activeWorkspaceID); const [session] = useOnyx(ONYXKEYS.SESSION, {selector: (sessionValue) => sessionValue && {authTokenType: sessionValue.authTokenType}}); @@ -63,7 +56,7 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, {displaySignIn && } - {isCustomSearchQuery && ( + {shouldDisplayCancelSearch && ( {translate('common.cancel')} )} - {shouldDisplaySearchRouter && } - {displaySearch && ( - - { - Timing.start(CONST.TIMING.CHAT_FINDER_RENDER); - Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); - Navigation.navigate(ROUTES.CHAT_FINDER); - })} - > - - - - )} + {displaySearch && } ); diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 114e89ff2bf5..980b28c3d635 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -76,7 +76,6 @@ const config: LinkingOptions['config'] = { [SCREENS.NOT_FOUND]: '*', [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: { screens: { - [SCREENS.LEFT_MODAL.CHAT_FINDER]: ROUTES.CHAT_FINDER, [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: { path: ROUTES.WORKSPACE_SWITCHER, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b698681966e2..ecd2895053f1 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1236,7 +1236,6 @@ type TransactionDuplicateNavigatorParamList = { }; type LeftModalNavigatorParamList = { - [SCREENS.LEFT_MODAL.CHAT_FINDER]: undefined; [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: undefined; }; diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 38e4c5166884..b4fb2aac61ff 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -85,6 +85,8 @@ function SearchPageBottomTab() { ); } + const shouldDisplayCancelSearch = shouldUseNarrowLayout && !SearchUtils.isCannedSearchQuery(queryJSON); + return ( diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index edc8dfb3cb3a..e77f2000b85f 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -4,6 +4,7 @@ import {useOnyx} from 'react-native-onyx'; import ScreenWrapper from '@components/ScreenWrapper'; import useActiveWorkspaceFromNavigationState from '@hooks/useActiveWorkspaceFromNavigationState'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {updateLastAccessedWorkspace} from '@libs/actions/Policy/Policy'; import * as Browser from '@libs/Browser'; @@ -27,6 +28,7 @@ function BaseSidebarScreen() { const styles = useThemeStyles(); const activeWorkspaceID = useActiveWorkspaceFromNavigationState(); const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const [activeWorkspace] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID ?? -1}`); useEffect(() => { @@ -43,6 +45,8 @@ function BaseSidebarScreen() { updateLastAccessedWorkspace(undefined); }, [activeWorkspace, activeWorkspaceID]); + const shouldDisplaySearch = shouldUseNarrowLayout; + return ( + jest.fn((fn: Record) => { + // eslint-disable-next-line no-param-reassign + fn.cancel = jest.fn(); + return fn; + }), +); + +jest.mock('@src/libs/Log'); + +jest.mock('@src/libs/API', () => ({ + write: jest.fn(), + makeRequestWithSideEffects: jest.fn(), + read: jest.fn(), +})); + +jest.mock('@src/libs/Navigation/Navigation', () => ({ + dismissModalWithReport: jest.fn(), + getTopmostReportId: jest.fn(), + isNavigationReady: jest.fn(() => Promise.resolve()), + isDisplayedInModal: jest.fn(() => false), +})); + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useFocusEffect: jest.fn(), + useIsFocused: () => true, + useRoute: () => jest.fn(), + useNavigation: () => ({ + navigate: jest.fn(), + addListener: () => jest.fn(), + }), + createNavigationContainerRef: () => ({ + addListener: () => jest.fn(), + removeListener: () => jest.fn(), + isReady: () => jest.fn(), + getCurrentRoute: () => jest.fn(), + getState: () => jest.fn(), + }), + }; +}); + +jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType) => { + function WithNavigationFocus(props: WithNavigationFocusProps) { + return ( + + ); + } + + WithNavigationFocus.displayName = 'WithNavigationFocus'; + + return WithNavigationFocus; +}); +// mock of useDismissedReferralBanners +jest.mock('../../src/hooks/useDismissedReferralBanners', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: jest.fn(() => ({ + isDismissed: false, + setAsDismissed: () => {}, + })), +})); + +const getMockedReports = (length = 100) => + createCollection( + (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, + (index) => createRandomReport(index), + length, + ); + +const getMockedPersonalDetails = (length = 100) => + createCollection( + (item) => item.accountID, + (index) => createPersonalDetails(index), + length, + ); + +const mockedReports = getMockedReports(600); +const mockedBetas = Object.values(CONST.BETAS); +const mockedPersonalDetails = getMockedPersonalDetails(100); +const mockedOptions = createOptionList(mockedPersonalDetails, mockedReports); + +beforeAll(() => + Onyx.init({ + keys: ONYXKEYS, + safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT], + }), +); + +// Initialize the network key for OfflineWithFeedback +beforeEach(() => { + global.fetch = TestHelper.getGlobalFetchMock(); + wrapOnyxWithWaitForBatchedUpdates(Onyx); + Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); +}); + +// Clear out Onyx after each test so that each test starts with a clean state +afterEach(() => { + Onyx.clear(); +}); + +function ChatFinderPageWrapper(args: any) { + return ( + + + + {/* */} + + + ); +} + +function ChatFinderPageWithCachedOptions(args: any) { + return ( + + ({options: mockedOptions, initializeOptions: () => {}, areOptionsInitialized: true}), [])}> + {/* */} + + + ); +} + +test('[ChatFinderPage] should render list with cached options', async () => { + const {addListener} = createAddListenerMock(); + + const scenario = async () => { + await screen.findByTestId('ChatFinderPage'); // Todo [Search] fix testID no longer existing + }; + + // const navigation = {addListener} as unknown as StackNavigationProp; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + .then(() => measureRenders(, {scenario})); +}); + +test('[ChatFinderPage] should interact when text input changes', async () => { + const {addListener} = createAddListenerMock(); + + const scenario = async () => { + await screen.findByTestId('ChatFinderPage'); + + const input = screen.getByTestId('selection-list-text-input'); + fireEvent.changeText(input, 'Email Four'); + fireEvent.changeText(input, 'Report'); + fireEvent.changeText(input, 'Email Five'); + }; + + // const navigation = {addListener} as unknown as StackNavigationProp; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + .then(() => + measureRenders( + , + {scenario}, + ), + ); +}); From cb51d04588f2c442d9b18b1223104d10f73474b7 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Thu, 3 Oct 2024 16:55:14 +0200 Subject: [PATCH 18/55] Add performance timings handling for SearchRouterList --- src/components/Search/SearchRouter/SearchButton.tsx | 1 - src/components/Search/SearchRouter/SearchRouter.tsx | 3 +++ src/components/Search/SearchRouter/SearchRouterList.tsx | 9 +++++++++ src/libs/E2E/tests/openSearchRouterTest.e2e.ts | 6 +++--- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index 53660ee1f6c2..b1e59a323926 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -27,7 +27,6 @@ function SearchButton() { accessibilityLabel={translate('common.search')} style={[styles.flexRow, styles.touchableButtonImage]} onPress={Session.checkIfActionIsAllowed(() => { - // Todo [Search] add finishing for this timing in Timing.start(CONST.TIMING.SEARCH_ROUTER_OPEN); Performance.markStart(CONST.TIMING.SEARCH_ROUTER_OPEN); diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 8f5ad55bc0c9..868144492ce7 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -19,6 +19,7 @@ import * as SearchUtils from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; +import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -65,7 +66,9 @@ function SearchRouter() { }; } + Timing.start(CONST.TIMING.SEARCH_FILTER_OPTIONS); const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedInputValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true}); + Timing.end(CONST.TIMING.SEARCH_FILTER_OPTIONS); return { recentReports: newOptions.recentReports, diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 7d86ce1150d5..a58ea969d28f 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -13,10 +13,13 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import Performance from '@libs/Performance'; import {getAllTaxRates} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; import * as SearchUtils from '@libs/SearchUtils'; import * as Report from '@userActions/Report'; +import Timing from '@userActions/Timing'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -47,6 +50,11 @@ type SearchRouterListProps = { closeAndClearRouter: () => void; }; +const setPerformanceTimersEnd = () => { + Timing.end(CONST.TIMING.SEARCH_ROUTER_OPEN); + Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_OPEN); +}; + function isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem { if ('singleIcon' in item && item.singleIcon && 'query' in item && item.query) { return true; @@ -174,6 +182,7 @@ function SearchRouterList( ListItem={SearchRouterItem} containerStyle={[styles.mh100]} sectionListStyle={[isSmallScreenWidth ? styles.ph5 : styles.ph2, styles.pb2]} + onLayout={setPerformanceTimersEnd} ref={ref} showScrollIndicator={!isSmallScreenWidth} /> diff --git a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts index 6ce1891a63d4..6c4646b09fbf 100644 --- a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts +++ b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts @@ -20,10 +20,10 @@ const test = () => { console.debug('[E2E] Logged in, getting search router metrics and submitting them…'); - const [openSearchPagePromise, openSearchPageResolve] = getPromiseWithResolve(); + const [openSearchRouterPromise, openSearchRouterResolve] = getPromiseWithResolve(); const [loadSearchOptionsPromise, loadSearchOptionsResolve] = getPromiseWithResolve(); - Promise.all([openSearchPagePromise, loadSearchOptionsPromise]).then(() => { + Promise.all([openSearchRouterPromise, loadSearchOptionsPromise]).then(() => { console.debug(`[E2E] Submitting!`); E2EClient.submitTestDone(); @@ -40,7 +40,7 @@ const test = () => { unit: 'ms', }) .then(() => { - openSearchPageResolve(); + openSearchRouterResolve(); console.debug('[E2E] Done with search, exiting…'); }) .catch((err) => { From 635e0da1032b297e9834791d4c7200b5852bc625 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Fri, 4 Oct 2024 11:52:05 +0200 Subject: [PATCH 19/55] Migrate reassure tests for ChatFinder to SearchRouter --- .../Search/SearchRouter/SearchRouter.tsx | 34 +++++----- .../Search/SearchRouter/SearchRouterInput.tsx | 1 + .../Search/SearchRouter/SearchRouterModal.tsx | 2 +- src/libs/E2E/reactNativeLaunchingTest.ts | 1 - .../Navigation/AppNavigator/AuthScreens.tsx | 2 +- ...rf-test.tsx => SearchRouter.perf-test.tsx} | 65 +++++-------------- 6 files changed, 35 insertions(+), 70 deletions(-) rename tests/perf-test/{ChatFinderPage.perf-test.tsx => SearchRouter.perf-test.tsx} (72%) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 868144492ce7..f49112e86c6c 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -23,13 +23,16 @@ import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {useSearchRouterContext} from './SearchRouterContext'; import SearchRouterInput from './SearchRouterInput'; import SearchRouterList from './SearchRouterList'; const SEARCH_DEBOUNCE_DELAY = 150; -function SearchRouter() { +type SearchRouterProps = { + onRouterClose: () => void; +}; + +function SearchRouter({onRouterClose}: SearchRouterProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [betas] = useOnyx(ONYXKEYS.BETAS); @@ -37,7 +40,6 @@ function SearchRouter() { const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const {isSmallScreenWidth} = useResponsiveLayout(); - const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); const listRef = useRef(null); const [textInputValue, debouncedInputValue, setTextInputValue] = useDebouncedState('', 500); @@ -90,15 +92,6 @@ function SearchRouter() { Report.searchInServer(debouncedInputValue.trim()); }, [debouncedInputValue]); - useEffect(() => { - if (!textInputValue && isSearchRouterDisplayed) { - return; - } - listRef.current?.updateAndScrollToFocusedIndex(0); - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSearchRouterDisplayed]); - const contextualReportData = contextualReportID ? searchOptions.recentReports?.find((option) => option.reportID === contextualReportID) : undefined; const clearUserQuery = () => { @@ -135,40 +128,43 @@ function SearchRouter() { }; const closeAndClearRouter = useCallback(() => { - closeSearchRouter(); + onRouterClose(); clearUserQuery(); // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - }, [closeSearchRouter]); + }, [onRouterClose]); const onSearchSubmit = useCallback( (query: SearchQueryJSON | undefined) => { if (!query) { return; } - closeSearchRouter(); + onRouterClose(); const queryString = SearchUtils.buildSearchQueryString(query); Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: queryString})); clearUserQuery(); }, // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - [closeSearchRouter], + [onRouterClose], ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { - closeSearchRouter(); + onRouterClose(); clearUserQuery(); }); const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.popoverWidth}; return ( - + {isSmallScreenWidth && ( closeSearchRouter()} + onBackButtonPress={() => onRouterClose()} /> )} - {isSearchRouterDisplayed && } + {isSearchRouterDisplayed && } ); } diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index 672d66637ef0..fdd305baf88c 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -32,7 +32,6 @@ if (!appInstanceId) { // import your test here, define its name and config first in e2e/config.js const tests: Tests = { [E2EConfig.TEST_NAMES.AppStartTime]: require('./tests/appStartTimeTest.e2e').default, - // Todo [Search] rename [E2EConfig.TEST_NAMES.OpenSearchRouter]: require('./tests/openSearchRouterTest.e2e').default, [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 62be199ee183..2ec31d8a8688 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -7,8 +7,8 @@ import ActiveGuidesEventListener from '@components/ActiveGuidesEventListener'; import ComposeProviders from '@components/ComposeProviders'; import OptionsListContextProvider from '@components/OptionListContextProvider'; import {SearchContextProvider} from '@components/Search/SearchContext'; -import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext'; +import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useOnboardingFlowRouter from '@hooks/useOnboardingFlow'; import usePermissions from '@hooks/usePermissions'; diff --git a/tests/perf-test/ChatFinderPage.perf-test.tsx b/tests/perf-test/SearchRouter.perf-test.tsx similarity index 72% rename from tests/perf-test/ChatFinderPage.perf-test.tsx rename to tests/perf-test/SearchRouter.perf-test.tsx index ed43b699adc4..5be0c93105c3 100644 --- a/tests/perf-test/ChatFinderPage.perf-test.tsx +++ b/tests/perf-test/SearchRouter.perf-test.tsx @@ -2,11 +2,11 @@ import type * as NativeNavigation from '@react-navigation/native'; import {fireEvent, screen} from '@testing-library/react-native'; import React, {useMemo} from 'react'; import type {ComponentType} from 'react'; -import {View} from 'react-native'; import Onyx from 'react-native-onyx'; import {measureRenders} from 'reassure'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OptionListContextProvider, {OptionsListContext} from '@components/OptionListContextProvider'; +import SearchRouter from '@components/Search/SearchRouter/SearchRouter'; import {KeyboardStateProvider} from '@components/withKeyboardState'; import type {WithNavigationFocusProps} from '@components/withNavigationFocus'; import {createOptionList} from '@libs/OptionsListUtils'; @@ -23,8 +23,6 @@ import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; -// Todo [Search] Either migrate this test to tests new SearchRouter or remove it completely. - jest.mock('lodash/debounce', () => jest.fn((fn: Record) => { // eslint-disable-next-line no-param-reassign @@ -66,6 +64,9 @@ jest.mock('@react-navigation/native', () => { getCurrentRoute: () => jest.fn(), getState: () => jest.fn(), }), + useNavigationState: () => ({ + routes: [], + }), }; }); @@ -84,15 +85,6 @@ jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType return WithNavigationFocus; }); -// mock of useDismissedReferralBanners -jest.mock('../../src/hooks/useDismissedReferralBanners', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - __esModule: true, - default: jest.fn(() => ({ - isDismissed: false, - setAsDismissed: () => {}, - })), -})); const getMockedReports = (length = 100) => createCollection( @@ -132,44 +124,33 @@ afterEach(() => { Onyx.clear(); }); -function ChatFinderPageWrapper(args: any) { +const mockOnClose = jest.fn(); + +function SearchRouterWrapper() { return ( - - {/* */} + ); } -function ChatFinderPageWithCachedOptions(args: any) { +function SearchRouterWrapperWithCachedOptions() { return ( ({options: mockedOptions, initializeOptions: () => {}, areOptionsInitialized: true}), [])}> - {/* */} + ); } -test('[ChatFinderPage] should render list with cached options', async () => { - const {addListener} = createAddListenerMock(); - +test('[SearchRouter] should render chat list with cached options', async () => { const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); // Todo [Search] fix testID no longer existing + await screen.findByTestId('SearchRouter'); }; - // const navigation = {addListener} as unknown as StackNavigationProp; - return waitForBatchedUpdates() .then(() => Onyx.multiSet({ @@ -179,23 +160,19 @@ test('[ChatFinderPage] should render list with cached options', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => measureRenders(, {scenario})); + .then(() => measureRenders(, {scenario})); }); -test('[ChatFinderPage] should interact when text input changes', async () => { - const {addListener} = createAddListenerMock(); - +test('[SearchRouter] should react to text input changes', async () => { const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); + await screen.findByTestId('SearchRouter'); - const input = screen.getByTestId('selection-list-text-input'); + const input = screen.getByTestId('search-router-text-input'); fireEvent.changeText(input, 'Email Four'); fireEvent.changeText(input, 'Report'); fireEvent.changeText(input, 'Email Five'); }; - // const navigation = {addListener} as unknown as StackNavigationProp; - return waitForBatchedUpdates() .then(() => Onyx.multiSet({ @@ -205,13 +182,5 @@ test('[ChatFinderPage] should interact when text input changes', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => - measureRenders( - , - {scenario}, - ), - ); + .then(() => measureRenders(, {scenario})); }); From 078420e53f8500f1e75fef3cfde10d64f2e7e8d5 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Fri, 4 Oct 2024 12:15:39 +0200 Subject: [PATCH 20/55] Remove SearchRouter dev check --- src/components/Search/SearchRouter/SearchButton.tsx | 5 ----- src/libs/Permissions.ts | 13 ------------- tests/perf-test/SearchRouter.perf-test.tsx | 1 - 3 files changed, 19 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index b1e59a323926..2ddc9a8262c2 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -6,7 +6,6 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Performance from '@libs/Performance'; -import Permissions from '@libs/Permissions'; import * as Session from '@userActions/Session'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; @@ -18,10 +17,6 @@ function SearchButton() { const {translate} = useLocalize(); const {openSearchRouter} = useSearchRouterContext(); - if (!Permissions.canUseNewSearchRouter()) { - return; - } - return ( ): boolean { return !!betas?.includes(CONST.BETAS.ALL); @@ -50,17 +49,6 @@ function canUseCombinedTrackSubmit(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.COMBINED_TRACK_SUBMIT); } -/** - * New Search Router is under construction and for now should be displayed only in dev to allow developers to work on it. - * We are not using BETA for this feature, as betas are heavier to cleanup, - * and the development of new router is expected to take 2-3 weeks at most - * - * After everything is implemented this function can be removed, as we will always use SearchRouter in the App. - */ -function canUseNewSearchRouter() { - return Environment.isDevelopment(); -} - /** * Link previews are temporarily disabled. */ @@ -80,5 +68,4 @@ export default { canUseNewDotCopilot, canUseWorkspaceRules, canUseCombinedTrackSubmit, - canUseNewSearchRouter, }; diff --git a/tests/perf-test/SearchRouter.perf-test.tsx b/tests/perf-test/SearchRouter.perf-test.tsx index 5be0c93105c3..e9154a36a9a1 100644 --- a/tests/perf-test/SearchRouter.perf-test.tsx +++ b/tests/perf-test/SearchRouter.perf-test.tsx @@ -18,7 +18,6 @@ import type {PersonalDetails, Report} from '@src/types/onyx'; import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomReport from '../utils/collections/reports'; -import createAddListenerMock from '../utils/createAddListenerMock'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; From e21ed95a38c101515a3dbc8a10f7307375b6e22b Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Fri, 4 Oct 2024 17:37:12 +0200 Subject: [PATCH 21/55] Fix SearchRouter opening from keyboard shortcut --- .../SearchRouter/SearchRouterContext.tsx | 25 ++++++++++++++++--- .../Navigation/AppNavigator/AuthScreens.tsx | 12 +++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index d935fff110a4..4e01071a0916 100644 --- a/src/components/Search/SearchRouter/SearchRouterContext.tsx +++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx @@ -1,10 +1,11 @@ -import React, {useContext, useMemo, useState} from 'react'; +import React, {useContext, useMemo, useRef, useState} from 'react'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; const defaultSearchContext = { isSearchRouterDisplayed: false, openSearchRouter: () => {}, closeSearchRouter: () => {}, + toggleSearchRouter: () => {}, }; type SearchRouterContext = typeof defaultSearchContext; @@ -13,15 +14,33 @@ const Context = React.createContext(defaultSearchContext); function SearchRouterContextProvider({children}: ChildrenProps) { const [isSearchRouterDisplayed, setIsSearchRouterDisplayed] = useState(false); + const searchRouterDisplayedRef = useRef(false); const routerContext = useMemo(() => { - const openSearchRouter = () => setIsSearchRouterDisplayed(true); - const closeSearchRouter = () => setIsSearchRouterDisplayed(false); + const openSearchRouter = () => { + setIsSearchRouterDisplayed(true); + searchRouterDisplayedRef.current = true; + }; + const closeSearchRouter = () => { + setIsSearchRouterDisplayed(false); + searchRouterDisplayedRef.current = false; + }; + + // There are callbacks that live outside of React render-loop and interact with SearchRouter + // So we need a function that is based on ref to correctly open/close it + const toggleSearchRouter = () => { + if (searchRouterDisplayedRef.current) { + closeSearchRouter(); + } else { + openSearchRouter(); + } + }; return { isSearchRouterDisplayed, openSearchRouter, closeSearchRouter, + toggleSearchRouter, }; }, [isSearchRouterDisplayed, setIsSearchRouterDisplayed]); diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 2ec31d8a8688..b9ea16a344f2 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -234,7 +234,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const screenOptions = getRootNavigatorScreenOptions(shouldUseNarrowLayout, styles, StyleUtils); const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); - const {openSearchRouter} = useSearchRouterContext(); + const {toggleSearchRouter} = useSearchRouterContext(); const onboardingModalScreenOptions = useMemo(() => screenOptions.onboardingModalNavigator(onboardingIsMediumOrLargerScreenWidth), [screenOptions, onboardingIsMediumOrLargerScreenWidth]); const onboardingScreenOptions = useMemo( @@ -372,13 +372,9 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const unsubscribeSearchShortcut = KeyboardShortcut.subscribe( searchShortcutConfig.shortcutKey, () => { - Modal.close( - Session.checkIfActionIsAllowed(() => { - openSearchRouter(); - }), - true, - true, - ); + Session.checkIfActionIsAllowed(() => { + toggleSearchRouter(); + })(); }, shortcutsOverviewShortcutConfig.descriptionKey, shortcutsOverviewShortcutConfig.modifiers, From 4b81669e070a276265442998aaba3890603e66b1 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 7 Oct 2024 11:47:54 +0200 Subject: [PATCH 22/55] close modals on openSearchRouter --- .../Search/SearchRouter/SearchRouterContext.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index 4e01071a0916..2e4cbec0d6bb 100644 --- a/src/components/Search/SearchRouter/SearchRouterContext.tsx +++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx @@ -1,4 +1,5 @@ import React, {useContext, useMemo, useRef, useState} from 'react'; +import * as Modal from '@userActions/Modal'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; const defaultSearchContext = { @@ -18,8 +19,14 @@ function SearchRouterContextProvider({children}: ChildrenProps) { const routerContext = useMemo(() => { const openSearchRouter = () => { - setIsSearchRouterDisplayed(true); - searchRouterDisplayedRef.current = true; + Modal.close( + () => { + setIsSearchRouterDisplayed(true); + searchRouterDisplayedRef.current = true; + }, + false, + true, + ); }; const closeSearchRouter = () => { setIsSearchRouterDisplayed(false); From 47ba23f6e5dd5b744135ed46dbe76fac100df4a7 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 8 Oct 2024 11:05:11 +0200 Subject: [PATCH 23/55] fix design comments --- src/components/Search/SearchRouter/SearchRouterList.tsx | 1 + src/components/Search/SearchRouter/SearchRouterModal.tsx | 2 +- src/styles/index.ts | 4 ++-- src/styles/utils/spacing.ts | 4 ++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index a58ea969d28f..70abc5c89550 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -185,6 +185,7 @@ function SearchRouterList( onLayout={setPerformanceTimersEnd} ref={ref} showScrollIndicator={!isSmallScreenWidth} + sectionTitleStyles={styles.mhn2} /> ); } diff --git a/src/components/Search/SearchRouter/SearchRouterModal.tsx b/src/components/Search/SearchRouter/SearchRouterModal.tsx index c44b66044ab3..7e403461dd34 100644 --- a/src/components/Search/SearchRouter/SearchRouterModal.tsx +++ b/src/components/Search/SearchRouter/SearchRouterModal.tsx @@ -17,7 +17,7 @@ function SearchRouterModal() { type={modalType} fullscreen isVisible={isSearchRouterDisplayed} - popoverAnchorPosition={{right: 20, top: 20}} + popoverAnchorPosition={{right: 6, top: 6}} onClose={closeSearchRouter} > {isSearchRouterDisplayed && } diff --git a/src/styles/index.ts b/src/styles/index.ts index 8220389867d9..b81d76c8d7d9 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3615,8 +3615,8 @@ const styles = (theme: ThemeColors) => searchInputStyle: { color: theme.textSupporting, - fontSize: 13, - lineHeight: 16, + fontSize: variables.fontSizeNormal, + lineHeight: variables.fontSizeNormalHeight, }, searchRouterTextInputContainer: { diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts index c2008d8a68f0..9f741c9d925b 100644 --- a/src/styles/utils/spacing.ts +++ b/src/styles/utils/spacing.ts @@ -55,6 +55,10 @@ export default { marginHorizontal: 32, }, + mhn2: { + marginHorizontal: -8, + }, + mhn5: { marginHorizontal: -20, }, From 9136e8a0b1f765feaae603135be1a417534e16e5 Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Tue, 8 Oct 2024 20:17:03 +0800 Subject: [PATCH 24/55] set the selected rates to the transaction currency and make prompt text agnostic --- src/languages/en.ts | 3 +-- src/languages/es.ts | 3 +-- src/languages/params.ts | 3 --- .../iou/request/step/IOURequestStepDistanceRate.tsx | 10 +++++++--- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 18dcbe35c9f6..b0fbf03298f0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -109,7 +109,6 @@ import type { PayerSettledParams, PaySomeoneParams, ReconciliationWorksParams, - ReimbursementRateParams, RemovedFromApprovalWorkflowParams, RemovedTheRequestParams, RemoveMemberPromptParams, @@ -1009,7 +1008,7 @@ const translations = { changed: 'changed', removed: 'removed', transactionPending: 'Transaction pending.', - chooseARate: ({unit}: ReimbursementRateParams) => `Select a workspace reimbursement rate per ${unit}`, + chooseARate: `Select a workspace reimbursement rate per mile or kilometer`, unapprove: 'Unapprove', unapproveReport: 'Unapprove report', headsUp: 'Heads up!', diff --git a/src/languages/es.ts b/src/languages/es.ts index 8e16c03a91d3..aad4b3967866 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -107,7 +107,6 @@ import type { PayerSettledParams, PaySomeoneParams, ReconciliationWorksParams, - ReimbursementRateParams, RemovedFromApprovalWorkflowParams, RemovedTheRequestParams, RemoveMemberPromptParams, @@ -1003,7 +1002,7 @@ const translations = { changed: 'cambió', removed: 'eliminó', transactionPending: 'Transacción pendiente.', - chooseARate: ({unit}: ReimbursementRateParams) => `Selecciona una tasa de reembolso por ${unit} del espacio de trabajo`, + chooseARate: `Selecciona una tasa de reembolso por milla o kilómetro para el espacio de trabajo`, unapprove: 'Desaprobar', unapproveReport: 'Anular la aprobación del informe', headsUp: 'Atención!', diff --git a/src/languages/params.ts b/src/languages/params.ts index 5560316d2ddd..893b02e97591 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -279,8 +279,6 @@ type LogSizeAndDateParams = {size: number; date: string}; type HeldRequestParams = {comment: string}; -type ReimbursementRateParams = {unit: Unit}; - type ChangeFieldParams = {oldValue?: string; newValue: string; fieldName: string}; type ChangePolicyParams = {fromPolicy: string; toPolicy: string}; @@ -648,7 +646,6 @@ export type { PayerPaidAmountParams, PayerPaidParams, PayerSettledParams, - ReimbursementRateParams, RemovedTheRequestParams, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 59e1591a23ff..096651089cab 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -64,6 +64,8 @@ function IOURequestStepDistanceRate({ const currentRateID = TransactionUtils.getRateID(transaction) ?? '-1'; + const transactionCurrency = TransactionUtils.getCurrency(transaction); + const rates = DistanceRequestUtils.getMileageRates(policy, false, currentRateID); const navigateBack = () => { @@ -71,7 +73,9 @@ function IOURequestStepDistanceRate({ }; const sections = Object.values(rates).map((rate) => { - const rateForDisplay = DistanceRequestUtils.getRateForDisplay(rate.unit, rate.rate, rate.currency, translate, toLocaleDigit); + const isSelected = currentRateID ? currentRateID === rate.customUnitRateID : rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE; + + const rateForDisplay = DistanceRequestUtils.getRateForDisplay(rate.unit, rate.rate, isSelected ? transactionCurrency : rate.currency, translate, toLocaleDigit); return { text: rate.name ?? rateForDisplay, @@ -79,7 +83,7 @@ function IOURequestStepDistanceRate({ keyForList: rate.customUnitRateID, value: rate.customUnitRateID, isDisabled: !rate.enabled, - isSelected: currentRateID ? currentRateID === rate.customUnitRateID : rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE, + isSelected, }; }); @@ -118,7 +122,7 @@ function IOURequestStepDistanceRate({ shouldShowWrapper testID={IOURequestStepDistanceRate.displayName} > - {translate('iou.chooseARate', {unit})} + {translate('iou.chooseARate')} Date: Tue, 8 Oct 2024 20:19:49 +0800 Subject: [PATCH 25/55] lint fix --- src/languages/params.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/params.ts b/src/languages/params.ts index 893b02e97591..e4a52f5150fd 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -1,6 +1,6 @@ import type {OnyxInputOrEntry, ReportAction} from '@src/types/onyx'; import type {DelegateRole} from '@src/types/onyx/Account'; -import type {AllConnectionName, ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName, Unit} from '@src/types/onyx/Policy'; +import type {AllConnectionName, ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName} from '@src/types/onyx/Policy'; import type {ViolationDataType} from '@src/types/onyx/TransactionViolation'; type AddressLineParams = { From 5a7ada0c7df68a7c9370e03ce1a30398e1075e1b Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Tue, 8 Oct 2024 21:14:38 +0800 Subject: [PATCH 26/55] migrate to useOnyx --- .../step/IOURequestStepDistanceRate.tsx | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 096651089cab..b2d633a9b880 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; @@ -23,35 +23,23 @@ import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -type IOURequestStepDistanceRateOnyxProps = { - /** Policy details */ - policy: OnyxEntry; - - /** Collection of categories attached to the policy */ - policyCategories: OnyxEntry; - - /** Collection of tags attached to the policy */ - policyTags: OnyxEntry; +type IOURequestStepDistanceRateProps = WithWritableReportOrNotFoundProps & { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + transaction: OnyxEntry; }; -type IOURequestStepDistanceRateProps = IOURequestStepDistanceRateOnyxProps & - WithWritableReportOrNotFoundProps & { - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - transaction: OnyxEntry; - }; - function IOURequestStepDistanceRate({ - policy: policyReal, report, reportDraft, route: { params: {action, reportID, backTo, transactionID}, }, transaction, - policyTags, - policyCategories, }: IOURequestStepDistanceRateProps) { const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft) ?? '-1'}`); + const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '-1'}`); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`); const policy = policyReal ?? policyDraft; @@ -137,20 +125,8 @@ function IOURequestStepDistanceRate({ IOURequestStepDistanceRate.displayName = 'IOURequestStepDistanceRate'; -const IOURequestStepDistanceRateWithOnyx = withOnyx({ - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '-1'}`, - }, - policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, - }, - policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, - }, -})(IOURequestStepDistanceRate); - // eslint-disable-next-line rulesdir/no-negated-variables -const IOURequestStepDistanceRateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepDistanceRateWithOnyx); +const IOURequestStepDistanceRateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepDistanceRate); // eslint-disable-next-line rulesdir/no-negated-variables const IOURequestStepDistanceRateWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepDistanceRateWithWritableReportOrNotFound); From 36a4e27d7637d710b24a53aa1992c8ec606d954d Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Tue, 8 Oct 2024 21:27:01 +0800 Subject: [PATCH 27/55] minor fix --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b0fbf03298f0..f4faade51600 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1008,7 +1008,7 @@ const translations = { changed: 'changed', removed: 'removed', transactionPending: 'Transaction pending.', - chooseARate: `Select a workspace reimbursement rate per mile or kilometer`, + chooseARate: 'Select a workspace reimbursement rate per mile or kilometer', unapprove: 'Unapprove', unapproveReport: 'Unapprove report', headsUp: 'Heads up!', diff --git a/src/languages/es.ts b/src/languages/es.ts index aad4b3967866..99d240c2372c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1002,7 +1002,7 @@ const translations = { changed: 'cambió', removed: 'eliminó', transactionPending: 'Transacción pendiente.', - chooseARate: `Selecciona una tasa de reembolso por milla o kilómetro para el espacio de trabajo`, + chooseARate: 'Selecciona una tasa de reembolso por milla o kilómetro para el espacio de trabajo', unapprove: 'Desaprobar', unapproveReport: 'Anular la aprobación del informe', headsUp: 'Atención!', From c0542d589a974e741d89b1484b0598d21eb30ab5 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 8 Oct 2024 20:03:34 +0100 Subject: [PATCH 28/55] fix(debug mode): wrong reason for why RBR reports are visible in LHN --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/DebugUtils.ts | 9 ++- src/libs/OptionsListUtils.ts | 119 +--------------------------- src/libs/ReportUtils.ts | 115 ++++++++++++++++++++++++++- src/libs/SidebarUtils.ts | 20 +---- src/libs/WorkspacesSettingsUtils.ts | 2 +- 7 files changed, 129 insertions(+), 138 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index c44ac6f2cedf..eb68cf9b23c2 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4931,6 +4931,7 @@ const translations = { reasonVisibleInLHN: { hasDraftComment: 'Has draft comment', hasGBR: 'Has GBR', + hasRBR: 'Has RBR', pinnedByUser: 'Pinned by user', hasIOUViolations: 'Has IOU violations', hasAddWorkspaceRoomErrors: 'Has add workspace room errors', diff --git a/src/languages/es.ts b/src/languages/es.ts index 76cb22772f88..7aec0d6eef50 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5443,6 +5443,7 @@ const translations = { reasonVisibleInLHN: { hasDraftComment: 'Tiene comentario en borrador', hasGBR: 'Tiene GBR', + hasRBR: 'Tiene RBR', pinnedByUser: 'Fijado por el usuario', hasIOUViolations: 'Tiene violaciones de IOU', hasAddWorkspaceRoomErrors: 'Tiene errores al agregar sala de espacio de trabajo', diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index ed8e486517ff..af682e9cdc6a 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -7,7 +7,6 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, Policy, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; -import * as OptionsListUtils from './OptionsListUtils'; import * as ReportUtils from './ReportUtils'; class NumberError extends SyntaxError { @@ -598,7 +597,11 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath return null; } - const doesReportHaveViolations = OptionsListUtils.shouldShowViolations(report, transactionViolations); + const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations); + + if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { + return `debug.reasonVisibleInLHN.hasRBR`; + } const reason = ReportUtils.reasonForReportToBeInOptionList({ report, @@ -646,7 +649,7 @@ function getReasonAndReportActionForGBRInLHNRow(report: OnyxEntry): GBRR * Gets the report action that is causing the RBR to show up in LHN */ function getRBRReportAction(report: OnyxEntry, reportActions: OnyxEntry): OnyxEntry { - const {reportAction} = OptionsListUtils.getAllReportActionsErrorsAndReportActionThatRequiresAttention(report, reportActions); + const {reportAction} = ReportUtils.getAllReportActionsErrorsAndReportActionThatRequiresAttention(report, reportActions); return reportAction; } diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index a004974b88e4..2bb52b461f55 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -41,7 +41,6 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; import Timing from './actions/Timing'; -import * as ErrorUtils from './ErrorUtils'; import filterArrayByMatch from './filterArrayByMatch'; import localeCompare from './LocaleCompare'; import * as LocalePhoneNumber from './LocalePhoneNumber'; @@ -342,26 +341,6 @@ Onyx.connect({ }, }); -let allTransactions: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - return; - } - - allTransactions = Object.keys(value) - .filter((key) => !!value[key]) - .reduce((result: OnyxCollection, key) => { - if (result) { - // eslint-disable-next-line no-param-reassign - result[key] = value[key]; - } - return result; - }, {}); - }, -}); let activePolicyID: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, @@ -480,78 +459,6 @@ function uniqFast(items: string[]): string[] { return result; } -type ReportErrorsAndReportActionThatRequiresAttention = { - errors: OnyxCommon.ErrorFields; - reportAction?: OnyxEntry; -}; - -function getAllReportActionsErrorsAndReportActionThatRequiresAttention(report: OnyxEntry, reportActions: OnyxEntry): ReportErrorsAndReportActionThatRequiresAttention { - const reportActionsArray = Object.values(reportActions ?? {}); - const reportActionErrors: OnyxCommon.ErrorFields = {}; - let reportAction: OnyxEntry; - - for (const action of reportActionsArray) { - if (action && !isEmptyObject(action.errors)) { - Object.assign(reportActionErrors, action.errors); - - if (!reportAction) { - reportAction = action; - } - } - } - const parentReportAction: OnyxEntry = - !report?.parentReportID || !report?.parentReportActionID ? undefined : allReportActions?.[report.parentReportID ?? '-1']?.[report.parentReportActionID ?? '-1']; - - if (ReportActionUtils.wasActionTakenByCurrentUser(parentReportAction) && ReportActionUtils.isTransactionThread(parentReportAction)) { - const transactionID = ReportActionUtils.isMoneyRequestAction(parentReportAction) ? ReportActionUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID : null; - const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) { - reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); - reportAction = undefined; - } - } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { - if (ReportUtils.shouldShowRBRForMissingSmartscanFields(report?.reportID ?? '-1') && !ReportUtils.isSettled(report?.reportID)) { - reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); - reportAction = ReportUtils.getReportActionWithMissingSmartscanFields(report?.reportID ?? '-1'); - } - } else if (ReportUtils.hasSmartscanError(reportActionsArray)) { - reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); - reportAction = ReportUtils.getReportActionWithSmartscanError(reportActionsArray); - } - - return { - errors: reportActionErrors, - reportAction, - }; -} - -/** - * Get an object of error messages keyed by microtime by combining all error objects related to the report. - */ -function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors { - const reportErrors = report?.errors ?? {}; - const reportErrorFields = report?.errorFields ?? {}; - const {errors: reportActionErrors} = getAllReportActionsErrorsAndReportActionThatRequiresAttention(report, reportActions); - - // All error objects related to the report. Each object in the sources contains error messages keyed by microtime - const errorSources = { - reportErrors, - ...reportErrorFields, - ...reportActionErrors, - }; - - // Combine all error messages keyed by microtime into one object - const errorSourcesArray = Object.values(errorSources ?? {}); - const allReportErrors = {}; - - for (const errors of errorSourcesArray) { - if (!isEmptyObject(errors)) { - Object.assign(allReportErrors, errors); - } - } - return allReportErrors; -} - /** * Get the last actor display name from last actor details. */ @@ -745,7 +652,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails } function hasReportErrors(report: Report, reportActions: OnyxEntry) { - return !isEmptyObject(getAllReportErrors(report, reportActions)); + return !isEmptyObject(ReportUtils.getAllReportErrors(report, reportActions)); } /** @@ -813,7 +720,7 @@ function createOption( result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); result.isOwnPolicyExpenseChat = report.isOwnPolicyExpenseChat ?? false; - result.allReportErrors = getAllReportErrors(report, reportActions); + result.allReportErrors = ReportUtils.getAllReportErrors(report, reportActions); result.brickRoadIndicator = hasReportErrors(report, reportActions) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom ?? report.pendingFields.createChat : undefined; result.ownerAccountID = report.ownerAccountID; @@ -1767,23 +1674,6 @@ function getUserToInviteOption({ return userToInvite; } -/** - * Check whether report has violations - */ -function shouldShowViolations(report: Report, transactionViolations: OnyxCollection) { - const {parentReportID, parentReportActionID} = report ?? {}; - const canGetParentReport = parentReportID && parentReportActionID && allReportActions; - if (!canGetParentReport) { - return false; - } - const parentReportActions = allReportActions ? allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? {} : {}; - const parentReportAction = parentReportActions[parentReportActionID] ?? null; - if (!parentReportAction) { - return false; - } - return ReportUtils.shouldDisplayTransactionThreadViolations(report, transactionViolations, parentReportAction); -} - /** * filter options based on specific conditions */ @@ -1894,7 +1784,7 @@ function getOptions( // Filter out all the reports that shouldn't be displayed const filteredReportOptions = options.reports.filter((option) => { const report = option.item; - const doesReportHaveViolations = shouldShowViolations(report, transactionViolations); + const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations); return ReportUtils.shouldReportBeInOptionList({ report, @@ -2625,7 +2515,6 @@ export { getPersonalDetailsForAccountIDs, getIOUConfirmationOptionsFromPayeePersonalDetail, isSearchStringMatchUserDetails, - getAllReportErrors, getPolicyExpenseReportOption, getIOUReportIDOfLastAction, getParticipantsOption, @@ -2651,13 +2540,11 @@ export { getFirstKeyForList, canCreateOptimisticPersonalDetailOption, getUserToInviteOption, - shouldShowViolations, getPersonalDetailSearchTerms, getCurrentUserSearchTerms, getEmptyOptions, shouldUseBoldText, getAlternateText, - getAllReportActionsErrorsAndReportActionThatRequiresAttention, hasReportErrors, }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4a8d48d4d81a..8bf7984e3e34 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -48,7 +48,7 @@ import type {Participant} from '@src/types/onyx/IOU'; import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft'; import type {OriginalMessageExportedToIntegration} from '@src/types/onyx/OldDotAction'; import type Onboarding from '@src/types/onyx/Onboarding'; -import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {ErrorFields, Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {OriginalMessageChangeLog, PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type {ConnectionName} from '@src/types/onyx/Policy'; @@ -64,6 +64,7 @@ import * as SessionUtils from './actions/Session'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; +import * as ErrorUtils from './ErrorUtils'; import getAttachmentDetails from './fileDownload/getAttachmentDetails'; import getIsSmallScreenWidth from './getIsSmallScreenWidth'; import isReportMessageAttachment from './isReportMessageAttachment'; @@ -1375,7 +1376,7 @@ function findLastAccessedReport(ignoreDomainRooms: boolean, openOnAdminRoom = fa } // We allow public announce rooms, admins, and announce rooms through since we bypass the default rooms beta for them. - // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. + // Check where findLastAccessedReport is called in MainDrawerNavigator.js for more context. // Domain rooms are now the only type of default room that are on the defaultRooms beta. if (ignoreDomainRooms && isDomainRoom(report) && !hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number))) { return false; @@ -6217,6 +6218,112 @@ function shouldAdminsRoomBeVisible(report: OnyxEntry): boolean { return true; } +/** + * Check whether report has violations + */ +function shouldShowViolations(report: Report, transactionViolations: OnyxCollection) { + const {parentReportID, parentReportActionID} = report ?? {}; + const canGetParentReport = parentReportID && parentReportActionID && allReportActions; + if (!canGetParentReport) { + return false; + } + const parentReportActions = allReportActions ? allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? {} : {}; + const parentReportAction = parentReportActions[parentReportActionID] ?? null; + if (!parentReportAction) { + return false; + } + return shouldDisplayTransactionThreadViolations(report, transactionViolations, parentReportAction); +} + +type ReportErrorsAndReportActionThatRequiresAttention = { + errors: ErrorFields; + reportAction?: OnyxEntry; +}; + +function getAllReportActionsErrorsAndReportActionThatRequiresAttention(report: OnyxEntry, reportActions: OnyxEntry): ReportErrorsAndReportActionThatRequiresAttention { + const reportActionsArray = Object.values(reportActions ?? {}); + const reportActionErrors: ErrorFields = {}; + let reportAction: OnyxEntry; + + for (const action of reportActionsArray) { + if (action && !isEmptyObject(action.errors)) { + Object.assign(reportActionErrors, action.errors); + + if (!reportAction) { + reportAction = action; + } + } + } + const parentReportAction: OnyxEntry = + !report?.parentReportID || !report?.parentReportActionID ? undefined : allReportActions?.[report.parentReportID ?? '-1']?.[report.parentReportActionID ?? '-1']; + + if (ReportActionsUtils.wasActionTakenByCurrentUser(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction)) { + const transactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID : null; + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !isSettled(transaction?.reportID)) { + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); + reportAction = undefined; + } + } else if ((isIOUReport(report) || isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { + if (shouldShowRBRForMissingSmartscanFields(report?.reportID ?? '-1') && !isSettled(report?.reportID)) { + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); + reportAction = getReportActionWithMissingSmartscanFields(report?.reportID ?? '-1'); + } + } else if (hasSmartscanError(reportActionsArray)) { + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); + reportAction = getReportActionWithSmartscanError(reportActionsArray); + } + + return { + errors: reportActionErrors, + reportAction, + }; +} + +/** + * Get an object of error messages keyed by microtime by combining all error objects related to the report. + */ +function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): Errors { + const reportErrors = report?.errors ?? {}; + const reportErrorFields = report?.errorFields ?? {}; + const {errors: reportActionErrors} = getAllReportActionsErrorsAndReportActionThatRequiresAttention(report, reportActions); + + // All error objects related to the report. Each object in the sources contains error messages keyed by microtime + const errorSources = { + reportErrors, + ...reportErrorFields, + ...reportActionErrors, + }; + + // Combine all error messages keyed by microtime into one object + const errorSourcesArray = Object.values(errorSources ?? {}); + const allReportErrors = {}; + + for (const errors of errorSourcesArray) { + if (!isEmptyObject(errors)) { + Object.assign(allReportErrors, errors); + } + } + return allReportErrors; +} + +function hasReportErrorsOtherThanFailedReceipt(report: Report, doesReportHaveViolations: boolean, transactionViolations: OnyxCollection) { + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`] ?? {}; + const allReportErrors = getAllReportErrors(report, reportActions) ?? {}; + const transactionReportActions = ReportActionsUtils.getAllReportActions(report.reportID); + const oneTransactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, transactionReportActions, undefined); + let doesTransactionThreadReportHasViolations = false; + if (oneTransactionThreadReportID) { + const transactionReport = getReport(oneTransactionThreadReportID); + doesTransactionThreadReportHasViolations = !!transactionReport && shouldShowViolations(transactionReport, transactionViolations); + } + return ( + doesTransactionThreadReportHasViolations || + doesReportHaveViolations || + Object.values(allReportErrors).some((error) => error?.[0] !== Localize.translateLocal('iou.error.genericSmartscanFailureMessage')) + ); +} + type ShouldReportBeInOptionListParams = { report: OnyxEntry; currentReportId: string; @@ -8428,6 +8535,10 @@ export { hasMissingInvoiceBankAccount, reasonForReportToBeInOptionList, getReasonAndReportActionThatRequiresAttention, + hasReportErrorsOtherThanFailedReceipt, + shouldShowViolations, + getAllReportErrors, + getAllReportActionsErrorsAndReportActionThatRequiresAttention, }; export type { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 99811645a6ea..dd62b40a2493 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -105,23 +105,11 @@ function getOrderedReportIDs( if ((Object.values(CONST.REPORT.UNSUPPORTED_TYPE) as string[]).includes(report?.type ?? '')) { return; } - const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`] ?? {}; const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); - const doesReportHaveViolations = OptionsListUtils.shouldShowViolations(report, transactionViolations); + const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations); const isHidden = ReportUtils.getReportNotificationPreference(report) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; const isFocused = report.reportID === currentReportId; - const allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) ?? {}; - const transactionReportActions = ReportActionsUtils.getAllReportActions(report.reportID); - const oneTransactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, transactionReportActions, undefined); - let doesTransactionThreadReportHasViolations = false; - if (oneTransactionThreadReportID) { - const transactionReport = ReportUtils.getReport(oneTransactionThreadReportID); - doesTransactionThreadReportHasViolations = !!transactionReport && OptionsListUtils.shouldShowViolations(transactionReport, transactionViolations); - } - const hasErrorsOtherThanFailedReceipt = - doesTransactionThreadReportHasViolations || - doesReportHaveViolations || - Object.values(allReportErrors).some((error) => error?.[0] !== Localize.translateLocal('iou.error.genericSmartscanFailureMessage')); + const hasErrorsOtherThanFailedReceipt = ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations); const isReportInAccessible = report?.errorFields?.notFound; if (ReportUtils.isOneTransactionThread(report.reportID, report.parentReportID ?? '0', parentReportAction)) { return; @@ -235,7 +223,7 @@ function getOrderedReportIDs( } function shouldShowRedBrickRoad(report: Report, reportActions: OnyxEntry, hasViolations: boolean, transactionViolations?: OnyxCollection) { - const hasErrors = Object.keys(OptionsListUtils.getAllReportErrors(report, reportActions)).length !== 0; + const hasErrors = Object.keys(ReportUtils.getAllReportErrors(report, reportActions)).length !== 0; const oneTransactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, ReportActionsUtils.getAllReportActions(report.reportID)); if (oneTransactionThreadReportID) { @@ -291,7 +279,7 @@ function getOptionData({ const result: ReportUtils.OptionData = { text: '', alternateText: undefined, - allReportErrors: OptionsListUtils.getAllReportErrors(report, reportActions), + allReportErrors: ReportUtils.getAllReportErrors(report, reportActions), brickRoadIndicator: null, tooltipText: null, subtitle: undefined, diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts index d8cd2ff00828..b8365d9e89d6 100644 --- a/src/libs/WorkspacesSettingsUtils.ts +++ b/src/libs/WorkspacesSettingsUtils.ts @@ -60,7 +60,7 @@ Onyx.connect({ */ const getBrickRoadForPolicy = (report: Report, altReportActions?: OnyxCollection): BrickRoad => { const reportActions = (altReportActions ?? allReportActions)?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`] ?? {}; - const reportErrors = OptionsListUtils.getAllReportErrors(report, reportActions); + const reportErrors = ReportUtils.getAllReportErrors(report, reportActions); const oneTransactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions); let doesReportContainErrors = Object.keys(reportErrors ?? {}).length !== 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined; From 39bdf3309c65663506eedcab50cc50b58c7b81e2 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 8 Oct 2024 20:53:29 +0100 Subject: [PATCH 29/55] chore: fix test cases failing for DebugUtils --- src/libs/DebugUtils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index af682e9cdc6a..6e95dca7114a 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -599,10 +599,6 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations); - if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { - return `debug.reasonVisibleInLHN.hasRBR`; - } - const reason = ReportUtils.reasonForReportToBeInOptionList({ report, // We can't pass report.reportID because it will cause reason to always be isFocused @@ -615,8 +611,12 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath includeSelfDM: true, }); - // When there's no specific reason, we default to isFocused since the report is only showing because we're viewing it + // When there's no specific reason, we default to isFocused if the report is only showing because we're viewing it + // Otherwise we return hasRBR if the report has errors other that failed receipt if (reason === null || reason === CONST.REPORT_IN_LHN_REASONS.DEFAULT) { + if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { + return `debug.reasonVisibleInLHN.hasRBR`; + } return 'debug.reasonVisibleInLHN.isFocused'; } From f0dc3c16dfbee852a5eb5d026ece715568b3ba02 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 8 Oct 2024 20:53:43 +0100 Subject: [PATCH 30/55] chore: remove unused dependency --- src/libs/WorkspacesSettingsUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts index b8365d9e89d6..2be641035be7 100644 --- a/src/libs/WorkspacesSettingsUtils.ts +++ b/src/libs/WorkspacesSettingsUtils.ts @@ -9,7 +9,6 @@ import type {Policy, ReimbursementAccount, Report, ReportAction, ReportActions, import type {PolicyConnectionSyncProgress, Unit} from '@src/types/onyx/Policy'; import {isConnectionInProgress} from './actions/connections'; import * as CurrencyUtils from './CurrencyUtils'; -import * as OptionsListUtils from './OptionsListUtils'; import {hasCustomUnitsError, hasEmployeeListError, hasPolicyError, hasSyncError, hasTaxRateError} from './PolicyUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import * as ReportConnection from './ReportConnection'; From 13807c5f0132562e6569284c0b41261ba3ea8558 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 10 Oct 2024 09:51:59 +0100 Subject: [PATCH 31/55] fix(debug mode): hasGBR showing instead of hasRBR in debug details page --- src/libs/DebugUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 3b06c8b98540..db84c2b473a7 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -610,12 +610,13 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath includeSelfDM: true, }); + if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { + return `debug.reasonVisibleInLHN.hasRBR`; + } + // When there's no specific reason, we default to isFocused if the report is only showing because we're viewing it // Otherwise we return hasRBR if the report has errors other that failed receipt if (reason === null || reason === CONST.REPORT_IN_LHN_REASONS.DEFAULT) { - if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { - return `debug.reasonVisibleInLHN.hasRBR`; - } return 'debug.reasonVisibleInLHN.isFocused'; } From 9358babd497b9c7f7b857bb32c6adc87660122fd Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 10 Oct 2024 10:18:17 +0100 Subject: [PATCH 32/55] fix(debug mode): not showing specific RBR reasons for report to be in LHN --- src/libs/DebugUtils.ts | 5 ++++- tests/unit/DebugUtilsTest.ts | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index db84c2b473a7..3b6c1c77713f 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -610,7 +610,10 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath includeSelfDM: true, }); - if (ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations)) { + if ( + !([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && + ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations) + ) { return `debug.reasonVisibleInLHN.hasRBR`; } diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index 34c2ad2bde73..7ac77fa1b448 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -963,8 +963,7 @@ describe('DebugUtils', () => { ); expect(reportAction).toBeUndefined(); }); - // TODO: remove '.failing' once the implementation is fixed - it.failing('returns parentReportAction if it is a transaction thread, the transaction is missing smart scan fields and the report is not settled', async () => { + it('returns undefined if it is a transaction thread, the transaction is missing smart scan fields and the report is not settled', async () => { const MOCK_REPORTS: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}1` as const]: { reportID: '1', @@ -1011,7 +1010,7 @@ describe('DebugUtils', () => { MOCK_REPORTS[`${ONYXKEYS.COLLECTION.REPORT}1`] as Report, undefined, ); - expect(reportAction).toBe(1); + expect(reportAction).toBe(undefined); }); describe("Report has missing fields, isn't settled and it's owner is the current user", () => { describe('Report is IOU', () => { From c4f70882f836269bf2fedb6f448e20e87db1315b Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 10 Oct 2024 12:16:03 +0200 Subject: [PATCH 33/55] fix PR comments --- src/components/HeaderWithBackButton/index.tsx | 2 +- src/components/Search/SearchRouter/SearchRouter.tsx | 3 +-- src/components/Search/SearchRouter/SearchRouterList.tsx | 2 +- src/styles/variables.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index eb04ad5540eb..e1843ee506d5 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -191,7 +191,7 @@ function HeaderWithBackButton({ /> )} {middleContent} - + {children} {shouldShowDownloadButton && ( diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index f49112e86c6c..564e5f0feda0 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -150,8 +150,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { - onRouterClose(); - clearUserQuery(); + closeAndClearRouter(); }); const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.popoverWidth}; diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 70abc5c89550..d05588afbe9a 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -167,7 +167,7 @@ function SearchRouterList( // Handle selection of "Recent chat" closeAndClearRouter(); if ('reportID' in item && item?.reportID) { - Navigation.closeAndNavigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID)); } else if ('login' in item) { Report.navigateToAndOpenReport(item?.login ? [item.login] : []); } diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 82d443928c7a..cd81e5fcb9c8 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -166,7 +166,7 @@ export default { signInLogoWidthLargeScreenPill: 162, modalContentMaxWidth: 360, listItemHeightNormal: 64, - popoverWidth: 375, + popoverWidth: 512, bankAccountActionPopoverRightSpacing: 32, bankAccountActionPopoverTopSpacing: 14, addPaymentPopoverRightSpacing: 23, From 7e44690b9ffe42ad43d43e3b4d074efe3518ed81 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 10 Oct 2024 12:32:26 +0200 Subject: [PATCH 34/55] fix paddings --- src/components/Search/SearchRouter/SearchButton.tsx | 9 +++++++-- src/pages/home/HeaderView.tsx | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index 2ddc9a8262c2..ae755fbf2e27 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; @@ -11,7 +12,11 @@ import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import {useSearchRouterContext} from './SearchRouterContext'; -function SearchButton() { +type SearchButtonProps = { + style?: StyleProp; +}; + +function SearchButton({style}: SearchButtonProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); @@ -20,7 +25,7 @@ function SearchButton() { return ( { Timing.start(CONST.TIMING.SEARCH_ROUTER_OPEN); Performance.markStart(CONST.TIMING.SEARCH_ROUTER_OPEN); diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 42483cc3d223..31aaf7cb1f1c 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -280,7 +280,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto {isTaskReport && !shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && } {canJoin && !shouldUseNarrowLayout && joinButton} - + Date: Thu, 10 Oct 2024 15:21:25 +0200 Subject: [PATCH 35/55] fix linter --- .../TopBar.tsx | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx index 4684eb9637be..8967486165f8 100644 --- a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx @@ -2,32 +2,25 @@ import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Breadcrumbs from '@components/Breadcrumbs'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; import SearchButton from '@components/Search/SearchRouter/SearchButton'; import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import Performance from '@libs/Performance'; import * as SearchUtils from '@libs/SearchUtils'; import SignInButton from '@pages/home/sidebar/SignInButton'; import * as Session from '@userActions/Session'; -import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; isCustomSearchQuery?: boolean; shouldDisplaySearchRouter?: boolean}; +type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; shouldDisplayCancelSearch?: boolean}; -function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, isCustomSearchQuery = false, shouldDisplaySearchRouter = false}: TopBarProps) { +function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false}: TopBarProps) { const styles = useThemeStyles(); - const theme = useTheme(); const {translate} = useLocalize(); const policy = usePolicy(activeWorkspaceID); const [session] = useOnyx(ONYXKEYS.SESSION, {selector: (sessionValue) => sessionValue && {authTokenType: sessionValue.authTokenType}}); @@ -63,7 +56,7 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, {displaySignIn && } - {isCustomSearchQuery && ( + {shouldDisplayCancelSearch && ( {translate('common.cancel')} )} - {shouldDisplaySearchRouter && } - {displaySearch && ( - - { - Timing.start(CONST.TIMING.CHAT_FINDER_RENDER); - Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); - Navigation.navigate(ROUTES.CHAT_FINDER); - })} - > - - - - )} + {displaySearch && } ); From 2fd6a047a5cc45353e016c42d5fbf72ce252347f Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 10 Oct 2024 16:05:19 +0100 Subject: [PATCH 36/55] chore: add test cases for hasRBR --- tests/unit/DebugUtilsTest.ts | 125 +++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index 7ac77fa1b448..a87648d08eec 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -783,6 +783,131 @@ describe('DebugUtils', () => { const reason = DebugUtils.getReasonForShowingRowInLHN(baseReport); expect(reason).toBe('debug.reasonVisibleInLHN.isFocused'); }); + it('returns correct reason when report has one transaction thread with violations', async () => { + const MOCK_TRANSACTION_REPORT: Report = { + reportID: '1', + ownerAccountID: 12345, + type: CONST.REPORT.TYPE.EXPENSE, + }; + const MOCK_REPORTS: ReportCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT}1` as const]: MOCK_TRANSACTION_REPORT, + [`${ONYXKEYS.COLLECTION.REPORT}2` as const]: { + reportID: '2', + type: CONST.REPORT.TYPE.CHAT, + parentReportID: '1', + parentReportActionID: '1', + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + }, + }; + const MOCK_REPORT_ACTIONS: ReportActionsCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1` as const]: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '1': { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorAccountID: 12345, + created: '2024-08-08 18:20:44.171', + childReportID: '2', + message: { + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount: 10, + currency: CONST.CURRENCY.USD, + IOUReportID: '1', + text: 'Vacation expense', + IOUTransactionID: '1', + }, + }, + }, + }; + await Onyx.multiSet({ + ...MOCK_REPORTS, + ...MOCK_REPORT_ACTIONS, + [ONYXKEYS.SESSION]: { + accountID: 12345, + }, + [`${ONYXKEYS.COLLECTION.TRANSACTION}1` as const]: { + transactionID: '1', + amount: 10, + modifiedAmount: 10, + reportID: '1', + }, + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}1` as const]: [ + { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.MISSING_CATEGORY, + }, + ], + }); + const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_TRANSACTION_REPORT); + expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); + }); + it('returns correct reason when report has violations', async () => { + const MOCK_EXPENSE_REPORT: Report = { + reportID: '1', + chatReportID: '2', + parentReportID: '2', + parentReportActionID: '1', + ownerAccountID: 12345, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + type: CONST.REPORT.TYPE.EXPENSE, + }; + const MOCK_REPORTS: ReportCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT}1` as const]: MOCK_EXPENSE_REPORT, + [`${ONYXKEYS.COLLECTION.REPORT}2` as const]: { + reportID: '2', + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + }, + }; + const MOCK_REPORT_ACTIONS: ReportActionsCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2` as const]: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '1': { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorAccountID: 12345, + created: '2024-08-08 18:20:44.171', + message: { + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount: 10, + currency: CONST.CURRENCY.USD, + IOUReportID: '1', + text: 'Vacation expense', + IOUTransactionID: '1', + }, + }, + }, + }; + await Onyx.multiSet({ + ...MOCK_REPORTS, + ...MOCK_REPORT_ACTIONS, + [ONYXKEYS.SESSION]: { + accountID: 12345, + }, + [`${ONYXKEYS.COLLECTION.TRANSACTION}1` as const]: { + transactionID: '1', + amount: 10, + modifiedAmount: 10, + reportID: '1', + }, + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}1` as const]: [ + { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.MISSING_CATEGORY, + }, + ], + }); + const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_EXPENSE_REPORT); + expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); + }); + it('returns correct reason when report has errors', () => { + const reason = DebugUtils.getReasonForShowingRowInLHN({ + ...baseReport, + errors: { + error: 'Something went wrong', + }, + }); + expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); + }); }); describe('getReasonAndReportActionForGBRInLHNRow', () => { beforeAll(() => { From 7f557e0a0b225fda6979efaa3cae6ac2189bfa85 Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Fri, 11 Oct 2024 08:31:41 +0800 Subject: [PATCH 37/55] update the selected currency on rate modal when workspace currency changes --- src/components/MoneyRequestConfirmationList.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 1faef0c6b44c..635a31e06623 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -292,7 +292,9 @@ function MoneyRequestConfirmationList({ const distance = TransactionUtils.getDistanceInMeters(transaction, unit); const prevDistance = usePrevious(distance); - const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance); + const prevCurrency = usePrevious(currency); + + const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance || prevCurrency !== currency); const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense; From 90e18c518ab36f348eae3c735bb0188d936452cd Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Fri, 11 Oct 2024 13:36:56 +0200 Subject: [PATCH 38/55] fix PR comments --- src/components/Search/SearchRouter/SearchRouter.tsx | 2 +- src/styles/variables.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 564e5f0feda0..5c1f4e27c93d 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -153,7 +153,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { closeAndClearRouter(); }); - const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.popoverWidth}; + const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.searchRouterPopoverWidth}; return ( Date: Fri, 11 Oct 2024 21:29:02 +0800 Subject: [PATCH 39/55] minor fix --- src/components/MoneyRequestConfirmationList.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 635a31e06623..f14ee940e329 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -256,6 +256,7 @@ function MoneyRequestConfirmationList({ const prevRate = usePrevious(rate); const currency = (mileageRate as MileageRate)?.currency ?? policyCurrency; + const prevCurrency = usePrevious(currency); // A flag for showing the categories field const shouldShowCategories = (isPolicyExpenseChat || isTypeInvoice) && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); @@ -292,8 +293,6 @@ function MoneyRequestConfirmationList({ const distance = TransactionUtils.getDistanceInMeters(transaction, unit); const prevDistance = usePrevious(distance); - const prevCurrency = usePrevious(currency); - const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance || prevCurrency !== currency); const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); From de2359ffba4eba811c3f4c19556c5828d1eab4b5 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 11 Oct 2024 18:33:46 +0100 Subject: [PATCH 40/55] fix(debug mode): is temporary focused reason shown instead of has RBR message --- src/libs/DebugUtils.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 3b6c1c77713f..97469f5f6c74 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -7,6 +7,7 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, Policy, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; import * as ReportUtils from './ReportUtils'; +import SidebarUtils from './SidebarUtils'; class NumberError extends SyntaxError { constructor() { @@ -127,6 +128,15 @@ Onyx.connect({ }, }); +let reportActions: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (value) => { + reportActions = value; + }, +}); + let transactionViolations: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, @@ -612,7 +622,7 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath if ( !([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && - ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations) + SidebarUtils.shouldShowRedBrickRoad(report, reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`], doesReportHaveViolations, transactionViolations) ) { return `debug.reasonVisibleInLHN.hasRBR`; } From 6a8b6fd22ecf49d14013c7dff83c3dcba8c2c2a2 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 11 Oct 2024 18:38:15 +0100 Subject: [PATCH 41/55] chore: fix eslint errors --- src/libs/DebugUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 97469f5f6c74..261d2db208d9 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -128,12 +128,12 @@ Onyx.connect({ }, }); -let reportActions: OnyxCollection; +let reportActionsCollection: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, waitForCollectionCallback: true, callback: (value) => { - reportActions = value; + reportActionsCollection = value; }, }); @@ -622,7 +622,7 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath if ( !([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && - SidebarUtils.shouldShowRedBrickRoad(report, reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`], doesReportHaveViolations, transactionViolations) + SidebarUtils.shouldShowRedBrickRoad(report, reportActionsCollection?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`], doesReportHaveViolations, transactionViolations) ) { return `debug.reasonVisibleInLHN.hasRBR`; } From 96f59359c35f3d5f2e4cf7f237ee64328e76db20 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 14 Oct 2024 10:34:46 +0100 Subject: [PATCH 42/55] refactor: apply suggestions --- src/libs/DebugUtils.ts | 17 ++--------------- src/pages/Debug/Report/DebugReportPage.tsx | 6 +++--- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 261d2db208d9..fee35c50e0dc 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -7,7 +7,6 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, Policy, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; import * as ReportUtils from './ReportUtils'; -import SidebarUtils from './SidebarUtils'; class NumberError extends SyntaxError { constructor() { @@ -128,15 +127,6 @@ Onyx.connect({ }, }); -let reportActionsCollection: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (value) => { - reportActionsCollection = value; - }, -}); - let transactionViolations: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, @@ -601,7 +591,7 @@ function validateReportActionJSON(json: string) { /** * Gets the reason for showing LHN row */ -function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPaths | null { +function getReasonForShowingRowInLHN(report: OnyxEntry, hasRBR: boolean): TranslationPaths | null { if (!report) { return null; } @@ -620,10 +610,7 @@ function getReasonForShowingRowInLHN(report: OnyxEntry): TranslationPath includeSelfDM: true, }); - if ( - !([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && - SidebarUtils.shouldShowRedBrickRoad(report, reportActionsCollection?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`], doesReportHaveViolations, transactionViolations) - ) { + if (!([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && hasRBR) { return `debug.reasonVisibleInLHN.hasRBR`; } diff --git a/src/pages/Debug/Report/DebugReportPage.tsx b/src/pages/Debug/Report/DebugReportPage.tsx index 530b4b5f4aec..28f4ddf3dc34 100644 --- a/src/pages/Debug/Report/DebugReportPage.tsx +++ b/src/pages/Debug/Report/DebugReportPage.tsx @@ -59,12 +59,12 @@ function DebugReportPage({ return []; } - const reasonLHN = DebugUtils.getReasonForShowingRowInLHN(report); - const {reason: reasonGBR, reportAction: reportActionGBR} = DebugUtils.getReasonAndReportActionForGBRInLHNRow(report) ?? {}; - const reportActionRBR = DebugUtils.getRBRReportAction(report, reportActions); const shouldDisplayViolations = ReportUtils.shouldDisplayTransactionThreadViolations(report, transactionViolations, parentReportAction); const shouldDisplayReportViolations = ReportUtils.isReportOwner(report) && ReportUtils.hasReportViolations(reportID); const hasRBR = SidebarUtils.shouldShowRedBrickRoad(report, reportActions, !!shouldDisplayViolations || shouldDisplayReportViolations, transactionViolations); + const reasonLHN = DebugUtils.getReasonForShowingRowInLHN(report, hasRBR); + const {reason: reasonGBR, reportAction: reportActionGBR} = DebugUtils.getReasonAndReportActionForGBRInLHNRow(report) ?? {}; + const reportActionRBR = DebugUtils.getRBRReportAction(report, reportActions); const hasGBR = !hasRBR && !!reasonGBR; return [ From 212f461eeba8e82d7256a7c15b133f7b1b199f37 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 14 Oct 2024 10:52:04 +0100 Subject: [PATCH 43/55] fix: debug utils hasRBR tests --- src/libs/DebugUtils.ts | 2 +- tests/unit/DebugUtilsTest.ts | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index fee35c50e0dc..e7ad63467781 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -591,7 +591,7 @@ function validateReportActionJSON(json: string) { /** * Gets the reason for showing LHN row */ -function getReasonForShowingRowInLHN(report: OnyxEntry, hasRBR: boolean): TranslationPaths | null { +function getReasonForShowingRowInLHN(report: OnyxEntry, hasRBR = false): TranslationPaths | null { if (!report) { return null; } diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index a87648d08eec..fa44b8972cf3 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -838,7 +838,7 @@ describe('DebugUtils', () => { }, ], }); - const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_TRANSACTION_REPORT); + const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_TRANSACTION_REPORT, true); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); it('returns correct reason when report has violations', async () => { @@ -896,16 +896,19 @@ describe('DebugUtils', () => { }, ], }); - const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_EXPENSE_REPORT); + const reason = DebugUtils.getReasonForShowingRowInLHN(MOCK_EXPENSE_REPORT, true); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); it('returns correct reason when report has errors', () => { - const reason = DebugUtils.getReasonForShowingRowInLHN({ - ...baseReport, - errors: { - error: 'Something went wrong', + const reason = DebugUtils.getReasonForShowingRowInLHN( + { + ...baseReport, + errors: { + error: 'Something went wrong', + }, }, - }); + true, + ); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); }); From 1d0811f96a28e507bc9b48ed468251e538ede386 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 14 Oct 2024 15:29:08 +0200 Subject: [PATCH 44/55] fix style bugs --- src/components/Search/SearchRouter/SearchRouterInput.tsx | 2 +- src/components/Search/SearchRouter/SearchRouterList.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index e28775c3686e..ee236494fcc6 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -88,7 +88,7 @@ function SearchRouterInput({ disabled={disabled} onSubmitEditing={onSubmit} shouldUseDisabledStyles={false} - textInputContainerStyles={styles.borderNone} + textInputContainerStyles={[styles.borderNone, styles.pb0]} inputStyle={[styles.searchInputStyle, inputWidth, styles.pl3, styles.pr3]} onFocus={() => { setIsFocused(true); diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index d05588afbe9a..8485d0c2eb50 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -80,7 +80,6 @@ function SearchRouterItem(props: UserListItemProps | SearchQueryList return ( @@ -145,7 +144,7 @@ function SearchRouterList( sections.push({title: translate('search.recentSearches'), data: recentSearchesData}); } - const styledRecentReports = recentReports.map((item) => ({...item, pressableStyle: styles.br2})); + const styledRecentReports = recentReports.map((item) => ({...item, pressableStyle: styles.br2, wrapperStyle: [styles.pr3, styles.pl3]})); sections.push({title: translate('search.recentChats'), data: styledRecentReports}); const onSelectRow = useCallback( @@ -182,6 +181,7 @@ function SearchRouterList( ListItem={SearchRouterItem} containerStyle={[styles.mh100]} sectionListStyle={[isSmallScreenWidth ? styles.ph5 : styles.ph2, styles.pb2]} + listItemWrapperStyle={[styles.pr3, styles.pl3]} onLayout={setPerformanceTimersEnd} ref={ref} showScrollIndicator={!isSmallScreenWidth} From 935067ed56dd45bd24edd3ce834edc2dd112bc7d Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 14 Oct 2024 16:03:53 +0200 Subject: [PATCH 45/55] add offline message --- src/CONST.ts | 2 +- .../Search/SearchRouter/SearchButton.tsx | 4 +- .../Search/SearchRouter/SearchRouter.tsx | 9 ++- .../Search/SearchRouter/SearchRouterInput.tsx | 77 ++++++++++++------- .../Search/SearchRouter/SearchRouterList.tsx | 5 +- .../E2E/tests/openSearchRouterTest.e2e.ts | 2 +- 6 files changed, 64 insertions(+), 35 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index ab6d057393e2..51da86fbcb7f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1102,7 +1102,7 @@ const CONST = { }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', - SEARCH_ROUTER_OPEN: 'search_router_render', + SEARCH_ROUTER_RENDER: 'search_router_render', CHAT_RENDER: 'chat_render', OPEN_REPORT: 'open_report', HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index ae755fbf2e27..7ed22ec8162f 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -27,8 +27,8 @@ function SearchButton({style}: SearchButtonProps) { accessibilityLabel={translate('common.search')} style={[styles.flexRow, styles.touchableButtonImage, style]} onPress={Session.checkIfActionIsAllowed(() => { - Timing.start(CONST.TIMING.SEARCH_ROUTER_OPEN); - Performance.markStart(CONST.TIMING.SEARCH_ROUTER_OPEN); + Timing.start(CONST.TIMING.SEARCH_ROUTER_RENDER); + Performance.markStart(CONST.TIMING.SEARCH_ROUTER_RENDER); openSearchRouter(); })} diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 1e6d8ff16ba8..e8d613e0b6f1 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -3,6 +3,7 @@ import debounce from 'lodash/debounce'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {useOptionsList} from '@components/OptionListContextProvider'; import type {SearchQueryJSON} from '@components/Search/types'; @@ -10,6 +11,7 @@ import type {SelectionListHandle} from '@components/SelectionList/types'; import useDebouncedState from '@hooks/useDebouncedState'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; @@ -176,8 +178,13 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { setValue={setTextInputValue} isFullWidth={isSmallScreenWidth} updateSearch={onSearchChange} + onSubmit={() => { + onSearchSubmit(SearchUtils.buildSearchQueryJSON(textInputValue)); + }} routerListRef={listRef} - wrapperStyle={[isSmallScreenWidth ? styles.mv3 : styles.mv2, isSmallScreenWidth ? styles.mh5 : styles.mh2, styles.border]} + shouldShowOfflineMessage + wrapperStyle={[styles.border, styles.alignItemsCenter]} + outerWrapperStyle={[isSmallScreenWidth ? styles.mv3 : styles.mv2, isSmallScreenWidth ? styles.mh5 : styles.mh2]} wrapperFocusedStyle={[styles.borderColorFocus]} isSearchingForReports={isSearchingForReports} /> diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index ee236494fcc6..84ebf2edc0b7 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -2,9 +2,11 @@ import React, {useState} from 'react'; import type {ReactNode, RefObject} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; +import FormHelpMessage from '@components/FormHelpMessage'; import type {SelectionListHandle} from '@components/SelectionList/types'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -31,6 +33,9 @@ type SearchRouterInputProps = { /** Whether the input is disabled */ disabled?: boolean; + /** Whether the offline message should be shown */ + shouldShowOfflineMessage?: boolean; + /** Whether the input should be focused */ autoFocus?: boolean; @@ -40,6 +45,9 @@ type SearchRouterInputProps = { /** Any additional styles to apply when input is focused */ wrapperFocusedStyle?: StyleProp; + /** Any additional styles to apply to text input along with FormHelperMessage */ + outerWrapperStyle?: StyleProp; + /** Component to be displayed on the right */ rightComponent?: ReactNode; @@ -55,15 +63,19 @@ function SearchRouterInput({ routerListRef, isFullWidth, disabled = false, + shouldShowOfflineMessage = false, autoFocus = true, wrapperStyle, wrapperFocusedStyle, + outerWrapperStyle, rightComponent, isSearchingForReports, }: SearchRouterInputProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isFocused, setIsFocused] = useState(false); + const {isOffline} = useNetwork(); + const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const onChangeText = (text: string) => { setValue(text); @@ -73,35 +85,44 @@ function SearchRouterInput({ const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; return ( - - - { - setIsFocused(true); - routerListRef?.current?.updateExternalTextInputFocus(true); - }} - onBlur={() => { - setIsFocused(false); - routerListRef?.current?.updateExternalTextInputFocus(false); - }} - isLoading={!!isSearchingForReports} - /> + + + + { + setIsFocused(true); + routerListRef?.current?.updateExternalTextInputFocus(true); + }} + onBlur={() => { + setIsFocused(false); + routerListRef?.current?.updateExternalTextInputFocus(false); + }} + isLoading={!!isSearchingForReports} + /> + + {rightComponent && {rightComponent}} - {rightComponent && {rightComponent}} + ); } diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 8485d0c2eb50..84c300bfc895 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -51,8 +51,8 @@ type SearchRouterListProps = { }; const setPerformanceTimersEnd = () => { - Timing.end(CONST.TIMING.SEARCH_ROUTER_OPEN); - Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_OPEN); + Timing.end(CONST.TIMING.SEARCH_ROUTER_RENDER); + Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_RENDER); }; function isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem { @@ -186,6 +186,7 @@ function SearchRouterList( ref={ref} showScrollIndicator={!isSmallScreenWidth} sectionTitleStyles={styles.mhn2} + shouldSingleExecuteRowSelect /> ); } diff --git a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts index 6c4646b09fbf..840af5acc2c9 100644 --- a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts +++ b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts @@ -32,7 +32,7 @@ const test = () => { Performance.subscribeToMeasurements((entry) => { console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`); - if (entry.name === CONST.TIMING.SEARCH_ROUTER_OPEN) { + if (entry.name === CONST.TIMING.SEARCH_ROUTER_RENDER) { E2EClient.submitTestResults({ branch: Config.E2E_BRANCH, name: 'Open Search Router TTI', From a6a74c5562f34291ebd5361297dd3048950966c4 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 14 Oct 2024 16:33:04 +0200 Subject: [PATCH 46/55] fix linter --- src/components/Search/SearchRouter/SearchRouter.tsx | 2 -- src/components/Search/SearchRouter/SearchRouterInput.tsx | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index e8d613e0b6f1..a957806ee4f7 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -3,7 +3,6 @@ import debounce from 'lodash/debounce'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; -import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {useOptionsList} from '@components/OptionListContextProvider'; import type {SearchQueryJSON} from '@components/Search/types'; @@ -11,7 +10,6 @@ import type {SelectionListHandle} from '@components/SelectionList/types'; import useDebouncedState from '@hooks/useDebouncedState'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index 84ebf2edc0b7..ef6963152c42 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -120,6 +120,7 @@ function SearchRouterInput({ {rightComponent && {rightComponent}} From 28cf9ad4e5738762185137877be52608f1c2754d Mon Sep 17 00:00:00 2001 From: HezekielT Date: Tue, 15 Oct 2024 11:10:34 +0300 Subject: [PATCH 47/55] added a comment --- src/components/Attachments/AttachmentView/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 132f0affc231..080e0ec589ec 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -36,6 +36,7 @@ type AttachmentViewProps = Attachment & { /** Function for handle on press */ onPress?: (e?: GestureResponderEvent | KeyboardEvent) => void; + /** Whether the attachment is used in attachment modal */ isUsedInAttachmentModal?: boolean; /** Flag to show/hide download icon */ From c6eecbefdb9a0cd42c5b179cd3865cdf0d8d6fd6 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Tue, 15 Oct 2024 22:58:27 +0200 Subject: [PATCH 48/55] implement advanced screen --- src/CONST.ts | 2 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/languages/en.ts | 7 ++ src/languages/es.ts | 7 ++ src/libs/API/types.ts | 4 + .../ModalStackNavigators/index.tsx | 2 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 6 ++ .../actions/connections/QuickbooksDesktop.ts | 28 +++++++ .../QuickbooksDesktopAdvancedPage.tsx | 82 +++++++++++++++++++ src/pages/workspace/accounting/utils.tsx | 4 +- src/types/onyx/Policy.ts | 7 +- 14 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAdvancedPage.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 40f3c541b0f7..5f7b36288500 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1492,7 +1492,9 @@ const CONST = { MARK_CHECKS_TO_BE_PRINTED: 'markChecksToBePrinted', REIMBURSABLE_ACCOUNT: 'reimbursableAccount', REIMBURSABLE: 'reimbursable', + AUTO_SYNC: 'autoSync', ENABLE_NEW_CATEGORIES: 'enableNewCategories', + SHOULD_AUTO_CREATE_VENDOR: 'shouldAutoCreateVendor', MAPPINGS: { CLASSES: 'classes', }, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7751ce7be4b6..816b3983117f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -670,6 +670,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/date-select', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/date-select` as const, }, + WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/advanced', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/advanced` as const, + }, POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT: { route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/export/date-select', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/export/date-select` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3da0f86d651e..5a114f7e2eda 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -317,6 +317,7 @@ const SCREENS = { QUICKBOOKS_ONLINE_ADVANCED: 'Policy_Accounting_Quickbooks_Online_Advanced', QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Account_Selector', QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Invoice_Account_Selector', + QUICKBOOKS_DESKTOP_ADVANCED: 'Policy_Accounting_Quickbooks_Desktop_Advanced', QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Date_Select', QUICKBOOKS_DESKTOP_EXPORT_PREFERRED_EXPORTER: 'Workspace_Accounting_Quickbooks_Desktop_Export_Preferred_Exporter', QUICKBOOKS_DESKTOP_EXPORT_OUT_OF_POCKET_EXPENSES: 'Workspace_Accounting_Quickbooks_Desktop_Export_Out_Of_Pocket_Expenses', diff --git a/src/languages/en.ts b/src/languages/en.ts index 58ac38d1856c..054c5d64e680 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2458,6 +2458,13 @@ const translations = { classesDescription: 'Choose how to handle QuickBooks Desktop classes in Expensify.', tagsDisplayedAsDescription: 'Line item level', reportFieldsDisplayedAsDescription: 'Report level', + advancedConfig: { + autoSyncDescription: 'Expensify will automatically sync with QuickBooks Desktop every day.', + createEntities: 'Auto-create entities', + createEntitiesDescription: + "Expensify will automatically create vendors in QuickBooks Desktop if they don't exist already, and auto-create customers when exporting invoices.", + reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the Quickbooks Online account below.', + }, }, qbo: { importDescription: 'Choose which coding configurations to import from QuickBooks Online to Expensify.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 650ccdf5e660..51a6e11f9e00 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2482,6 +2482,13 @@ const translations = { classesDescription: 'Elige cómo gestionar las clases de QuickBooks Desktop en Expensify.', tagsDisplayedAsDescription: 'Nivel de partida', reportFieldsDisplayedAsDescription: 'Nivel de informe', + advancedConfig: { + autoSyncDescription: 'Expensify se sincronizará automáticamente con QuickBooks Desktop todos los días.', + createEntities: 'Crear entidades automáticamente', + createEntitiesDescription: 'Expensify creará automáticamente proveedores en QuickBooks Desktop si aún no existen, y creará automáticamente clientes al exportar facturas.', + reimbursedReportsDescription: + 'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de Quickbooks Online indicadas a continuación.', + }, }, qbo: { importDescription: 'Elige que configuraciónes de codificación son importadas desde QuickBooks Online a Expensify.', diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 2463fc1cdc0c..a19d63816113 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -259,6 +259,8 @@ const WRITE_COMMANDS = { UPDATE_QUICKBOOKS_ONLINE_EXPORT: 'UpdateQuickbooksOnlineExport', UPDATE_QUICKBOOKS_DESKTOP_EXPORT_DATE: 'UpdateQuickbooksDesktopExportDate', UPDATE_MANY_POLICY_CONNECTION_CONFIGS: 'UpdateManyPolicyConnectionConfigurations', + UPDATE_QUICKBOOKS_DESKTOP_AUTO_CREATE_VENDOR: 'UpdateQuickbooksDesktopAutoCreateVendor', + UPDATE_QUICKBOOKS_DESKTOP_AUTO_SYNC: 'UpdateQuickbooksDesktopAutoSync', UPDATE_QUICKBOOKS_DESKTOP_EXPORT: 'UpdateQuickbooksDesktopExport', UPDATE_QUICKBOOKS_DESKTOP_REIMBURSABLE_EXPENSES_ACCOUNT: 'UpdateQuickbooksDesktopReimbursableExpensesAccount', UPDATE_QUICKBOOKS_DESKTOP_MARK_CHECKS_TO_BE_PRINTED: 'UpdateQuickbooksDesktopMarkChecksToBePrinted', @@ -698,6 +700,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_COLLECTION_ACCOUNT_ID]: Parameters.UpdateQuickbooksOnlineGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_EXPORT_DATE]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_MARK_CHECKS_TO_BE_PRINTED]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; + [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_AUTO_CREATE_VENDOR]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; + [WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_AUTO_SYNC]: Parameters.UpdateQuickbooksOnlineGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_REIMBURSABLE_EXPENSES_ACCOUNT]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: Parameters.UpdateQuickbooksDesktopExpensesExportDestinationTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_ENABLE_NEW_CATEGORIES]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index c32f63906443..f149d72bc34c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -323,6 +323,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseEntitySelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT]: () => require('../../../../pages/workspace/accounting/qbd/export/QuickbooksDesktopExportPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED]: () => + require('../../../../pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAdvancedPage').default, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_SETUP_MODAL]: () => require('../../../../pages/workspace/accounting/qbd/QuickBooksDesktopSetupPage').default, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_SETUP_REQUIRED_DEVICE_MODAL]: () => require('../../../../pages/workspace/accounting/qbd/RequireQuickBooksDesktopPage').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 552310dceae4..380977e4e06f 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -45,6 +45,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_ADVANCED, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR, + SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_PREFERRED_EXPORTER, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_OUT_OF_POCKET_EXPENSES, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 8f152e2d35de..28d5832e38ed 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -393,6 +393,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR]: { path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR.route, }, + [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED]: { + path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED.route, + }, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT.route}, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_PREFERRED_EXPORTER]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_PREFERRED_EXPORTER.route}, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_OUT_OF_POCKET_EXPENSES_ACCOUNT_SELECT]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index f1d5b38b6f44..90e2d0e0224b 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -436,6 +436,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_PREFERRED_EXPORTER]: { policyID: string; }; + [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED]: { + policyID: string; + }; [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT]: { policyID: string; }; @@ -1401,6 +1404,9 @@ type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: { policyID: string; }; + [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED]: { + policyID: string; + }; [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_ADVANCED]: { policyID: string; }; diff --git a/src/libs/actions/connections/QuickbooksDesktop.ts b/src/libs/actions/connections/QuickbooksDesktop.ts index 80f50dce7b8a..d20af8426395 100644 --- a/src/libs/actions/connections/QuickbooksDesktop.ts +++ b/src/libs/actions/connections/QuickbooksDesktop.ts @@ -341,6 +341,21 @@ function updateQuickbooksDesktopExpensesExportDestination( + policyID: string, + settingValue: TSettingValue, +) { + const onyxData = buildOnyxDataForQuickbooksConfiguration(policyID, CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR, settingValue, !settingValue); + + const parameters: UpdateQuickbooksDesktopGenericTypeParams = { + policyID, + settingValue: JSON.stringify(settingValue), + idempotencyKey: String(CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR), + }; + + API.write(WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_AUTO_CREATE_VENDOR, parameters, onyxData); +} + function updateQuickbooksDesktopMarkChecksToBePrinted( policyID: string, settingValue: TSettingValue, @@ -425,9 +440,22 @@ function updateQuickbooksDesktopExportDate(policyID: string, settingValue: TSettingValue) { + const onyxData = buildOnyxDataForQuickbooksConfiguration(policyID, CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC, {enabled: settingValue}, {enabled: !settingValue}); + + const parameters: UpdateQuickbooksDesktopGenericTypeParams = { + policyID, + settingValue: JSON.stringify(settingValue), + idempotencyKey: String(CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC), + }; + API.write(WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_AUTO_SYNC, parameters, onyxData); +} + export { + updateQuickbooksDesktopAutoSync, updateQuickbooksDesktopPreferredExporter, updateQuickbooksDesktopMarkChecksToBePrinted, + updateQuickbooksDesktopShouldAutoCreateVendor, updateQuickbooksDesktopExpensesExportDestination, updateQuickbooksDesktopReimbursableExpensesAccount, getQuickbooksDesktopCodatSetupLink, diff --git a/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAdvancedPage.tsx b/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAdvancedPage.tsx new file mode 100644 index 000000000000..cd54127d3a44 --- /dev/null +++ b/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAdvancedPage.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import ConnectionLayout from '@components/ConnectionLayout'; +import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {settingsPendingAction} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import {clearQBDErrorField} from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function QuickbooksDesktopAdvancedPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const policyID = policy?.id ?? '-1'; + const qbdConfig = policy?.connections?.quickbooksDesktop?.config; + const {canUseNewDotQBD} = usePermissions(); + + const qbdToggleSettingItems = [ + { + title: translate('workspace.accounting.autoSync'), + subtitle: translate('workspace.qbd.advancedConfig.autoSyncDescription'), + switchAccessibilityLabel: translate('workspace.qbd.advancedConfig.autoSyncDescription'), + isActive: !!qbdConfig?.autoSync?.enabled, + onToggle: (isOn: boolean) => QuickbooksDesktop.updateQuickbooksDesktopAutoSync(policyID, isOn), + subscribedSetting: CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC, + errors: ErrorUtils.getLatestErrorField(qbdConfig, CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC), + pendingAction: settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC], qbdConfig?.pendingFields), + }, + { + title: translate('workspace.qbd.advancedConfig.createEntities'), + subtitle: translate('workspace.qbd.advancedConfig.createEntitiesDescription'), + switchAccessibilityLabel: translate('workspace.qbd.advancedConfig.createEntitiesDescription'), + isActive: !!qbdConfig?.shouldAutoCreateVendor, + onToggle: (isOn: boolean) => { + QuickbooksDesktop.updateQuickbooksDesktopShouldAutoCreateVendor(policyID, isOn); + }, + subscribedSetting: CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR, + errors: ErrorUtils.getLatestErrorField(qbdConfig, CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR), + pendingAction: settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR], qbdConfig?.pendingFields), + }, + ]; + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID))} + > + {qbdToggleSettingItems.map((item) => ( + clearQBDErrorField(policyID, item.subscribedSetting)} + /> + ))} + + ); +} + +QuickbooksDesktopAdvancedPage.displayName = 'QuickbooksDesktopAdvancedPage'; + +export default withPolicyConnections(QuickbooksDesktopAdvancedPage); diff --git a/src/pages/workspace/accounting/utils.tsx b/src/pages/workspace/accounting/utils.tsx index 7e694babdd5c..cc1dfa1fcd9f 100644 --- a/src/pages/workspace/accounting/utils.tsx +++ b/src/pages/workspace/accounting/utils.tsx @@ -259,7 +259,7 @@ function getAccountingIntegrationData( onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_IMPORT.getRoute(policyID)), onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_EXPORT.getRoute(policyID)), onCardReconciliationPagePress: () => {}, - onAdvancedPagePress: () => {}, + onAdvancedPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED.getRoute(policyID)), // TODO: [QBD] Make sure all values are passed to subscribedSettings subscribedImportSettings: [CONST.QUICKBOOKS_DESKTOP_CONFIG.ENABLE_NEW_CATEGORIES, CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CLASSES], subscribedExportSettings: [ @@ -269,7 +269,7 @@ function getAccountingIntegrationData( CONST.QUICKBOOKS_DESKTOP_CONFIG.REIMBURSABLE_ACCOUNT, CONST.QUICKBOOKS_DESKTOP_CONFIG.MARK_CHECKS_TO_BE_PRINTED, ], - subscribedAdvancedSettings: [], + subscribedAdvancedSettings: [CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR, CONST.QUICKBOOKS_DESKTOP_CONFIG.AUTO_SYNC], }; default: return undefined; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 5ecf9ad8d84f..a1408a6340c5 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1247,13 +1247,16 @@ type QBDConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Configuration of automatic synchronization from QuickBooks Desktop to the app */ autoSync: { - /** TODO: Will be handled in another issue */ + /** Job ID of the synchronization */ jobID: string; - /** Whether changes made in QuickBooks Online should be reflected into the app automatically */ + /** Whether changes made in QuickBooks Desktop should be reflected into the app automatically */ enabled: boolean; }; + /** Whether AutoSync is enabled */ + isAutoSyncEnabled: boolean; + /** Whether a check to be printed */ markChecksToBePrinted: boolean; From e9d31f5440bb6848c131b9f215f8a9f27276eb4f Mon Sep 17 00:00:00 2001 From: Yauheni Date: Tue, 15 Oct 2024 23:05:46 +0200 Subject: [PATCH 49/55] refactor code --- src/languages/en.ts | 1 - src/languages/es.ts | 2 -- src/libs/Navigation/types.ts | 3 --- src/types/onyx/Policy.ts | 2 +- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 054c5d64e680..223fcada3814 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2463,7 +2463,6 @@ const translations = { createEntities: 'Auto-create entities', createEntitiesDescription: "Expensify will automatically create vendors in QuickBooks Desktop if they don't exist already, and auto-create customers when exporting invoices.", - reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the Quickbooks Online account below.', }, }, qbo: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 51a6e11f9e00..90389acf9da9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2486,8 +2486,6 @@ const translations = { autoSyncDescription: 'Expensify se sincronizará automáticamente con QuickBooks Desktop todos los días.', createEntities: 'Crear entidades automáticamente', createEntitiesDescription: 'Expensify creará automáticamente proveedores en QuickBooks Desktop si aún no existen, y creará automáticamente clientes al exportar facturas.', - reimbursedReportsDescription: - 'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de Quickbooks Online indicadas a continuación.', }, }, qbo: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 90e2d0e0224b..0bb6964395db 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1404,9 +1404,6 @@ type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: { policyID: string; }; - [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED]: { - policyID: string; - }; [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_ADVANCED]: { policyID: string; }; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index a1408a6340c5..5f9ecd659a69 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1247,7 +1247,7 @@ type QBDConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Configuration of automatic synchronization from QuickBooks Desktop to the app */ autoSync: { - /** Job ID of the synchronization */ + /** Job ID of the synchronization */ jobID: string; /** Whether changes made in QuickBooks Desktop should be reflected into the app automatically */ From c904c6f9b567b29de824f9341bdd4d6da1aeaf68 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Tue, 15 Oct 2024 23:22:28 +0200 Subject: [PATCH 50/55] fix ts issue --- src/libs/API/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index a19d63816113..53d9ed329969 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -701,7 +701,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_EXPORT_DATE]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_MARK_CHECKS_TO_BE_PRINTED]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_AUTO_CREATE_VENDOR]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; - [WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_AUTO_SYNC]: Parameters.UpdateQuickbooksOnlineGenericTypeParams; + [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_AUTO_SYNC]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_REIMBURSABLE_EXPENSES_ACCOUNT]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: Parameters.UpdateQuickbooksDesktopExpensesExportDestinationTypeParams; [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_ENABLE_NEW_CATEGORIES]: Parameters.UpdateQuickbooksDesktopGenericTypeParams; From 0a3ad65bab186ae1e2d8825dc51e27f9e0212bd8 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 16 Oct 2024 10:17:06 +0300 Subject: [PATCH 51/55] Fix areApprovalsConfigured check --- .../expensifyCard/WorkspaceEditCardLimitTypePage.tsx | 3 +-- src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitTypePage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitTypePage.tsx index e4f14f6c137a..94a1382b4454 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitTypePage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitTypePage.tsx @@ -23,7 +23,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; type WorkspaceEditCardLimitTypePageProps = StackScreenProps; @@ -38,7 +37,7 @@ function WorkspaceEditCardLimitTypePage({route}: WorkspaceEditCardLimitTypePageP const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); const card = cardsList?.[cardID]; - const areApprovalsConfigured = !isEmptyObject(policy?.approver) && policy?.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; + const areApprovalsConfigured = PolicyUtils.getApprovalWorkflow(policy) !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; const defaultLimitType = areApprovalsConfigured ? CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART : CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY; const initialLimitType = card?.nameValuePairs?.limitType ?? defaultLimitType; const promptTranslationKey = diff --git a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx index de4bca070d51..e0ae8954720c 100644 --- a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx @@ -11,11 +11,11 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as Card from '@userActions/Card'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; type LimitTypeStepProps = { // The policy that the card will be issued under @@ -27,7 +27,7 @@ function LimitTypeStep({policy}: LimitTypeStepProps) { const styles = useThemeStyles(); const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD); - const areApprovalsConfigured = !isEmptyObject(policy?.approver) && policy?.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; + const areApprovalsConfigured = PolicyUtils.getApprovalWorkflow(policy) !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; const defaultType = areApprovalsConfigured ? CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART : CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY; const [typeSelected, setTypeSelected] = useState(issueNewCard?.data?.limitType ?? defaultType); From 1631f7c4958399a901128cb42a283b8878387d84 Mon Sep 17 00:00:00 2001 From: Victoria O'Leary <54568958+VictoriaExpensify@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:53:15 +1000 Subject: [PATCH 52/55] Update Consolidated-Domain-Billing.md Updated the FAQ section to remove coding that was shown - https://github.com/Expensify/Expensify/issues/436400 --- .../expensify-billing/Consolidated-Domain-Billing.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md b/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md index 2e829c0785d3..671d0c41e772 100644 --- a/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md +++ b/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md @@ -16,9 +16,12 @@ When a Domain Admin enables Consolidated Domain Billing, all Group workspaces ow If you don’t have multiple billing owners across your organization, or if you want to keep billing separate for any reason, then this feature isn’t necessary. If you have an Annual Subscription and enable Consolidated Domain Billing, the Consolidated Domain Billing feature will gather the amounts due for each Group workspace Billing Owner (listed under **Settings > Workspaces > Group**). To make full use of the Annual Subscription for all workspaces in your domain, you should also be the billing owner for all Group workspaces. + {% include faq-begin.md %} + ## How do I take over the billing of a workspace with Consolidated Domain Billing enabled? You’ll have to toggle off Consolidated Domain Billing, take over ownership of the workspace, and then toggle it back on. + ## Can I use Consolidated Domain Billing to cover the bill for some workspaces, but not others? No, this feature means that you’ll be paying the bill for all domain members who choose a subscription. From 31b5d9fefa6bd251acabf9b821ed520197e44537 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Wed, 16 Oct 2024 12:18:31 +0200 Subject: [PATCH 53/55] remove isAutoSyncEnabled --- src/types/onyx/Policy.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 5f9ecd659a69..2bd94bce6da4 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1254,9 +1254,6 @@ type QBDConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ enabled: boolean; }; - /** Whether AutoSync is enabled */ - isAutoSyncEnabled: boolean; - /** Whether a check to be printed */ markChecksToBePrinted: boolean; From 6d8fc977ac644b87dd194da7b7d2d1521fa90562 Mon Sep 17 00:00:00 2001 From: Jules Date: Wed, 16 Oct 2024 12:00:02 +0100 Subject: [PATCH 54/55] add standalone/hybridApp checkboxes --- .github/ISSUE_TEMPLATE/Standard.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Standard.md b/.github/ISSUE_TEMPLATE/Standard.md index 663c6004a534..1468871bf31e 100644 --- a/.github/ISSUE_TEMPLATE/Standard.md +++ b/.github/ISSUE_TEMPLATE/Standard.md @@ -10,6 +10,8 @@ ___ **Version Number:** **Reproducible in staging?:** **Reproducible in production?:** +**Reproducible on New Expensify Standalone?:** +**Reproducible on HybridApp?:** **If this was caught during regression testing, add the test name, ID and link from TestRail:** **Email or phone of affected tester (no customers):** **Logs:** https://stackoverflow.com/c/expensify/questions/4856 @@ -34,9 +36,11 @@ Can the user still use Expensify without this being fixed? Have you informed the Check off any platforms that are affected by this issue ---> Which of our officially supported platforms is this issue occurring on? -- [ ] Android: Native +- [ ] Android: Standalone +- [ ] Android: HybridApp - [ ] Android: mWeb Chrome -- [ ] iOS: Native +- [ ] iOS: Standalone +- [ ] iOS: HybridApp - [ ] iOS: mWeb Safari - [ ] MacOS: Chrome / Safari - [ ] MacOS: Desktop From 75962b8e9a6fa0a1fccd4b9a61c587197d968ca7 Mon Sep 17 00:00:00 2001 From: Jules Date: Wed, 16 Oct 2024 12:02:59 +0100 Subject: [PATCH 55/55] simplify checklists --- .github/ISSUE_TEMPLATE/Standard.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Standard.md b/.github/ISSUE_TEMPLATE/Standard.md index 1468871bf31e..fa50d48b341b 100644 --- a/.github/ISSUE_TEMPLATE/Standard.md +++ b/.github/ISSUE_TEMPLATE/Standard.md @@ -10,8 +10,7 @@ ___ **Version Number:** **Reproducible in staging?:** **Reproducible in production?:** -**Reproducible on New Expensify Standalone?:** -**Reproducible on HybridApp?:** +**If this was caught on HybridApp, is this reproducible on New Expensify Standalone?:** **If this was caught during regression testing, add the test name, ID and link from TestRail:** **Email or phone of affected tester (no customers):** **Logs:** https://stackoverflow.com/c/expensify/questions/4856