From d99d92d4a1ba20976af09a51cc504fdd9a21169a Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sat, 20 Jan 2024 01:58:07 +0300 Subject: [PATCH 001/218] implemented receipt image component --- src/components/DistanceEReceipt.js | 13 +-- ...oraryForRefactorRequestConfirmationList.js | 8 +- src/components/ReceiptImage.tsx | 82 +++++++++++++++++++ .../ReportActionItemImage.tsx | 44 ++-------- 4 files changed, 97 insertions(+), 50 deletions(-) create mode 100644 src/components/ReceiptImage.tsx diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 0241eea44063..f900da2affcf 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -16,8 +16,8 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import ImageSVG from './ImageSVG'; import PendingMapView from './MapView/PendingMapView'; +import ReceiptImage from './ReceiptImage'; import Text from './Text'; -import ThumbnailImage from './ThumbnailImage'; import transactionPropTypes from './transactionPropTypes'; const propTypes = { @@ -64,16 +64,7 @@ function DistanceEReceipt({transaction}) { /> - {isOffline || !thumbnailSource ? ( - - ) : ( - - )} + {isOffline || !thumbnailSource ? : } {formattedTransactionAmount} diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 36d424ea28f2..67cf9db3674c 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -36,6 +36,7 @@ import Image from './Image'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import optionPropTypes from './optionPropTypes'; import OptionsSelector from './OptionsSelector'; +import ReceiptImage from './ReceiptImage'; import SettlementButton from './SettlementButton'; import Switch from './Switch'; import tagPropTypes from './tagPropTypes'; @@ -639,9 +640,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ )} {(receiptImage || receiptThumbnail) && ( - ; + isAuthTokenRequired?: boolean; + confirmationPage?: boolean; +}; + +function ReceiptImage({transaction, receiptPath, receiptFileName, style, isAuthTokenRequired, confirmationPage}: ReceiptImageProps) { + const styles = useThemeStyles(); + // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg + const path = transaction?.receipt?.source ?? receiptPath ?? ''; + // filename of uploaded image or last part of remote URI + const filename = transaction?.filename ?? receiptFileName ?? ''; + const isReceiptImage = Str.isImage(filename); + const shouldDisplayThumbnail = Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints') || !isReceiptImage; + const image = !shouldDisplayThumbnail && !(path.startsWith('blob:') || path.startsWith('file:')) ? `${path}.1024.jpg` : path; + const isLocalFile = typeof path === 'number' || path.startsWith('blob:') || path.startsWith('file:') || path.startsWith('/'); + + const imageSource = tryResolveUrlFromApiRoot(image ?? ''); + + const isEReceipt = transaction && TransactionUtils.hasEReceipt(transaction); + + if (!transaction) { + return ( + + ); + } + + if (!!isEReceipt || shouldDisplayThumbnail) { + if (!(!isLocalFile && !Str.isPDF(imageSource))) { + return ( + + + + ); + } + + return ( + + ); + } + + return isReceiptImage && !confirmationPage ? ( + + ) : ( + + ); +} + +export default ReceiptImage; diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index aa5d0513f0d7..3822f3ab42aa 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -1,16 +1,10 @@ -import Str from 'expensify-common/lib/str'; import React from 'react'; -import type {ReactElement} from 'react'; -import {View} from 'react-native'; import AttachmentModal from '@components/AttachmentModal'; -import EReceiptThumbnail from '@components/EReceiptThumbnail'; -import Image from '@components/Image'; import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; +import ReceiptImage from '@components/ReceiptImage'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; -import ThumbnailImage from '@components/ThumbnailImage'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; import type {Transaction} from '@src/types/onyx'; @@ -45,34 +39,6 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal = false, tr const styles = useThemeStyles(); const {translate} = useLocalize(); const imageSource = tryResolveUrlFromApiRoot(image ?? ''); - const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? ''); - const isEReceipt = transaction && TransactionUtils.hasEReceipt(transaction); - - let receiptImageComponent: ReactElement; - - if (isEReceipt) { - receiptImageComponent = ( - - - - ); - } else if (thumbnail && !isLocalFile && !Str.isPDF(imageSource as string)) { - receiptImageComponent = ( - - ); - } else { - receiptImageComponent = ( - - ); - } if (enablePreviewModal) { return ( @@ -98,7 +64,11 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal = false, tr accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={translate('accessibilityHints.viewAttachment')} > - {receiptImageComponent} + ) } @@ -108,7 +78,7 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal = false, tr ); } - return receiptImageComponent; + return ; } ReportActionItemImage.displayName = 'ReportActionItemImage'; From a1f5f69a11350073692f4bd4585d62f2ef6c0112 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sat, 20 Jan 2024 02:02:31 +0300 Subject: [PATCH 002/218] pass receipt name for non modal display --- src/components/ReportActionItem/ReportActionItemImage.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 3822f3ab42aa..1570aba49f7c 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -78,7 +78,13 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal = false, tr ); } - return ; + return ( + + ); } ReportActionItemImage.displayName = 'ReportActionItemImage'; From 7dfd88567c07d4279fa0ed4d1d8745b3c3a1961e Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sat, 20 Jan 2024 02:18:53 +0300 Subject: [PATCH 003/218] minor fix --- src/components/ReceiptImage.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 984ba0bf2e9a..8de6d23127a8 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -36,10 +36,11 @@ function ReceiptImage({transaction, receiptPath, receiptFileName, style, isAuthT if (!transaction) { return ( - ); } From e9c766607677fe453531776d31f4652ba2c4b959 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sat, 20 Jan 2024 22:00:19 +0300 Subject: [PATCH 004/218] changed implementation --- src/components/DistanceEReceipt.js | 12 +++- ...oraryForRefactorRequestConfirmationList.js | 13 ++-- src/components/ReceiptImage.tsx | 66 +++++-------------- .../ReportActionItem/MoneyRequestView.js | 1 + .../ReportActionItemImage.tsx | 39 ++++++----- .../ReportActionItemImages.tsx | 8 ++- src/libs/ReceiptUtils.ts | 38 ++--------- 7 files changed, 71 insertions(+), 106 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index f900da2affcf..8593cdcb5b93 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -34,7 +34,7 @@ function DistanceEReceipt({transaction}) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; + const {thumbnail, isThumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : translate('common.tbd'); const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); @@ -64,7 +64,15 @@ function DistanceEReceipt({transaction}) { /> - {isOffline || !thumbnailSource ? : } + {isOffline || !thumbnailSource ? ( + + ) : ( + + )} {formattedTransactionAmount} diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 67cf9db3674c..c6bc49fd381e 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -32,7 +32,6 @@ import categoryPropTypes from './categoryPropTypes'; import ConfirmedRoute from './ConfirmedRoute'; import FormHelpMessage from './FormHelpMessage'; import * as Expensicons from './Icon/Expensicons'; -import Image from './Image'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import optionPropTypes from './optionPropTypes'; import OptionsSelector from './OptionsSelector'; @@ -614,7 +613,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ); }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2, translate]); - const {image: receiptImage, thumbnail: receiptThumbnail} = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; + const { + image: receiptImage, + thumbnail: receiptThumbnail, + isThumbnail, + } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; return ( ; + transactionID?: string; + isThumbnail?: boolean; + shouldUseThumnailImage?: boolean; + isEReceipt?: boolean; + source?: string; isAuthTokenRequired?: boolean; - confirmationPage?: boolean; + style?: StyleProp; }; -function ReceiptImage({transaction, receiptPath, receiptFileName, style, isAuthTokenRequired, confirmationPage}: ReceiptImageProps) { +function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumnailImage = false, isEReceipt = false, source, isAuthTokenRequired, style}: ReceiptImageProps) { const styles = useThemeStyles(); - // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg - const path = transaction?.receipt?.source ?? receiptPath ?? ''; - // filename of uploaded image or last part of remote URI - const filename = transaction?.filename ?? receiptFileName ?? ''; - const isReceiptImage = Str.isImage(filename); - const shouldDisplayThumbnail = Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints') || !isReceiptImage; - const image = !shouldDisplayThumbnail && !(path.startsWith('blob:') || path.startsWith('file:')) ? `${path}.1024.jpg` : path; - const isLocalFile = typeof path === 'number' || path.startsWith('blob:') || path.startsWith('file:') || path.startsWith('/'); - const imageSource = tryResolveUrlFromApiRoot(image ?? ''); - - const isEReceipt = transaction && TransactionUtils.hasEReceipt(transaction); - - if (!transaction) { + if (isEReceipt || isThumbnail) { return ( - + + + ); } - if (!!isEReceipt || shouldDisplayThumbnail) { - if (!(!isLocalFile && !Str.isPDF(imageSource))) { - return ( - - - - ); - } - + if (shouldUseThumnailImage) { return ( - ) : ( + return ( ); } +export type {ReceiptImageProps}; export default ReceiptImage; diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 86affbcac114..4fa2f76e20b8 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -237,6 +237,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate - + ) } @@ -78,13 +93,7 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal = false, tr ); } - return ( - - ); + return ; } ReportActionItemImage.displayName = 'ReportActionItemImage'; diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx index c24defb8ac08..8c2fc9638dea 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.tsx +++ b/src/components/ReportActionItem/ReportActionItemImages.tsx @@ -11,8 +11,9 @@ import type {Transaction} from '@src/types/onyx'; import ReportActionItemImage from './ReportActionItemImage'; type Image = { - thumbnail: string | number; - image: string | number; + thumbnail?: string; + isThumbnail?: boolean; + image: string; transaction: Transaction; isLocalFile: boolean; }; @@ -71,7 +72,7 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report return ( - {shownImages.map(({thumbnail, image, transaction, isLocalFile}, index) => { + {shownImages.map(({thumbnail, isThumbnail, image, transaction, isLocalFile}, index) => { const isLastImage = index === numberOfShownImages - 1; // Show a border to separate multiple images. Shown to the right for each except the last. @@ -87,6 +88,7 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report image={image} isLocalFile={isLocalFile} transaction={transaction} + isThumbnail={isThumbnail} /> {isLastImage && remaining > 0 && ( diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index bcba68a3a0bd..a01f7779c13a 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -1,24 +1,13 @@ import Str from 'expensify-common/lib/str'; -import type {ImageSourcePropType} from 'react-native'; -import ReceiptDoc from '@assets/images/receipt-doc.png'; -import ReceiptGeneric from '@assets/images/receipt-generic.png'; -import ReceiptHTML from '@assets/images/receipt-html.png'; -import ReceiptSVG from '@assets/images/receipt-svg.png'; -import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; -import * as FileUtils from './fileDownload/FileUtils'; type ThumbnailAndImageURI = { - image: ImageSourcePropType | string; - thumbnail: ImageSourcePropType | string | null; + image: string; + thumbnail?: string; transaction?: Transaction; isLocalFile?: boolean; -}; - -type FileNameAndExtension = { - fileExtension?: string; - fileName?: string; + isThumbnail?: boolean; }; /** @@ -39,12 +28,12 @@ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string if (!Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { if (hasEReceipt) { - return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; + return {image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; } // For local files, we won't have a thumbnail yet if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { - return {thumbnail: null, image: path, isLocalFile: true}; + return {image: path, isLocalFile: true}; } if (isReceiptImage) { @@ -52,22 +41,9 @@ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string } } - const {fileExtension} = FileUtils.splitExtensionFromFileName(filename) as FileNameAndExtension; - let image = ReceiptGeneric; - if (fileExtension === CONST.IOU.FILE_TYPES.HTML) { - image = ReceiptHTML; - } - - if (fileExtension === CONST.IOU.FILE_TYPES.DOC || fileExtension === CONST.IOU.FILE_TYPES.DOCX) { - image = ReceiptDoc; - } - - if (fileExtension === CONST.IOU.FILE_TYPES.SVG) { - image = ReceiptSVG; - } - const isLocalFile = typeof path === 'number' || path.startsWith('blob:') || path.startsWith('file:') || path.startsWith('/'); - return {thumbnail: image, image: path, isLocalFile}; + + return {isThumbnail: true, image: path, isLocalFile}; } // eslint-disable-next-line import/prefer-default-export From e88e1ce8082cfc9845a6dbe267e45ee6c73f1c2c Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sat, 20 Jan 2024 22:08:44 +0300 Subject: [PATCH 005/218] fix styling --- src/components/ReceiptImage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index df34aec496e0..7c5fde88f600 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -21,7 +21,7 @@ function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumnailImag if (isEReceipt || isThumbnail) { return ( - + ); From 26fa66ba9be8a4f87a8ce1b673653811f3c87f9c Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sat, 20 Jan 2024 22:35:37 +0300 Subject: [PATCH 006/218] fix borderRadius --- src/components/EReceiptThumbnail.tsx | 4 +++- src/components/ReceiptImage.tsx | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 5976200975cd..b677a60cc02c 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -18,6 +18,7 @@ import Image from './Image'; type EReceiptThumbnailOnyxProps = { transaction: OnyxEntry; + borderRadius?: number; }; type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { @@ -35,7 +36,7 @@ const backgroundImages = { [CONST.ERECEIPT_COLORS.PINK]: eReceiptBGs.EReceiptBG_Pink, }; -function EReceiptThumbnail({transaction}: EReceiptThumbnailProps) { +function EReceiptThumbnail({transaction, borderRadius}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -83,6 +84,7 @@ function EReceiptThumbnail({transaction}: EReceiptThumbnailProps) { styles.overflowHidden, styles.alignItemsCenter, containerHeight && containerHeight < variables.eReceiptThumnailCenterReceiptBreakpoint ? styles.justifyContentCenter : {}, + borderRadius && {borderRadius}, ]} onLayout={onContainerLayout} > diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 7c5fde88f600..7317869cf5d3 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -22,7 +22,10 @@ function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumnailImag if (isEReceipt || isThumbnail) { return ( - + ); } From d4abf53440ba81bca0752b5b6836a787829d50b6 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sat, 20 Jan 2024 23:01:52 +0300 Subject: [PATCH 007/218] type fix --- src/components/EReceiptThumbnail.tsx | 4 ++-- src/components/ReceiptImage.tsx | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index b677a60cc02c..a15f0a2d17bc 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -18,13 +18,13 @@ import Image from './Image'; type EReceiptThumbnailOnyxProps = { transaction: OnyxEntry; - borderRadius?: number; }; type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { /** TransactionID of the transaction this EReceipt corresponds to. It's used by withOnyx HOC */ // eslint-disable-next-line react/no-unused-prop-types transactionID: string; + borderRadius?: number; }; const backgroundImages = { @@ -84,7 +84,7 @@ function EReceiptThumbnail({transaction, borderRadius}: EReceiptThumbnailProps) styles.overflowHidden, styles.alignItemsCenter, containerHeight && containerHeight < variables.eReceiptThumnailCenterReceiptBreakpoint ? styles.justifyContentCenter : {}, - borderRadius && {borderRadius}, + borderRadius ? {borderRadius} : {}, ]} onLayout={onContainerLayout} > diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 7317869cf5d3..2c78337c725a 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import type {ImageStyle, StyleProp} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import EReceiptThumbnail from './EReceiptThumbnail'; import Image from './Image'; import ThumbnailImage from './ThumbnailImage'; +type Style = {height: number; borderRadius: number; margin: number}; + type ReceiptImageProps = { transactionID?: string; isThumbnail?: boolean; @@ -13,7 +14,7 @@ type ReceiptImageProps = { isEReceipt?: boolean; source?: string; isAuthTokenRequired?: boolean; - style?: StyleProp; + style?: Style; }; function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumnailImage = false, isEReceipt = false, source, isAuthTokenRequired, style}: ReceiptImageProps) { From 63b41f664bb31b0bfe309521f18982f65abf2024 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sat, 20 Jan 2024 23:28:32 +0300 Subject: [PATCH 008/218] minor fix --- src/components/DistanceEReceipt.js | 1 - src/components/ReportActionItem/ReportActionItemImage.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 8593cdcb5b93..ead2e2487848 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -69,7 +69,6 @@ function DistanceEReceipt({transaction}) { ) : ( )} diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index b9a98777eb53..d7d2514352c7 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -56,7 +56,7 @@ function ReportActionItemImage({thumbnail, isThumbnail, image, enablePreviewModa } else if ((thumbnail ?? isThumbnail) && !isLocalFile && !Str.isPDF(imageSource)) { propsObj = thumbnailSource ? {shouldUseThumnailImage: true, source: thumbnailSource} : {isThumbnail: true, transactionID: transaction?.transactionID}; } else { - propsObj = {isThumbnail, source: thumbnail ?? image}; + propsObj = {isThumbnail, transactionID: transaction?.transactionID, source: thumbnail ?? image}; } if (enablePreviewModal) { From 404fd8c39c0b19cff84e3c634e715c397e0d2240 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sat, 20 Jan 2024 23:54:15 +0300 Subject: [PATCH 009/218] passed transaction prop --- src/components/DistanceEReceipt.js | 2 +- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- src/components/ReportActionItem/ReportPreview.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index ead2e2487848..e322ccd28f4d 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -34,7 +34,7 @@ function DistanceEReceipt({transaction}) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {thumbnail, isThumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; + const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : translate('common.tbd'); const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 9cb27e6fac4a..e0351e4e642f 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -178,7 +178,7 @@ function MoneyRequestPreview(props) { merchantOrDescription = requestMerchant.replace(CONST.REGEX.FIRST_SPACE, translate('common.tbd')); } - const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(props.transaction)] : []; + const receiptImages = hasReceipt ? [{...ReceiptUtils.getThumbnailAndImageURIs(props.transaction), transaction: props.transaction}] : []; const getSettledMessage = () => { if (isExpensifyCardTransaction) { diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 204c9b5e31d4..f448a227a1a6 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -173,7 +173,7 @@ function ReportPreview(props) { const isScanning = hasReceipts && areAllRequestsBeingSmartScanned; const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(props.iouReportID, props.transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); - const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); + const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction})); let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; if (TransactionUtils.isPartialMerchant(formattedMerchant)) { formattedMerchant = null; From 9d38d933207f225f9667a15cc424003c59cc8099 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Wed, 24 Jan 2024 00:13:34 +0300 Subject: [PATCH 010/218] updated attachement modal to consider thumnail display --- src/components/AttachmentModal.js | 6 +++++- src/components/ReportActionItem/ReportActionItemImage.tsx | 1 + src/libs/ReceiptUtils.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 346ff19987ef..2fd8f3b30170 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -49,6 +49,9 @@ const propTypes = { /** Optional source (URL, SVG function) for the image shown. If not passed in via props must be specified when modal is opened. */ source: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), + /** Whether thumnail should be display */ + isThumbnail: PropTypes.bool, + /** Optional callback to fire when we want to preview an image and approve it for use. */ onConfirm: PropTypes.func, @@ -101,6 +104,7 @@ const propTypes = { const defaultProps = { source: '', + isThumbnail: false, onConfirm: null, defaultOpen: false, originalFileName: '', @@ -471,7 +475,7 @@ function AttachmentModal(props) { setDownloadButtonVisibility={setDownloadButtonVisibility} /> ) : ( - Boolean(sourceForAttachmentView) && + (Boolean(sourceForAttachmentView) || props.isThumbnail) && shouldLoadAttachment && ( Date: Wed, 24 Jan 2024 00:15:27 +0300 Subject: [PATCH 011/218] minor fix --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index b82f3eaf2f0f..1ece4679d065 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -643,7 +643,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ )} - {receiptImage || receiptThumbnail ? ( + {receiptImage || receiptThumbnail || isThumbnail ? ( Date: Wed, 24 Jan 2024 00:17:23 +0300 Subject: [PATCH 012/218] minor revert --- src/libs/ReceiptUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 18d56075844c..70925b0d42c3 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -44,7 +44,7 @@ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string const isLocalFile = typeof path === 'number' || path.startsWith('blob:') || path.startsWith('file:') || path.startsWith('/'); - return {isThumbnail: true, isLocalFile}; + return {isThumbnail: true, image: path, isLocalFile}; } // eslint-disable-next-line import/prefer-default-export From dc760ca13e718edbcbb3b768b247c608c1d8f7c9 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Wed, 24 Jan 2024 00:18:48 +0300 Subject: [PATCH 013/218] small fix --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 1ece4679d065..b82f3eaf2f0f 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -643,7 +643,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ )} - {receiptImage || receiptThumbnail || isThumbnail ? ( + {receiptImage || receiptThumbnail ? ( Date: Wed, 24 Jan 2024 17:53:31 +0300 Subject: [PATCH 014/218] fix on thumbnail display logic --- src/components/AttachmentModal.js | 6 +----- src/components/MoneyRequestConfirmationList.js | 15 ++++++++++----- .../ReportActionItem/ReportActionItemImage.tsx | 1 - src/libs/ReceiptUtils.ts | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 2fd8f3b30170..346ff19987ef 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -49,9 +49,6 @@ const propTypes = { /** Optional source (URL, SVG function) for the image shown. If not passed in via props must be specified when modal is opened. */ source: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), - /** Whether thumnail should be display */ - isThumbnail: PropTypes.bool, - /** Optional callback to fire when we want to preview an image and approve it for use. */ onConfirm: PropTypes.func, @@ -104,7 +101,6 @@ const propTypes = { const defaultProps = { source: '', - isThumbnail: false, onConfirm: null, defaultOpen: false, originalFileName: '', @@ -475,7 +471,7 @@ function AttachmentModal(props) { setDownloadButtonVisibility={setDownloadButtonVisibility} /> ) : ( - (Boolean(sourceForAttachmentView) || props.isThumbnail) && + Boolean(sourceForAttachmentView) && shouldLoadAttachment && ( )} {receiptImage || receiptThumbnail ? ( - Date: Wed, 24 Jan 2024 18:47:34 +0300 Subject: [PATCH 015/218] minor type fix --- src/libs/ReceiptUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index c7728889c865..509c88d5305d 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -3,7 +3,7 @@ import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; type ThumbnailAndImageURI = { - image?: string; + image: string; thumbnail?: string; transaction?: Transaction; isLocalFile?: boolean; From 0cfb3cba8a164fceafd8e9b534b3320a49760bdf Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Thu, 25 Jan 2024 20:56:52 +0300 Subject: [PATCH 016/218] Added file type label support for eThumbnail --- src/components/EReceiptThumbnail.tsx | 8 ++++++-- src/components/MoneyRequestConfirmationList.js | 2 ++ ...poraryForRefactorRequestConfirmationList.js | 2 ++ src/components/ReceiptImage.tsx | 5 ++++- .../ReportActionItem/MoneyRequestView.js | 1 + .../ReportActionItem/ReportActionItemImage.tsx | 18 +++++++++++++++--- .../ReportActionItemImages.tsx | 15 ++++----------- src/libs/ReceiptUtils.ts | 8 ++++++-- 8 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index a15f0a2d17bc..75f82b0f0e26 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -15,6 +15,7 @@ import * as eReceiptBGs from './Icon/EReceiptBGs'; import * as Expensicons from './Icon/Expensicons'; import * as MCCIcons from './Icon/MCCIcons'; import Image from './Image'; +import Text from './Text'; type EReceiptThumbnailOnyxProps = { transaction: OnyxEntry; @@ -25,6 +26,8 @@ type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types transactionID: string; borderRadius?: number; + fileExtension?: string; + isThumbnail?: boolean; }; const backgroundImages = { @@ -36,7 +39,7 @@ const backgroundImages = { [CONST.ERECEIPT_COLORS.PINK]: eReceiptBGs.EReceiptBG_Pink, }; -function EReceiptThumbnail({transaction, borderRadius}: EReceiptThumbnailProps) { +function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnail = false}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -102,7 +105,8 @@ function EReceiptThumbnail({transaction, borderRadius}: EReceiptThumbnailProps) fill={secondaryColor} additionalStyles={[styles.fullScreen]} /> - {MCCIcon ? ( + {isThumbnail && fileExtension && {fileExtension.toUpperCase()}} + {MCCIcon && !isThumbnail ? ( ) : ( // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index b82f3eaf2f0f..aea3d20f0b00 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -618,6 +618,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ image: receiptImage, thumbnail: receiptThumbnail, isThumbnail, + fileExtension, } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; return ( ) : ( // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 2c78337c725a..bf4b0ef832fc 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -15,9 +15,10 @@ type ReceiptImageProps = { source?: string; isAuthTokenRequired?: boolean; style?: Style; + fileExtension?: string; }; -function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumnailImage = false, isEReceipt = false, source, isAuthTokenRequired, style}: ReceiptImageProps) { +function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumnailImage = false, isEReceipt = false, source, isAuthTokenRequired, style, fileExtension}: ReceiptImageProps) { const styles = useThemeStyles(); if (isEReceipt || isThumbnail) { @@ -26,6 +27,8 @@ function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumnailImag ); diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index e2ebc701523d..73c478ca0164 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -237,6 +237,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate - {shownImages.map(({thumbnail, isThumbnail, image, transaction, isLocalFile}, index) => { + {shownImages.map(({thumbnail, isThumbnail, image, transaction, isLocalFile, fileExtension}, index) => { const isLastImage = index === numberOfShownImages - 1; // Show a border to separate multiple images. Shown to the right for each except the last. @@ -85,6 +77,7 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report > type === fileExtension), image: path, isLocalFile}; } // eslint-disable-next-line import/prefer-default-export export {getThumbnailAndImageURIs}; +export type {ThumbnailAndImageURI}; From c059ed1b78e9af5fa69bae467aaf682a17989625 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Thu, 25 Jan 2024 21:15:53 +0300 Subject: [PATCH 017/218] pass transactionID for confirmation --- src/components/MoneyRequestConfirmationList.js | 1 + .../MoneyTemporaryForRefactorRequestConfirmationList.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 5f43e6a83204..ed39b6256dc4 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -620,6 +620,7 @@ function MoneyRequestConfirmationList(props) { // So if we have a thumbnail, it means we're retrieving the image from the server isAuthTokenRequired={!_.isEmpty(receiptThumbnail)} fileExtension={fileExtension} + transactionID={props.transactionID || (props.transaction && props.transaction.transactionID)} /> ) : ( // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index aea3d20f0b00..a47ddfc3843b 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -653,6 +653,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // but we don't need it to load the blob:// or file:// image when starting a money request / split bill // So if we have a thumbnail, it means we're retrieving the image from the server isAuthTokenRequired={!_.isEmpty(receiptThumbnail)} + transactionID={transaction && transaction.transactionID} fileExtension={fileExtension} /> ) : ( From 7008a5d10307b64093b65440a675d2e3f0325393 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 29 Jan 2024 16:36:34 +0300 Subject: [PATCH 018/218] minor fix --- src/components/DistanceEReceipt.js | 2 +- src/components/ReceiptImage.tsx | 6 +++--- src/components/ReportActionItem/ReportActionItemImage.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index e322ccd28f4d..a42b4cbe42db 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -69,7 +69,7 @@ function DistanceEReceipt({transaction}) { ) : ( )} diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index bf4b0ef832fc..d04830bebb36 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -10,7 +10,7 @@ type Style = {height: number; borderRadius: number; margin: number}; type ReceiptImageProps = { transactionID?: string; isThumbnail?: boolean; - shouldUseThumnailImage?: boolean; + shouldUseThumbnailImage?: boolean; isEReceipt?: boolean; source?: string; isAuthTokenRequired?: boolean; @@ -18,7 +18,7 @@ type ReceiptImageProps = { fileExtension?: string; }; -function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumnailImage = false, isEReceipt = false, source, isAuthTokenRequired, style, fileExtension}: ReceiptImageProps) { +function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumbnailImage = false, isEReceipt = false, source, isAuthTokenRequired, style, fileExtension}: ReceiptImageProps) { const styles = useThemeStyles(); if (isEReceipt || isThumbnail) { @@ -34,7 +34,7 @@ function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumnailImag ); } - if (shouldUseThumnailImage) { + if (shouldUseThumbnailImage) { return ( Date: Mon, 29 Jan 2024 17:11:24 +0300 Subject: [PATCH 019/218] fix unnecessary condition --- src/components/ReportActionItem/ReportActionItemImage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index a63b491c18a5..435ff20a6f55 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -65,8 +65,8 @@ function ReportActionItemImage({ if (isEReceipt) { propsObj = {isEReceipt: true, transactionID: transaction.transactionID}; - } else if ((thumbnail ?? isThumbnail) && !isLocalFile && !Str.isPDF(imageSource)) { - propsObj = thumbnailSource ? {shouldUseThumbnailImage: true, source: thumbnailSource} : {isThumbnail: true, fileExtension, transactionID: transaction?.transactionID}; + } else if (thumbnail && !isLocalFile && !Str.isPDF(imageSource)) { + propsObj = {shouldUseThumbnailImage: true, source: thumbnailSource}; } else { propsObj = {isThumbnail, fileExtension, transactionID: transaction?.transactionID, source: thumbnail ?? image}; } From 3f019a51c6d2b6b4c19155e0084a7e814b82fb0d Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 29 Jan 2024 18:16:46 +0300 Subject: [PATCH 020/218] update to static icon layout --- src/components/EReceiptThumbnail.tsx | 11 ++++++----- src/components/ReceiptImage.tsx | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 75f82b0f0e26..efef40b6f7bb 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -28,6 +28,7 @@ type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { borderRadius?: number; fileExtension?: string; isThumbnail?: boolean; + useStaticIconLayout?: boolean; }; const backgroundImages = { @@ -39,7 +40,7 @@ const backgroundImages = { [CONST.ERECEIPT_COLORS.PINK]: eReceiptBGs.EReceiptBG_Pink, }; -function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnail = false}: EReceiptThumbnailProps) { +function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnail = false, useStaticIconLayout = false}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -69,11 +70,11 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnai let receiptIconHeight: number = variables.eReceiptIconHeight; let receiptMCCSize: number = variables.eReceiptMCCHeightWidth; - if (isSmall) { + if (isSmall && !useStaticIconLayout) { receiptIconWidth = variables.eReceiptIconWidthSmall; receiptIconHeight = variables.eReceiptIconHeightSmall; receiptMCCSize = variables.eReceiptMCCHeightWidthSmall; - } else if (isMedium) { + } else if (isMedium || useStaticIconLayout) { receiptIconWidth = variables.eReceiptIconWidthMedium; receiptIconHeight = variables.eReceiptIconHeightMedium; receiptMCCSize = variables.eReceiptMCCHeightWidthMedium; @@ -86,10 +87,10 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnai primaryColor ? StyleUtils.getBackgroundColorStyle(primaryColor) : {}, styles.overflowHidden, styles.alignItemsCenter, - containerHeight && containerHeight < variables.eReceiptThumnailCenterReceiptBreakpoint ? styles.justifyContentCenter : {}, + useStaticIconLayout || (containerHeight && containerHeight < variables.eReceiptThumnailCenterReceiptBreakpoint) ? styles.justifyContentCenter : {}, borderRadius ? {borderRadius} : {}, ]} - onLayout={onContainerLayout} + onLayout={useStaticIconLayout ? undefined : onContainerLayout} > ); From 6d7481c27b0ceed08d726298c7c30ef684f9e130 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Wed, 31 Jan 2024 21:32:48 +0300 Subject: [PATCH 021/218] changed to isStaticIconLayout --- src/components/EReceiptThumbnail.tsx | 12 ++++++------ src/components/ReceiptImage.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index efef40b6f7bb..23429fe35d16 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -28,7 +28,7 @@ type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { borderRadius?: number; fileExtension?: string; isThumbnail?: boolean; - useStaticIconLayout?: boolean; + isStaticIconLayout?: boolean; }; const backgroundImages = { @@ -40,7 +40,7 @@ const backgroundImages = { [CONST.ERECEIPT_COLORS.PINK]: eReceiptBGs.EReceiptBG_Pink, }; -function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnail = false, useStaticIconLayout = false}: EReceiptThumbnailProps) { +function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnail = false, isStaticIconLayout = false}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -70,11 +70,11 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnai let receiptIconHeight: number = variables.eReceiptIconHeight; let receiptMCCSize: number = variables.eReceiptMCCHeightWidth; - if (isSmall && !useStaticIconLayout) { + if (isSmall && !isStaticIconLayout) { receiptIconWidth = variables.eReceiptIconWidthSmall; receiptIconHeight = variables.eReceiptIconHeightSmall; receiptMCCSize = variables.eReceiptMCCHeightWidthSmall; - } else if (isMedium || useStaticIconLayout) { + } else if (isMedium || isStaticIconLayout) { receiptIconWidth = variables.eReceiptIconWidthMedium; receiptIconHeight = variables.eReceiptIconHeightMedium; receiptMCCSize = variables.eReceiptMCCHeightWidthMedium; @@ -87,10 +87,10 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnai primaryColor ? StyleUtils.getBackgroundColorStyle(primaryColor) : {}, styles.overflowHidden, styles.alignItemsCenter, - useStaticIconLayout || (containerHeight && containerHeight < variables.eReceiptThumnailCenterReceiptBreakpoint) ? styles.justifyContentCenter : {}, + isStaticIconLayout || (containerHeight && containerHeight < variables.eReceiptThumnailCenterReceiptBreakpoint) ? styles.justifyContentCenter : {}, borderRadius ? {borderRadius} : {}, ]} - onLayout={useStaticIconLayout ? undefined : onContainerLayout} + onLayout={isStaticIconLayout ? undefined : onContainerLayout} > ); From e61a6c7dffbd70f02df2faa72ad7f6c7a267c3de Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Wed, 31 Jan 2024 21:34:26 +0300 Subject: [PATCH 022/218] typescript fix --- src/components/ReportActionItem/ReportActionItemImages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx index 69bb6c592f54..e1abcb54adbb 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.tsx +++ b/src/components/ReportActionItem/ReportActionItemImages.tsx @@ -72,7 +72,7 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {}; return ( Date: Wed, 31 Jan 2024 22:05:51 +0300 Subject: [PATCH 023/218] set label text black --- src/components/EReceiptThumbnail.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 23429fe35d16..708b1707716f 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -6,6 +6,7 @@ import {withOnyx} from 'react-native-onyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; +import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -106,7 +107,7 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnai fill={secondaryColor} additionalStyles={[styles.fullScreen]} /> - {isThumbnail && fileExtension && {fileExtension.toUpperCase()}} + {isThumbnail && fileExtension && {fileExtension.toUpperCase()}} {MCCIcon && !isThumbnail ? ( Date: Thu, 1 Feb 2024 14:44:54 +0300 Subject: [PATCH 024/218] removed inline styles --- src/components/EReceiptThumbnail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 708b1707716f..fa2607965b57 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -107,7 +107,7 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnai fill={secondaryColor} additionalStyles={[styles.fullScreen]} /> - {isThumbnail && fileExtension && {fileExtension.toUpperCase()}} + {isThumbnail && fileExtension && {fileExtension.toUpperCase()}} {MCCIcon && !isThumbnail ? ( Date: Thu, 1 Feb 2024 14:51:07 +0300 Subject: [PATCH 025/218] fix on comment --- src/libs/ReceiptUtils.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index bf153118ecfd..44e054b4edc7 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -7,7 +7,7 @@ import type {ReceiptError} from '@src/types/onyx/Transaction'; import {splitExtensionFromFileName} from './fileDownload/FileUtils'; type ThumbnailAndImageURI = { - image: string; + image?: string; thumbnail?: string; transaction?: Transaction; isLocalFile?: boolean; @@ -23,14 +23,13 @@ type ThumbnailAndImageURI = { * @param receiptFileName */ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { - // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg + if (Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { + return {isThumbnail: true, isLocalFile: true}; + } // If there're errors, we need to display them in preview. We can store many files in errors, but we just need to get the last one const errors = _.findLast(transaction.errors) as ReceiptError | undefined; + // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg const path = errors?.source ?? transaction?.receipt?.source ?? receiptPath ?? ''; - if (Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { - return {isThumbnail: true, image: path, isLocalFile: true}; - } - // filename of uploaded image or last part of remote URI const filename = errors?.filename ?? transaction?.filename ?? receiptFileName ?? ''; const isReceiptImage = Str.isImage(filename); From 73f6495d8cf1d906e190a6461bf94aaa23007fd5 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Thu, 1 Feb 2024 16:31:38 +0300 Subject: [PATCH 026/218] fixed pending waypoints thumbnail display --- src/components/AttachmentModal.tsx | 6 ++---- src/components/ReportActionItem/ReportActionItemImage.tsx | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index f3e8ed316c52..ce1e3c99ac82 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -519,10 +519,8 @@ function AttachmentModal({ setDownloadButtonVisibility={setDownloadButtonVisibility} /> ) : ( - !!sourceForAttachmentView && - shouldLoadAttachment && - !isLoading && - !shouldShowNotFoundPage && ( + ((TransactionUtils.isDistanceRequest(transaction) && Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) || + (!!sourceForAttachmentView && shouldLoadAttachment && !isLoading && !shouldShowNotFoundPage)) && ( Date: Thu, 1 Feb 2024 16:46:16 +0300 Subject: [PATCH 027/218] simplified condition --- src/components/AttachmentModal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index ce1e3c99ac82..d355c6fc98bb 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -519,8 +519,10 @@ function AttachmentModal({ setDownloadButtonVisibility={setDownloadButtonVisibility} /> ) : ( - ((TransactionUtils.isDistanceRequest(transaction) && Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) || - (!!sourceForAttachmentView && shouldLoadAttachment && !isLoading && !shouldShowNotFoundPage)) && ( + (!!sourceForAttachmentView || Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) && + shouldLoadAttachment && + !isLoading && + !shouldShowNotFoundPage && ( Date: Thu, 1 Feb 2024 19:33:12 +0300 Subject: [PATCH 028/218] changed to isReceiptThumbnail prop --- src/components/EReceiptThumbnail.tsx | 19 ++++++++++--------- src/components/ReceiptImage.tsx | 3 +-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index fa2607965b57..4ccb6cfc7243 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -28,8 +28,9 @@ type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { transactionID: string; borderRadius?: number; fileExtension?: string; - isThumbnail?: boolean; - isStaticIconLayout?: boolean; + + /** Whether it is a receipt thumbnail we are displaying. */ + isReceiptThumbnail?: boolean; }; const backgroundImages = { @@ -41,7 +42,7 @@ const backgroundImages = { [CONST.ERECEIPT_COLORS.PINK]: eReceiptBGs.EReceiptBG_Pink, }; -function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnail = false, isStaticIconLayout = false}: EReceiptThumbnailProps) { +function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptThumbnail = false}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -71,11 +72,11 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnai let receiptIconHeight: number = variables.eReceiptIconHeight; let receiptMCCSize: number = variables.eReceiptMCCHeightWidth; - if (isSmall && !isStaticIconLayout) { + if (isSmall && !isReceiptThumbnail) { receiptIconWidth = variables.eReceiptIconWidthSmall; receiptIconHeight = variables.eReceiptIconHeightSmall; receiptMCCSize = variables.eReceiptMCCHeightWidthSmall; - } else if (isMedium || isStaticIconLayout) { + } else if (isMedium || isReceiptThumbnail) { receiptIconWidth = variables.eReceiptIconWidthMedium; receiptIconHeight = variables.eReceiptIconHeightMedium; receiptMCCSize = variables.eReceiptMCCHeightWidthMedium; @@ -88,10 +89,10 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isThumbnai primaryColor ? StyleUtils.getBackgroundColorStyle(primaryColor) : {}, styles.overflowHidden, styles.alignItemsCenter, - isStaticIconLayout || (containerHeight && containerHeight < variables.eReceiptThumnailCenterReceiptBreakpoint) ? styles.justifyContentCenter : {}, + isReceiptThumbnail || (containerHeight && containerHeight < variables.eReceiptThumnailCenterReceiptBreakpoint) ? styles.justifyContentCenter : {}, borderRadius ? {borderRadius} : {}, ]} - onLayout={isStaticIconLayout ? undefined : onContainerLayout} + onLayout={isReceiptThumbnail ? undefined : onContainerLayout} > - {isThumbnail && fileExtension && {fileExtension.toUpperCase()}} - {MCCIcon && !isThumbnail ? ( + {isReceiptThumbnail && fileExtension && {fileExtension.toUpperCase()}} + {MCCIcon && !isReceiptThumbnail ? ( ); From 1fb6284ff86d9b862605889ca0a9f4c9237616d2 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 5 Feb 2024 12:27:29 +0300 Subject: [PATCH 029/218] separated Ereceipt and thumbnail logic --- src/components/ReceiptImage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 0b7155ee6597..5f15ea675ff4 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -22,13 +22,13 @@ function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumbnailIma const styles = useThemeStyles(); if (isEReceipt || isThumbnail) { + const props = !isEReceipt && {borderRadius: style?.borderRadius, fileExtension, isReceiptThumbnail: true}; return ( ); From 75ca883259e66cd565bd1b27f300119cb2333c3d Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 5 Feb 2024 14:44:03 +0300 Subject: [PATCH 030/218] minor lint fix --- src/components/ReportActionItem/ReportActionItemImage.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index e32a67acb4fb..91f1e5310273 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -1,9 +1,6 @@ /* eslint-disable react/jsx-props-no-spreading */ import Str from 'expensify-common/lib/str'; import React from 'react'; -import type {ReactElement} from 'react'; -import type {ImageSourcePropType, ViewStyle} from 'react-native'; -import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import AttachmentModal from '@components/AttachmentModal'; import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; From ee4d711f9efcdafa5bbf63dabb587a3bba4a1e8f Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 5 Feb 2024 15:01:05 +0300 Subject: [PATCH 031/218] fix minor typescript issue --- src/components/ReportActionItem/ReportActionItemImage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 91f1e5310273..de12c15a0360 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -1,6 +1,7 @@ /* eslint-disable react/jsx-props-no-spreading */ import Str from 'expensify-common/lib/str'; import React from 'react'; +import type {ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import AttachmentModal from '@components/AttachmentModal'; import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; From 5f07c3970ce5a2889c428c3df65aeff7ba3b9f6d Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Tue, 6 Feb 2024 22:40:25 +0300 Subject: [PATCH 032/218] fix type --- src/types/onyx/Transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index b559346a48de..8a1395d6d931 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -38,7 +38,7 @@ type Geometry = { type?: GeometryType; }; -type ReceiptSource = string | number; +type ReceiptSource = string; type Receipt = { receiptID?: number; From 8f8572152fc1bde10cccbddf69885eda5cc685e9 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Tue, 6 Feb 2024 23:22:16 +0300 Subject: [PATCH 033/218] fix based on comments --- src/components/EReceiptThumbnail.tsx | 6 +++++- src/components/ReceiptImage.tsx | 15 +++++++++++++++ src/styles/variables.ts | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 4ccb6cfc7243..b5f709887478 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -26,7 +26,11 @@ type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { /** TransactionID of the transaction this EReceipt corresponds to. It's used by withOnyx HOC */ // eslint-disable-next-line react/no-unused-prop-types transactionID: string; + + /** Border radius to be applied on the parent view. */ borderRadius?: number; + + /** The file extension of the receipt that the preview thumbnail is being displayed for. */ fileExtension?: string; /** Whether it is a receipt thumbnail we are displaying. */ @@ -89,7 +93,7 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT primaryColor ? StyleUtils.getBackgroundColorStyle(primaryColor) : {}, styles.overflowHidden, styles.alignItemsCenter, - isReceiptThumbnail || (containerHeight && containerHeight < variables.eReceiptThumnailCenterReceiptBreakpoint) ? styles.justifyContentCenter : {}, + isReceiptThumbnail || (containerHeight && containerHeight < variables.eReceiptThumbnailCenterReceiptBreakpoint) ? styles.justifyContentCenter : {}, borderRadius ? {borderRadius} : {}, ]} onLayout={isReceiptThumbnail ? undefined : onContainerLayout} diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 5f15ea675ff4..9037fc29a4c0 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -8,13 +8,28 @@ import ThumbnailImage from './ThumbnailImage'; type Style = {height: number; borderRadius: number; margin: number}; type ReceiptImageProps = { + /** Transaction ID of the transaction the receipt belongs to. */ transactionID?: string; + + /** Whether it is receipt preview thumbnail we are displaying. */ isThumbnail?: boolean; + + /** Whether we should display the receipt with ThumbnailImage component */ shouldUseThumbnailImage?: boolean; + + /** Whether it is EReceipt */ isEReceipt?: boolean; + + /** Url of the receipt image */ source?: string; + + /** Whether the receipt image requires an authToken */ isAuthTokenRequired?: boolean; + + /** Any additional styles to apply */ style?: Style; + + /** The file extension of the receipt file */ fileExtension?: string; }; diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 296780abf0ae..d5e930fc5bbc 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -163,7 +163,7 @@ export default { addBankAccountLeftSpacing: 3, eReceiptThumbnailSmallBreakpoint: 110, eReceiptThumbnailMediumBreakpoint: 335, - eReceiptThumnailCenterReceiptBreakpoint: 200, + eReceiptThumbnailCenterReceiptBreakpoint: 200, eReceiptIconHeight: 100, eReceiptIconWidth: 72, eReceiptMCCHeightWidth: 40, From 7b14b86bc6c7dd2b9e42990ae51c156b91d444c8 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Wed, 7 Feb 2024 17:24:56 +0300 Subject: [PATCH 034/218] fix type --- src/components/ReceiptImage.tsx | 36 +++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 9037fc29a4c0..f962b64ab7b9 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -7,22 +7,36 @@ import ThumbnailImage from './ThumbnailImage'; type Style = {height: number; borderRadius: number; margin: number}; -type ReceiptImageProps = { - /** Transaction ID of the transaction the receipt belongs to. */ - transactionID?: string; +type ReceiptImageProps = ( + | { + /** Transaction ID of the transaction the receipt belongs to */ + transactionID: string; - /** Whether it is receipt preview thumbnail we are displaying. */ - isThumbnail?: boolean; + /** Whether it is EReceipt */ + isEReceipt: boolean; + /** Whether it is receipt preview thumbnail we are displaying */ + isThumbnail?: boolean; + + /** Url of the receipt image */ + source?: string; + } + | { + transactionID: string; + isEReceipt?: boolean; + isThumbnail: boolean; + source?: string; + } + | { + transactionID?: string; + isEReceipt?: boolean; + isThumbnail?: boolean; + source: string; + } +) & { /** Whether we should display the receipt with ThumbnailImage component */ shouldUseThumbnailImage?: boolean; - /** Whether it is EReceipt */ - isEReceipt?: boolean; - - /** Url of the receipt image */ - source?: string; - /** Whether the receipt image requires an authToken */ isAuthTokenRequired?: boolean; From 2a1ddf3a50c5a6f02961c24d91b9893ce5e9bddc Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Wed, 7 Feb 2024 17:28:42 +0300 Subject: [PATCH 035/218] fix typescript --- src/components/ReportActionItem/ReportActionItemImage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index de12c15a0360..a1692a635a19 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -74,7 +74,7 @@ function ReportActionItemImage({ } else if (thumbnail && !isLocalFile && !Str.isPDF(imageSource)) { propsObj = {shouldUseThumbnailImage: true, source: thumbnailSource}; } else { - propsObj = {isThumbnail, fileExtension, transactionID: transaction?.transactionID, source: thumbnail ?? image}; + propsObj = {isThumbnail, fileExtension, transactionID: transaction?.transactionID, source: thumbnail ?? image ?? ''}; } if (enablePreviewModal) { From 6dfc00ba39dd529445e962455bec2caf2631a0a6 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 12 Feb 2024 23:59:10 +0300 Subject: [PATCH 036/218] minor change --- src/components/ReceiptImage.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index f962b64ab7b9..1f1cba77a873 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import EReceiptThumbnail from './EReceiptThumbnail'; +import type {IconSize} from './EReceiptThumbnail'; import Image from './Image'; import ThumbnailImage from './ThumbnailImage'; @@ -20,6 +21,9 @@ type ReceiptImageProps = ( /** Url of the receipt image */ source?: string; + + /** number of images displayed in the same parent container */ + iconSize?: IconSize; } | { transactionID: string; From 4a086019a153bab8b3f7807cdfeba154a1a42466 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Tue, 13 Feb 2024 00:06:34 +0300 Subject: [PATCH 037/218] pass iconSize prop --- src/components/ReceiptImage.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 1f1cba77a873..4de2197b2b8c 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -21,9 +21,6 @@ type ReceiptImageProps = ( /** Url of the receipt image */ source?: string; - - /** number of images displayed in the same parent container */ - iconSize?: IconSize; } | { transactionID: string; @@ -49,13 +46,26 @@ type ReceiptImageProps = ( /** The file extension of the receipt file */ fileExtension?: string; + + /** number of images displayed in the same parent container */ + iconSize?: IconSize; }; -function ReceiptImage({transactionID, isThumbnail = false, shouldUseThumbnailImage = false, isEReceipt = false, source, isAuthTokenRequired, style, fileExtension}: ReceiptImageProps) { +function ReceiptImage({ + transactionID, + isThumbnail = false, + shouldUseThumbnailImage = false, + isEReceipt = false, + source, + isAuthTokenRequired, + style, + fileExtension, + iconSize, +}: ReceiptImageProps) { const styles = useThemeStyles(); if (isEReceipt || isThumbnail) { - const props = !isEReceipt && {borderRadius: style?.borderRadius, fileExtension, isReceiptThumbnail: true}; + const props = isEReceipt ? {iconSize} : {borderRadius: style?.borderRadius, fileExtension, isReceiptThumbnail: true}; return ( Date: Tue, 13 Feb 2024 22:07:54 +0300 Subject: [PATCH 038/218] use fetching waypoint function --- src/components/AttachmentModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index b1ec589a3d3e..6292dbed0a25 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -523,7 +523,7 @@ function AttachmentModal({ setDownloadButtonVisibility={setDownloadButtonVisibility} /> ) : ( - (!!sourceForAttachmentView || Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) && + (!!sourceForAttachmentView || TransactionUtils.isFetchingWaypointsFromServer(transaction)) && shouldLoadAttachment && !isLoading && !shouldShowNotFoundPage && ( From 32268d427b0fa9ab3fff4f8232a3ae9541948f48 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 15 Feb 2024 14:11:29 +0100 Subject: [PATCH 039/218] ref: move TagPicker to TS --- src/ONYXKEYS.ts | 2 +- .../ReportActionItem/MoneyRequestView.tsx | 3 +- src/components/TagPicker/index.js | 94 ------------ src/components/TagPicker/index.tsx | 135 ++++++++++++++++++ .../TagPicker/tagPickerPropTypes.js | 41 ------ src/libs/OptionsListUtils.ts | 36 ++--- src/libs/PolicyUtils.ts | 7 +- src/types/onyx/PolicyTag.ts | 7 +- 8 files changed, 160 insertions(+), 165 deletions(-) delete mode 100644 src/components/TagPicker/index.js create mode 100644 src/components/TagPicker/index.tsx delete mode 100644 src/components/TagPicker/tagPickerPropTypes.js diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5e41e08d0c78..d66182938465 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -429,7 +429,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_DRAFTS]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategories; - [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTags; + [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTagList; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 6b16f272e4c8..cc723b7288cb 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -55,7 +55,7 @@ type MoneyRequestViewOnyxPropsWithoutTransaction = { policyCategories: OnyxEntry; /** Collection of tags attached to a policy */ - policyTags: OnyxEntry; + policyTags: OnyxEntry; /** The expense report or iou report (only will have a value if this is a transaction thread) */ parentReport: OnyxEntry; @@ -155,7 +155,6 @@ function MoneyRequestView({ Navigation.dismissModal(); return; } - // @ts-expect-error: the type used across the app for policyTags is not what is returned by Onyx, PolicyTagList represents that, but existing policy tag utils need a refactor to fix this IOU.updateMoneyRequestBillable(transaction?.transactionID ?? '', report?.reportID, newBillable, policy, policyTags, policyCategories); Navigation.dismissModal(); }, diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.js deleted file mode 100644 index e258472eae93..000000000000 --- a/src/components/TagPicker/index.js +++ /dev/null @@ -1,94 +0,0 @@ -import lodashGet from 'lodash/get'; -import React, {useMemo, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import OptionsSelector from '@components/OptionsSelector'; -import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import {defaultProps, propTypes} from './tagPickerPropTypes'; - -function TagPicker({selectedTag, tag, policyTags, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption, insets, onSubmit}) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {translate} = useLocalize(); - const [searchValue, setSearchValue] = useState(''); - - const policyRecentlyUsedTagsList = lodashGet(policyRecentlyUsedTags, tag, []); - const policyTagList = PolicyUtils.getTagList(policyTags, tag); - const policyTagsCount = _.size(_.filter(policyTagList, (policyTag) => policyTag.enabled)); - const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD; - - const shouldShowTextInput = !isTagsCountBelowThreshold; - - const selectedOptions = useMemo(() => { - if (!selectedTag) { - return []; - } - - return [ - { - name: selectedTag, - enabled: true, - accountID: null, - }, - ]; - }, [selectedTag]); - - const enabledTags = useMemo(() => { - if (!shouldShowDisabledAndSelectedOption) { - return policyTagList; - } - const selectedNames = _.map(selectedOptions, (s) => s.name); - const tags = [...selectedOptions, ..._.filter(policyTagList, (policyTag) => policyTag.enabled && !selectedNames.includes(policyTag.name))]; - return tags; - }, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]); - - const sections = useMemo( - () => OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, false, {}, [], true, enabledTags, policyRecentlyUsedTagsList, false).tagOptions, - [searchValue, enabledTags, selectedOptions, policyRecentlyUsedTagsList], - ); - - const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(lodashGet(sections, '[0].data.length', 0) > 0, searchValue); - - const selectedOptionKey = lodashGet(_.filter(lodashGet(sections, '[0].data', []), (policyTag) => policyTag.searchText === selectedTag)[0], 'keyForList'); - - return ( - - ); -} - -TagPicker.displayName = 'TagPicker'; -TagPicker.propTypes = propTypes; -TagPicker.defaultProps = defaultProps; - -export default withOnyx({ - policyTags: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - }, - policyRecentlyUsedTags: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, - }, -})(TagPicker); diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx new file mode 100644 index 000000000000..48ef977a1225 --- /dev/null +++ b/src/components/TagPicker/index.tsx @@ -0,0 +1,135 @@ +import React, {useMemo, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import type {EdgeInsets} from 'react-native-safe-area-context'; +import OptionsSelector from '@components/OptionsSelector'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/types/onyx'; + +type SelectedTagOption = { + name: string; + enabled?: boolean; + accountID: number | null; +}; + +type TagPickerOnyxProps = { + /** Collection of tags attached to a policy */ + policyTags: OnyxEntry; + + /** List of recently used tags */ + policyRecentlyUsedTags: OnyxEntry; +}; + +type TagPickerProps = TagPickerOnyxProps & { + /** The policyID we are getting tags for */ + // It's used in withOnyx HOC. + // eslint-disable-next-line react/no-unused-prop-types + policyID: string; + + /** The selected tag of the money request */ + selectedTag: string; + + /** The name of tag list we are getting tags for */ + tag: string; + + /** Callback to submit the selected tag */ + onSubmit: () => void; + + /** + * Safe area insets required for reflecting the portion of the view, + * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. + */ + insets: EdgeInsets; + + /** Should show the selected option that is disabled? */ + shouldShowDisabledAndSelectedOption?: boolean; +}; + +function TagPicker({selectedTag, tag, policyTags, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, insets, onSubmit}: TagPickerProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const [searchValue, setSearchValue] = useState(''); + + const policyTagList = PolicyUtils.getTagList(policyTags, tag); + const policyTagsCount = Object.values(policyTagList).filter((policyTag) => policyTag.enabled).length; + const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD; + + const shouldShowTextInput = !isTagsCountBelowThreshold; + + const selectedOptions: SelectedTagOption[] = useMemo(() => { + if (!selectedTag) { + return []; + } + + return [ + { + name: selectedTag, + enabled: true, + accountID: null, + }, + ]; + }, [selectedTag]); + + const enabledTags: PolicyTags | Array = useMemo(() => { + if (!shouldShowDisabledAndSelectedOption) { + return policyTagList; + } + const selectedNames = selectedOptions.map((s) => s.name); + + return [...selectedOptions, ...Object.values(policyTagList).filter((policyTag) => policyTag.enabled && !selectedNames.includes(policyTag.name))]; + }, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]); + + const sections = useMemo(() => { + const policyRecentlyUsedTagsList = policyRecentlyUsedTags?.tag ?? []; + + return OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, false, {}, [], true, enabledTags, policyRecentlyUsedTagsList, false) + .tagOptions; + }, [searchValue, enabledTags, selectedOptions, policyRecentlyUsedTags?.tag]); + + const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList((sections?.[0]?.data?.length ?? 0) > 0, searchValue); + + const selectedOptionKey = sections[0]?.data?.filter((policyTag) => policyTag.searchText === selectedTag)?.[0]?.keyForList; + + return ( + + ); +} + +TagPicker.displayName = 'TagPicker'; + +export default withOnyx({ + policyTags: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + }, + policyRecentlyUsedTags: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, + }, +})(TagPicker); + +export type {SelectedTagOption}; diff --git a/src/components/TagPicker/tagPickerPropTypes.js b/src/components/TagPicker/tagPickerPropTypes.js deleted file mode 100644 index b98f7f6ef8e9..000000000000 --- a/src/components/TagPicker/tagPickerPropTypes.js +++ /dev/null @@ -1,41 +0,0 @@ -import PropTypes from 'prop-types'; -import tagPropTypes from '@components/tagPropTypes'; -import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes'; - -const propTypes = { - /** The policyID we are getting tags for */ - policyID: PropTypes.string.isRequired, - - /** The selected tag of the money request */ - selectedTag: PropTypes.string.isRequired, - - /** The name of tag list we are getting tags for */ - tag: PropTypes.string.isRequired, - - /** Callback to submit the selected tag */ - onSubmit: PropTypes.func.isRequired, - - /** - * Safe area insets required for reflecting the portion of the view, - * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. - */ - insets: safeAreaInsetPropTypes.isRequired, - - /* Onyx Props */ - /** Collection of tags attached to a policy */ - policyTags: tagPropTypes, - - /** List of recently used tags */ - policyRecentlyUsedTags: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)), - - /** Should show the selected option that is disabled? */ - shouldShowDisabledAndSelectedOption: PropTypes.bool, -}; - -const defaultProps = { - policyTags: {}, - policyRecentlyUsedTags: {}, - shouldShowDisabledAndSelectedOption: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 346cc71953e6..30b022862987 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -7,6 +7,7 @@ import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {SelectedTagOption} from '@components/TagPicker'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -19,6 +20,7 @@ import type { PolicyCategories, PolicyCategory, PolicyTag, + PolicyTags, Report, ReportAction, ReportActions, @@ -50,12 +52,6 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -type Tag = { - enabled: boolean; - name: string; - accountID: number | null; -}; - type Option = Partial; type PayeePersonalDetails = { @@ -106,7 +102,7 @@ type GetOptionsConfig = { categories?: PolicyCategories; recentlyUsedCategories?: string[]; includeTags?: boolean; - tags?: Record; + tags?: PolicyTags | Array; recentlyUsedTags?: string[]; canInviteUser?: boolean; includeSelectedOptions?: boolean; @@ -866,16 +862,8 @@ function sortCategories(categories: Record): Category[] { /** * Sorts tags alphabetically by name. */ -function sortTags(tags: Record | Tag[]) { - let sortedTags; - - if (Array.isArray(tags)) { - sortedTags = tags.sort((a, b) => a.name.localeCompare(b.name)); - } else { - sortedTags = Object.values(tags).sort((a, b) => a.name.localeCompare(b.name)); - } - - return sortedTags; +function sortTags(tags: Array) { + return tags.sort((a, b) => a.name?.localeCompare(b.name)); } /** @@ -1038,7 +1026,7 @@ function getCategoryListSections( * * @param tags - an initial tag array */ -function getTagsOptions(tags: Category[]): Option[] { +function getTagsOptions(tags: Array>): Option[] { return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); @@ -1055,7 +1043,13 @@ function getTagsOptions(tags: Category[]): Option[] { /** * Build the section list for tags */ -function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOptions: Category[], searchInputValue: string, maxRecentReportsToShow: number) { +function getTagListSections( + tags: Array, + recentlyUsedTags: string[], + selectedOptions: SelectedTagOption[], + searchInputValue: string, + maxRecentReportsToShow: number, +) { const tagSections = []; const sortedTags = sortTags(tags); const enabledTags = sortedTags.filter((tag) => tag.enabled); @@ -1367,7 +1361,7 @@ function getOptions( } if (includeTags) { - const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); + const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as SelectedTagOption[], searchInputValue, maxRecentReportsToShow); return { recentReports: [], @@ -1767,7 +1761,7 @@ function getFilteredOptions( categories: PolicyCategories = {}, recentlyUsedCategories: string[] = [], includeTags = false, - tags: Record = {}, + tags: PolicyTags | Array = {}, recentlyUsedTags: string[] = [], canInviteUser = true, includeSelectedOptions = false, diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 90dfa8fde339..d80bff0f43a3 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -3,7 +3,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, PolicyMembers, PolicyTag, PolicyTagList, PolicyTags} from '@src/types/onyx'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -155,13 +155,12 @@ function getIneligibleInvitees(policyMembers: OnyxEntry, personal /** * Gets the tag from policy tags, defaults to the first if no key is provided. */ -function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags): PolicyTag | undefined | EmptyObject { +function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags): PolicyTag | undefined | EmptyObject { if (isEmptyObject(policyTags)) { return {}; } const policyTagKey = tagKey ?? Object.keys(policyTags ?? {})[0]; - return policyTags?.[policyTagKey] ?? {}; } @@ -181,7 +180,7 @@ function getTagListName(policyTags: OnyxEntry) { /** * Gets the tags of a policy for a specific key. Defaults to the first tag if no key is provided. */ -function getTagList(policyTags: OnyxCollection, tagKey: string) { +function getTagList(policyTags: OnyxEntry, tagKey: string) { if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index ff688419605d..33e6ef4f7bb1 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -3,14 +3,17 @@ type PolicyTag = { name: string; /** Flag that determines if a tag is active and able to be selected */ - enabled: boolean; + enabled?: boolean; /** "General Ledger code" that corresponds to this tag in an accounting system. Similar to an ID. */ // eslint-disable-next-line @typescript-eslint/naming-convention - 'GL Code': string; + 'GL Code'?: string; /** Nested tags */ tags: PolicyTags; + + /** Flag that determines if a tag is required */ + required: boolean; }; type PolicyTags = Record; From 0f157fce2d446b8ff270e00e1b8c2d44178765de Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Tue, 20 Feb 2024 23:18:37 +0300 Subject: [PATCH 040/218] fix type --- src/components/DistanceEReceipt.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx index ae6516edf266..b22d21ba8d8a 100644 --- a/src/components/DistanceEReceipt.tsx +++ b/src/components/DistanceEReceipt.tsx @@ -29,7 +29,7 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) { const thumbnail = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction).thumbnail : null; const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction) ?? {}; const formattedTransactionAmount = CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency); - const thumbnailSource = tryResolveUrlFromApiRoot((thumbnail as string) || ''); + const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? ''); const waypoints = useMemo(() => transaction?.comment?.waypoints ?? {}, [transaction?.comment?.waypoints]); const sortedWaypoints = useMemo( () => From e154fea0ed57c106935d2558b6b1d26cc738f8de Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 21 Feb 2024 13:41:22 +0100 Subject: [PATCH 041/218] fix: missing value --- src/components/TagPicker/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index 917351e68228..b553e183a37f 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -62,7 +62,7 @@ function TagPicker({selectedTag, tag, policyTags, tagIndex, policyRecentlyUsedTa const policyRecentlyUsedTagsList = useMemo(() => policyRecentlyUsedTags?.[tag] ?? [], [policyRecentlyUsedTags, tag]); const policyTagList = PolicyUtils.getTagList(policyTags, tagIndex); - const policyTagsCount = PolicyUtils.getCountOfEnabledTagsOfList(policyTagList); + const policyTagsCount = PolicyUtils.getCountOfEnabledTagsOfList(policyTagList.tags); const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD; const shouldShowTextInput = !isTagsCountBelowThreshold; From c44b4309b00d1f91d8220a162513b05493491b81 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 21 Feb 2024 18:09:50 +0300 Subject: [PATCH 042/218] add fallbackIcon --- src/components/ReceiptImage.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 4de2197b2b8c..f293d3b047d5 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import type IconAsset from '@src/types/utils/IconAsset'; import EReceiptThumbnail from './EReceiptThumbnail'; import type {IconSize} from './EReceiptThumbnail'; import Image from './Image'; @@ -49,6 +50,12 @@ type ReceiptImageProps = ( /** number of images displayed in the same parent container */ iconSize?: IconSize; + + /** If the image fails to load – show the provided fallback icon */ + fallbackIcon?: IconAsset; + + /** The size of the fallback icon */ + fallbackIconSize?: number; }; function ReceiptImage({ @@ -61,6 +68,8 @@ function ReceiptImage({ style, fileExtension, iconSize, + fallbackIcon, + fallbackIconSize, }: ReceiptImageProps) { const styles = useThemeStyles(); @@ -84,6 +93,8 @@ function ReceiptImage({ style={[styles.w100, styles.h100]} isAuthTokenRequired shouldDynamicallyResize={false} + fallbackIcon={fallbackIcon} + fallbackIconSize={fallbackIconSize} /> ); } From 7025bfa7b10a6c36d788f9804ac627d6156e7e67 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 21 Feb 2024 18:30:35 +0300 Subject: [PATCH 043/218] applied dynamic thumbnail icon size --- src/components/ReceiptImage.tsx | 3 ++- src/components/ReportActionItem/ReportActionItemImage.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index f293d3b047d5..cd70413b7a2e 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -74,11 +74,12 @@ function ReceiptImage({ const styles = useThemeStyles(); if (isEReceipt || isThumbnail) { - const props = isEReceipt ? {iconSize} : {borderRadius: style?.borderRadius, fileExtension, isReceiptThumbnail: true}; + const props = isThumbnail && {borderRadius: style?.borderRadius, fileExtension, isReceiptThumbnail: true}; return ( diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 0f6131a61c05..cc84abccd6d1 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -86,7 +86,13 @@ function ReportActionItemImage({ fallbackIconSize: isSingleImage ? variables.iconSizeSuperLarge : variables.iconSizeExtraLarge, }; } else { - propsObj = {isThumbnail, fileExtension, transactionID: transaction?.transactionID, source: thumbnail ?? image ?? ''}; + propsObj = { + isThumbnail, + ...(isThumbnail && {iconSize: isSingleImage ? 'medium' : ('small' as IconSize)}), + fileExtension, + transactionID: transaction?.transactionID, + source: thumbnail ?? image ?? '', + }; } if (enablePreviewModal) { From 9eb1edd18a8545774e4114ec3d35a6073be0e3ef Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Mon, 26 Feb 2024 12:22:43 +0300 Subject: [PATCH 044/218] use primary color for label text color --- src/components/EReceiptThumbnail.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 5093e2853e18..f6d5beb7e24d 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -105,7 +105,9 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT fill={secondaryColor} additionalStyles={[styles.fullScreen]} /> - {isReceiptThumbnail && fileExtension && {fileExtension.toUpperCase()}} + {isReceiptThumbnail && fileExtension && ( + {fileExtension.toUpperCase()} + )} {MCCIcon && !isReceiptThumbnail ? ( Date: Mon, 26 Feb 2024 15:26:54 +0100 Subject: [PATCH 045/218] feat: memoize SidebarLinksData --- src/pages/home/sidebar/SidebarLinksData.js | 41 ++++++++++++++-------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index c4cc0713c596..0c8125cae79f 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -2,7 +2,7 @@ import {deepEqual} from 'fast-equals'; import lodashGet from 'lodash/get'; import lodashMap from 'lodash/map'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -136,18 +136,14 @@ function SidebarLinksData({ const reportIDsRef = useRef(null); const isLoading = isLoadingApp; + + const optionItemsMemoized = useMemo( + () => SidebarUtils.getOrderedReportIDs(null, chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs), + [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs], + ); + const optionListItems = useMemo(() => { - const reportIDs = SidebarUtils.getOrderedReportIDs( - null, - chatReports, - betas, - policies, - priorityMode, - allReportActions, - transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - ); + const reportIDs = optionItemsMemoized; if (deepEqual(reportIDsRef.current, reportIDs)) { return reportIDsRef.current; @@ -160,7 +156,7 @@ function SidebarLinksData({ reportIDsRef.current = reportIDs; } return reportIDsRef.current || []; - }, [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, isLoading, network.isOffline, prevPriorityMode]); + }, [optionItemsMemoized, priorityMode, isLoading, network.isOffline, prevPriorityMode]); // We need to make sure the current report is in the list of reports, but we do not want // to have to re-generate the list every time the currentReportID changes. To do that @@ -334,4 +330,21 @@ export default compose( initialValue: {}, }, }), -)(SidebarLinksData); +)( + memo( + SidebarLinksData, + (prevProps, nextProps) => + _.isEqual(prevProps.chatReports, nextProps.chatReports) && + _.isEqual(prevProps.allReportActions, nextProps.allReportActions) && + prevProps.isLoadingApp === nextProps.isLoadingApp && + prevProps.priorityMode === nextProps.priorityMode && + _.isEqual(prevProps.betas, nextProps.betas) && + _.isEqual(prevProps.policies, nextProps.policies) && + prevProps.network.isOffline === nextProps.network.isOffline && + _.isEqual(prevProps.insets, nextProps.insets) && + prevProps.onLinkClick === nextProps.onLinkClick && + _.isEqual(prevProps.policyMembers, nextProps.policyMembers) && + _.isEqual(prevProps.transactionViolations, nextProps.transactionViolations) && + _.isEqual(prevProps.currentUserPersonalDetails, nextProps.currentUserPersonalDetails), + ), +); From 79d94952cafce2cc05726e4fe29a397346945c13 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 26 Feb 2024 14:45:58 +0000 Subject: [PATCH 046/218] refactor(typescript): migrate multiple tests and utils --- src/libs/OptionsListUtils.ts | 2 +- src/libs/ReportUtils.ts | 2 +- .../output/{markdown.js => markdown.ts} | 51 ++- tests/e2e/compare/types.ts | 57 ++++ tests/e2e/measure/math.ts | 11 +- tests/e2e/utils/logger.js | 10 +- ...erf-test.js => SidebarLinks.perf-test.tsx} | 22 +- .../{DateUtilsTest.js => DateUtilsTest.ts} | 55 ++-- ...stUtilsTest.js => OptionsListUtilsTest.ts} | 307 ++++++++++++------ ...{ReportUtilsTest.js => ReportUtilsTest.ts} | 230 +++++++------ 10 files changed, 467 insertions(+), 280 deletions(-) rename tests/e2e/compare/output/{markdown.js => markdown.ts} (59%) create mode 100644 tests/e2e/compare/types.ts rename tests/perf-test/{SidebarLinks.perf-test.js => SidebarLinks.perf-test.tsx} (85%) rename tests/unit/{DateUtilsTest.js => DateUtilsTest.ts} (85%) rename tests/unit/{OptionsListUtilsTest.js => OptionsListUtilsTest.ts} (89%) rename tests/unit/{ReportUtilsTest.js => ReportUtilsTest.ts} (80%) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 97b4fc0144c8..e4ec9da4dc12 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2048,4 +2048,4 @@ export { getShareLogOptions, }; -export type {MemberForList, CategorySection, GetOptions}; +export type {MemberForList, CategorySection, GetOptions, PolicyTaxRateWithDefault, Tag}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ae6e02e70d29..d981309f0285 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -938,7 +938,7 @@ function filterReportsByPolicyIDAndMemberAccountIDs(reports: Report[], policyMem /** * Given an array of reports, return them sorted by the last read timestamp. */ -function sortReportsByLastRead(reports: Report[], reportMetadata: OnyxCollection): Array> { +function sortReportsByLastRead(reports: Array>, reportMetadata: OnyxCollection): Array> { return reports .filter((report) => !!report?.reportID && !!(reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`]?.lastVisitTime ?? report?.lastReadTime)) .sort((a, b) => { diff --git a/tests/e2e/compare/output/markdown.js b/tests/e2e/compare/output/markdown.ts similarity index 59% rename from tests/e2e/compare/output/markdown.js rename to tests/e2e/compare/output/markdown.ts index 119830a5bb2c..07cf9897e570 100644 --- a/tests/e2e/compare/output/markdown.js +++ b/tests/e2e/compare/output/markdown.ts @@ -1,35 +1,34 @@ // From: https://raw.githubusercontent.com/callstack/reassure/main/packages/reassure-compare/src/output/markdown.ts import fs from 'node:fs/promises'; import path from 'path'; -import _ from 'underscore'; import * as Logger from '../../utils/logger'; +import type {AddedEntry, CompareEntry, CompareResult, PerformanceEntry, RemovedEntry} from '../types'; import * as format from './format'; import markdownTable from './markdownTable'; const tableHeader = ['Name', 'Duration']; -const collapsibleSection = (title, content) => `
\n${title}\n\n${content}\n
\n\n`; +const collapsibleSection = (title: string, content: string): string => `
\n${title}\n\n${content}\n
\n\n`; -const buildDurationDetails = (title, entry) => { +const buildDurationDetails = (title: string, entry: PerformanceEntry): string => { const relativeStdev = entry.stdev / entry.mean; - return _.filter( - [ - `**${title}**`, - `Mean: ${format.formatDuration(entry.mean)}`, - `Stdev: ${format.formatDuration(entry.stdev)} (${format.formatPercent(relativeStdev)})`, - entry.entries ? `Runs: ${entry.entries.join(' ')}` : '', - ], - Boolean, - ).join('
'); + return [ + `**${title}**`, + `Mean: ${format.formatDuration(entry.mean)}`, + `Stdev: ${format.formatDuration(entry.stdev)} (${format.formatPercent(relativeStdev)})`, + entry.entries ? `Runs: ${entry.entries.join(' ')}` : '', + ] + .filter(Boolean) + .join('
'); }; -const buildDurationDetailsEntry = (entry) => - _.filter(['baseline' in entry ? buildDurationDetails('Baseline', entry.baseline) : '', 'current' in entry ? buildDurationDetails('Current', entry.current) : ''], Boolean).join( - '

', - ); +const buildDurationDetailsEntry = (entry: CompareEntry | AddedEntry | RemovedEntry): string => + ['baseline' in entry ? buildDurationDetails('Baseline', entry.baseline) : '', 'current' in entry ? buildDurationDetails('Current', entry.current) : ''] + .filter(Boolean) + .join('

'); -const formatEntryDuration = (entry) => { +const formatEntryDuration = (entry: CompareEntry | AddedEntry | RemovedEntry): string => { if ('baseline' in entry && 'current' in entry) { return format.formatDurationDiffChange(entry); } @@ -42,39 +41,39 @@ const formatEntryDuration = (entry) => { return ''; }; -const buildDetailsTable = (entries) => { +const buildDetailsTable = (entries: Array): string => { if (!entries.length) { return ''; } - const rows = _.map(entries, (entry) => [entry.name, buildDurationDetailsEntry(entry)]); + const rows = entries.map((entry) => [entry.name, buildDurationDetailsEntry(entry)]); const content = markdownTable([tableHeader, ...rows]); return collapsibleSection('Show details', content); }; -const buildSummaryTable = (entries, collapse = false) => { +const buildSummaryTable = (entries: Array, collapse = false): string => { if (!entries.length) { return '_There are no entries_'; } - const rows = _.map(entries, (entry) => [entry.name, formatEntryDuration(entry)]); + const rows = entries.map((entry) => [entry.name, formatEntryDuration(entry)]); const content = markdownTable([tableHeader, ...rows]); return collapse ? collapsibleSection('Show entries', content) : content; }; -const buildMarkdown = (data) => { +const buildMarkdown = (data: CompareResult): string => { let result = '## Performance Comparison Report 📊'; - if (data.errors && data.errors.length) { + if (data.errors?.length) { result += '\n\n### Errors\n'; data.errors.forEach((message) => { result += ` 1. 🛑 ${message}\n`; }); } - if (data.warnings && data.warnings.length) { + if (data.warnings?.length) { result += '\n\n### Warnings\n'; data.warnings.forEach((message) => { result += ` 1. 🟡 ${message}\n`; @@ -92,7 +91,7 @@ const buildMarkdown = (data) => { return result; }; -const writeToFile = (filePath, content) => +const writeToFile = (filePath: string, content: string): Promise => fs .writeFile(filePath, content) .then(() => { @@ -106,7 +105,7 @@ const writeToFile = (filePath, content) => throw error; }); -const writeToMarkdown = (filePath, data) => { +const writeToMarkdown = (filePath: string, data: CompareResult): Promise => { const markdown = buildMarkdown(data); return writeToFile(filePath, markdown).catch((error) => { console.error(error); diff --git a/tests/e2e/compare/types.ts b/tests/e2e/compare/types.ts new file mode 100644 index 000000000000..e0be36716977 --- /dev/null +++ b/tests/e2e/compare/types.ts @@ -0,0 +1,57 @@ +type Entries = number[]; + +/** Metadata information for performance results. */ + +/** Entry in the performance results file. */ +type PerformanceEntry = { + /** Number of times the measurement test was run. */ + runs: number; + + /** Arithmetic average of measured render/execution durations for each run. */ + mean: number; + + /** Standard deviation of measured render/execution durations for each run. */ + stdev: number; + + /** Array of measured render/execution durations for each run. */ + entries: Entries; +}; + +/** + * Compare entry for tests that have both baseline and current entry + */ +type CompareEntry = { + name: string; + current: PerformanceEntry; + baseline: PerformanceEntry; + durationDiff: number; + relativeDurationDiff: number; +}; + +/** + * Compare entry for tests that have only current entry + */ +type AddedEntry = { + name: string; + current: PerformanceEntry; +}; + +/** + * Compare entry for tests that have only baseline entry + */ +type RemovedEntry = { + name: string; + baseline: PerformanceEntry; +}; + +/** Output of compare function. */ +type CompareResult = { + significance: CompareEntry[]; + meaningless: CompareEntry[]; + added: AddedEntry[]; + removed: RemovedEntry[]; + errors?: string[]; + warnings?: string[]; +}; + +export type {Entries, PerformanceEntry, CompareEntry, AddedEntry, RemovedEntry, CompareResult}; diff --git a/tests/e2e/measure/math.ts b/tests/e2e/measure/math.ts index e1c0cb981a0c..b5e303af7ea7 100644 --- a/tests/e2e/measure/math.ts +++ b/tests/e2e/measure/math.ts @@ -1,11 +1,4 @@ -type Entries = number[]; - -type Stats = { - mean: number; - stdev: number; - runs: number; - entries: Entries; -}; +import type {Entries, PerformanceEntry} from '../compare/types'; const filterOutliersViaIQR = (data: Entries): Entries => { let q1; @@ -35,7 +28,7 @@ const std = (arr: Entries): number => { return Math.sqrt(arr.map((i) => (i - avg) ** 2).reduce((a, b) => a + b) / arr.length); }; -const getStats = (entries: Entries): Stats => { +const getStats = (entries: Entries): PerformanceEntry => { const cleanedEntries = filterOutliersViaIQR(entries); const meanDuration = mean(cleanedEntries); const stdevDuration = std(cleanedEntries); diff --git a/tests/e2e/utils/logger.js b/tests/e2e/utils/logger.js index d0770b7aa8e4..6a39b4c10328 100644 --- a/tests/e2e/utils/logger.js +++ b/tests/e2e/utils/logger.js @@ -66,12 +66,4 @@ const error = (...args) => { log(...lines); }; -module.exports = { - log, - info, - warn, - note, - error, - success, - writeToLogFile, -}; +export {log, info, warn, note, error, success, writeToLogFile}; diff --git a/tests/perf-test/SidebarLinks.perf-test.js b/tests/perf-test/SidebarLinks.perf-test.tsx similarity index 85% rename from tests/perf-test/SidebarLinks.perf-test.js rename to tests/perf-test/SidebarLinks.perf-test.tsx index 0b10718fd0c4..c19a34517f45 100644 --- a/tests/perf-test/SidebarLinks.perf-test.js +++ b/tests/perf-test/SidebarLinks.perf-test.tsx @@ -1,23 +1,23 @@ import {fireEvent, screen} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; import {measurePerformance} from 'reassure'; -import _ from 'underscore'; -import CONST from '../../src/CONST'; -import ONYXKEYS from '../../src/ONYXKEYS'; -import variables from '../../src/styles/variables'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; -jest.mock('../../src/libs/Permissions'); -jest.mock('../../src/hooks/usePermissions.ts'); -jest.mock('../../src/libs/Navigation/Navigation'); -jest.mock('../../src/components/Icon/Expensicons'); +jest.mock('@libs/Permissions'); +jest.mock('@hooks/usePermissions.ts'); +jest.mock('@libs/Navigation/Navigation'); +jest.mock('@components/Icon/Expensicons'); jest.mock('@react-navigation/native'); const getMockedReportsMap = (length = 100) => { - const mockReports = Array.from({length}, (__, i) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const mockReports = Array.from({length}, (_, i) => { const reportID = i + 1; const participants = [1, 2]; const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; @@ -26,7 +26,7 @@ const getMockedReportsMap = (length = 100) => { return {[reportKey]: report}; }); - return _.assign({}, ...mockReports); + return {...mockReports}; }; const mockedResponseMap = getMockedReportsMap(500); @@ -36,11 +36,9 @@ describe('SidebarLinks', () => { Onyx.init({ keys: ONYXKEYS, safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], - registerStorageEventListener: () => {}, }); Onyx.multiSet({ - [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS], [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.ts similarity index 85% rename from tests/unit/DateUtilsTest.js rename to tests/unit/DateUtilsTest.ts index a752eea1a990..8758050e6f62 100644 --- a/tests/unit/DateUtilsTest.js +++ b/tests/unit/DateUtilsTest.ts @@ -1,9 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import {addDays, addMinutes, format, setHours, setMinutes, subDays, subHours, subMinutes, subSeconds} from 'date-fns'; import {format as tzFormat, utcToZonedTime} from 'date-fns-tz'; import Onyx from 'react-native-onyx'; -import CONST from '../../src/CONST'; -import DateUtils from '../../src/libs/DateUtils'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import DateUtils from '@libs/DateUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; const LOCALE = CONST.LOCALES.EN; @@ -14,13 +16,14 @@ describe('DateUtils', () => { keys: ONYXKEYS, initialKeyStates: { [ONYXKEYS.SESSION]: {accountID: 999}, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: {999: {timezone: {selected: UTC}}}, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: {'999': {accountID: 999, timezone: {selected: 'Europe/London'}}}, }, }); return waitForBatchedUpdates(); }); afterEach(() => { + jest.restoreAllMocks(); jest.useRealTimers(); Onyx.clear(); }); @@ -53,32 +56,35 @@ describe('DateUtils', () => { }); it('should fallback to current date when getLocalDateFromDatetime is failing', () => { - const localDate = DateUtils.getLocalDateFromDatetime(LOCALE, undefined, 'InvalidTimezone'); + const localDate = DateUtils.getLocalDateFromDatetime(LOCALE, undefined, 'InvalidTimezone' as SelectedTimezone); expect(localDate.getTime()).not.toBeNaN(); }); it('should return the date in calendar time when calling datetimeToCalendarTime', () => { - const today = setMinutes(setHours(new Date(), 14), 32); + const today = setMinutes(setHours(new Date(), 14), 32).toString(); expect(DateUtils.datetimeToCalendarTime(LOCALE, today)).toBe('Today at 2:32 PM'); - const tomorrow = addDays(setMinutes(setHours(new Date(), 14), 32), 1); + const tomorrow = addDays(setMinutes(setHours(new Date(), 14), 32), 1).toString(); expect(DateUtils.datetimeToCalendarTime(LOCALE, tomorrow)).toBe('Tomorrow at 2:32 PM'); - const yesterday = setMinutes(setHours(subDays(new Date(), 1), 7), 43); + const yesterday = setMinutes(setHours(subDays(new Date(), 1), 7), 43).toString(); expect(DateUtils.datetimeToCalendarTime(LOCALE, yesterday)).toBe('Yesterday at 7:43 AM'); - const date = setMinutes(setHours(new Date('2022-11-05'), 10), 17); + const date = setMinutes(setHours(new Date('2022-11-05'), 10), 17).toString(); expect(DateUtils.datetimeToCalendarTime(LOCALE, date)).toBe('Nov 5, 2022 at 10:17 AM'); - const todayLowercaseDate = setMinutes(setHours(new Date(), 14), 32); + const todayLowercaseDate = setMinutes(setHours(new Date(), 14), 32).toString(); expect(DateUtils.datetimeToCalendarTime(LOCALE, todayLowercaseDate, false, undefined, true)).toBe('today at 2:32 PM'); }); it('should update timezone if automatic and selected timezone do not match', () => { - Intl.DateTimeFormat = jest.fn(() => ({ - resolvedOptions: () => ({timeZone: 'America/Chicago'}), - })); - Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {999: {timezone: {selected: UTC, automatic: true}}}).then(() => { + jest.spyOn(Intl, 'DateTimeFormat').mockImplementation( + () => + ({ + resolvedOptions: () => ({timeZone: 'America/Chicago'}), + } as Intl.DateTimeFormat), + ); + Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {'999': {accountID: 999, timezone: {selected: 'Europe/London', automatic: true}}}).then(() => { const result = DateUtils.getCurrentTimezone(); expect(result).toEqual({ selected: 'America/Chicago', @@ -88,10 +94,13 @@ describe('DateUtils', () => { }); it('should not update timezone if automatic and selected timezone match', () => { - Intl.DateTimeFormat = jest.fn(() => ({ - resolvedOptions: () => ({timeZone: UTC}), - })); - Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {999: {timezone: {selected: UTC, automatic: true}}}).then(() => { + jest.spyOn(Intl, 'DateTimeFormat').mockImplementation( + () => + ({ + resolvedOptions: () => ({timeZone: UTC}), + } as Intl.DateTimeFormat), + ); + Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {'999': {accountID: 999, timezone: {selected: 'Europe/London', automatic: true}}}).then(() => { const result = DateUtils.getCurrentTimezone(); expect(result).toEqual({ selected: UTC, @@ -102,7 +111,7 @@ describe('DateUtils', () => { it('canUpdateTimezone should return true when lastUpdatedTimezoneTime is more than 5 minutes ago', () => { // Use fake timers to control the current time - jest.useFakeTimers('modern'); + jest.useFakeTimers(); jest.setSystemTime(addMinutes(new Date(), 6)); const isUpdateTimezoneAllowed = DateUtils.canUpdateTimezone(); expect(isUpdateTimezoneAllowed).toBe(true); @@ -110,20 +119,20 @@ describe('DateUtils', () => { it('canUpdateTimezone should return false when lastUpdatedTimezoneTime is less than 5 minutes ago', () => { // Use fake timers to control the current time - jest.useFakeTimers('modern'); + jest.useFakeTimers(); jest.setSystemTime(addMinutes(new Date(), 4)); const isUpdateTimezoneAllowed = DateUtils.canUpdateTimezone(); expect(isUpdateTimezoneAllowed).toBe(false); }); it('should return the date in calendar time when calling datetimeToRelative', () => { - const aFewSecondsAgo = subSeconds(new Date(), 10); + const aFewSecondsAgo = subSeconds(new Date(), 10).toString(); expect(DateUtils.datetimeToRelative(LOCALE, aFewSecondsAgo)).toBe('less than a minute ago'); - const aMinuteAgo = subMinutes(new Date(), 1); + const aMinuteAgo = subMinutes(new Date(), 1).toString(); expect(DateUtils.datetimeToRelative(LOCALE, aMinuteAgo)).toBe('1 minute ago'); - const anHourAgo = subHours(new Date(), 1); + const anHourAgo = subHours(new Date(), 1).toString(); expect(DateUtils.datetimeToRelative(LOCALE, anHourAgo)).toBe('about 1 hour ago'); }); diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.ts similarity index 89% rename from tests/unit/OptionsListUtilsTest.js rename to tests/unit/OptionsListUtilsTest.ts index 00f1307ab59f..07dabf143110 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.ts @@ -1,30 +1,32 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import CONST from '../../src/CONST'; -import * as OptionsListUtils from '../../src/libs/OptionsListUtils'; -import * as ReportUtils from '../../src/libs/ReportUtils'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import CONST from '@src/CONST'; +import type {PolicyTaxRateWithDefault, Tag} from '@src/libs/OptionsListUtils'; +import * as OptionsListUtils from '@src/libs/OptionsListUtils'; +import * as ReportUtils from '@src/libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {PolicyCategories, Report} from '@src/types/onyx'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; describe('OptionsListUtils', () => { // Given a set of reports with both single participants and multiple participants some pinned and some not - const REPORTS = { - 1: { + const REPORTS: Record = { + '1': { lastReadTime: '2021-01-14 11:25:39.295', lastVisibleActionCreated: '2022-11-22 03:26:02.015', isPinned: false, - reportID: 1, + reportID: '1', participantAccountIDs: [2, 1], visibleChatMemberAccountIDs: [2, 1], reportName: 'Iron Man, Mister Fantastic', hasDraft: true, type: CONST.REPORT.TYPE.CHAT, }, - 2: { + '2': { lastReadTime: '2021-01-14 11:25:39.296', lastVisibleActionCreated: '2022-11-22 03:26:02.016', isPinned: false, - reportID: 2, + reportID: '2', participantAccountIDs: [3], visibleChatMemberAccountIDs: [3], reportName: 'Spider-Man', @@ -32,41 +34,41 @@ describe('OptionsListUtils', () => { }, // This is the only report we are pinning in this test - 3: { + '3': { lastReadTime: '2021-01-14 11:25:39.297', lastVisibleActionCreated: '2022-11-22 03:26:02.170', isPinned: true, - reportID: 3, + reportID: '3', participantAccountIDs: [1], visibleChatMemberAccountIDs: [1], reportName: 'Mister Fantastic', type: CONST.REPORT.TYPE.CHAT, }, - 4: { + '4': { lastReadTime: '2021-01-14 11:25:39.298', lastVisibleActionCreated: '2022-11-22 03:26:02.180', isPinned: false, - reportID: 4, + reportID: '4', participantAccountIDs: [4], visibleChatMemberAccountIDs: [4], reportName: 'Black Panther', type: CONST.REPORT.TYPE.CHAT, }, - 5: { + '5': { lastReadTime: '2021-01-14 11:25:39.299', lastVisibleActionCreated: '2022-11-22 03:26:02.019', isPinned: false, - reportID: 5, + reportID: '5', participantAccountIDs: [5], visibleChatMemberAccountIDs: [5], reportName: 'Invisible Woman', type: CONST.REPORT.TYPE.CHAT, }, - 6: { + '6': { lastReadTime: '2021-01-14 11:25:39.300', lastVisibleActionCreated: '2022-11-22 03:26:02.020', isPinned: false, - reportID: 6, + reportID: '6', participantAccountIDs: [6], visibleChatMemberAccountIDs: [6], reportName: 'Thor', @@ -74,11 +76,11 @@ describe('OptionsListUtils', () => { }, // Note: This report has the largest lastVisibleActionCreated - 7: { + '7': { lastReadTime: '2021-01-14 11:25:39.301', lastVisibleActionCreated: '2022-11-22 03:26:03.999', isPinned: false, - reportID: 7, + reportID: '7', participantAccountIDs: [7], visibleChatMemberAccountIDs: [7], reportName: 'Captain America', @@ -86,11 +88,11 @@ describe('OptionsListUtils', () => { }, // Note: This report has no lastVisibleActionCreated - 8: { + '8': { lastReadTime: '2021-01-14 11:25:39.301', lastVisibleActionCreated: '2022-11-22 03:26:02.000', isPinned: false, - reportID: 8, + reportID: '8', participantAccountIDs: [12], visibleChatMemberAccountIDs: [12], reportName: 'Silver Surfer', @@ -98,23 +100,23 @@ describe('OptionsListUtils', () => { }, // Note: This report has an IOU - 9: { + '9': { lastReadTime: '2021-01-14 11:25:39.302', lastVisibleActionCreated: '2022-11-22 03:26:02.998', isPinned: false, - reportID: 9, + reportID: '9', participantAccountIDs: [8], visibleChatMemberAccountIDs: [8], reportName: 'Mister Sinister', - iouReportID: 100, + iouReportID: '100', type: CONST.REPORT.TYPE.CHAT, }, // This report is an archived room – it does not have a name and instead falls back on oldPolicyName - 10: { + '10': { lastReadTime: '2021-01-14 11:25:39.200', lastVisibleActionCreated: '2022-11-22 03:26:02.001', - reportID: 10, + reportID: '10', isPinned: false, participantAccountIDs: [2, 7], visibleChatMemberAccountIDs: [2, 7], @@ -133,69 +135,79 @@ describe('OptionsListUtils', () => { // And a set of personalDetails some with existing reports and some without const PERSONAL_DETAILS = { // These exist in our reports - 1: { + '1': { accountID: 1, displayName: 'Mister Fantastic', login: 'reedrichards@expensify.com', isSelected: true, + reportID: '1', }, - 2: { + '2': { accountID: 2, displayName: 'Iron Man', login: 'tonystark@expensify.com', + reportID: '1', }, - 3: { + '3': { accountID: 3, displayName: 'Spider-Man', login: 'peterparker@expensify.com', + reportID: '1', }, - 4: { + '4': { accountID: 4, displayName: 'Black Panther', login: 'tchalla@expensify.com', + reportID: '1', }, - 5: { + '5': { accountID: 5, displayName: 'Invisible Woman', login: 'suestorm@expensify.com', + reportID: '1', }, - 6: { + '6': { accountID: 6, displayName: 'Thor', login: 'thor@expensify.com', + reportID: '1', }, - 7: { + '7': { accountID: 7, displayName: 'Captain America', login: 'steverogers@expensify.com', + reportID: '1', }, - 8: { + '8': { accountID: 8, displayName: 'Mr Sinister', login: 'mistersinister@marauders.com', + reportID: '1', }, // These do not exist in reports at all - 9: { + '9': { accountID: 9, displayName: 'Black Widow', login: 'natasharomanoff@expensify.com', + reportID: '', }, - 10: { + '10': { accountID: 10, displayName: 'The Incredible Hulk', login: 'brucebanner@expensify.com', + reportID: '', }, }; const REPORTS_WITH_CONCIERGE = { ...REPORTS, - 11: { + '11': { lastReadTime: '2021-01-14 11:25:39.302', lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, - reportID: 11, + reportID: '11', participantAccountIDs: [999], visibleChatMemberAccountIDs: [999], reportName: 'Concierge', @@ -205,11 +217,11 @@ describe('OptionsListUtils', () => { const REPORTS_WITH_CHRONOS = { ...REPORTS, - 12: { + '12': { lastReadTime: '2021-01-14 11:25:39.302', lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, - reportID: 12, + reportID: '12', participantAccountIDs: [1000], visibleChatMemberAccountIDs: [1000], reportName: 'Chronos', @@ -219,11 +231,11 @@ describe('OptionsListUtils', () => { const REPORTS_WITH_RECEIPTS = { ...REPORTS, - 13: { + '13': { lastReadTime: '2021-01-14 11:25:39.302', lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, - reportID: 13, + reportID: '13', participantAccountIDs: [1001], visibleChatMemberAccountIDs: [1001], reportName: 'Receipts', @@ -233,11 +245,11 @@ describe('OptionsListUtils', () => { const REPORTS_WITH_WORKSPACE_ROOMS = { ...REPORTS, - 14: { + '14': { lastReadTime: '2021-01-14 11:25:39.302', lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, - reportID: 14, + reportID: '14', participantAccountIDs: [1, 10, 3], visibleChatMemberAccountIDs: [1, 10, 3], reportName: '', @@ -252,7 +264,7 @@ describe('OptionsListUtils', () => { const PERSONAL_DETAILS_WITH_CONCIERGE = { ...PERSONAL_DETAILS, - 999: { + '999': { accountID: 999, displayName: 'Concierge', login: 'concierge@expensify.com', @@ -262,7 +274,7 @@ describe('OptionsListUtils', () => { const PERSONAL_DETAILS_WITH_CHRONOS = { ...PERSONAL_DETAILS, - 1000: { + '1000': { accountID: 1000, displayName: 'Chronos', login: 'chronos@expensify.com', @@ -272,7 +284,7 @@ describe('OptionsListUtils', () => { const PERSONAL_DETAILS_WITH_RECEIPTS = { ...PERSONAL_DETAILS, - 1001: { + '1001': { accountID: 1001, displayName: 'Receipts', login: 'receipts@expensify.com', @@ -282,7 +294,7 @@ describe('OptionsListUtils', () => { const PERSONAL_DETAILS_WITH_PERIODS = { ...PERSONAL_DETAILS, - 1002: { + '1002': { accountID: 1002, displayName: 'The Flash', login: 'barry.allen@expensify.com', @@ -290,9 +302,14 @@ describe('OptionsListUtils', () => { }; const POLICY = { - policyID: 'ABC123', + id: 'ABC123', name: 'Hero Policy', - }; + role: 'user', + type: 'free', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, + } as const; // Set the currently logged in user, report data, and personal details beforeAll(() => { @@ -300,11 +317,12 @@ describe('OptionsListUtils', () => { keys: ONYXKEYS, initialKeyStates: { [ONYXKEYS.SESSION]: {accountID: 2, email: 'tonystark@expensify.com'}, - [`${ONYXKEYS.COLLECTION.REPORT}100`]: { + [`${ONYXKEYS.COLLECTION.REPORT}100` as const]: { + reportID: '', ownerAccountID: 8, - total: '1000', + total: 1000, }, - [`${ONYXKEYS.COLLECTION.POLICY}${POLICY.policyID}`]: POLICY, + [`${ONYXKEYS.COLLECTION.POLICY}${POLICY.id}` as const]: POLICY, }, }); Onyx.registerLogger(() => {}); @@ -319,7 +337,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails.length).toBe(2); // Then all of the reports should be shown including the archived rooms. - expect(results.recentReports.length).toBe(_.size(REPORTS)); + expect(results.recentReports.length).toBe(Object.values(REPORTS).length); // When we filter again but provide a searchValue results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, 'spider'); @@ -360,7 +378,7 @@ describe('OptionsListUtils', () => { // We should expect all personalDetails to be returned, // minus the currently logged in user and recent reports count - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 1 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS).length - 1 - MAX_RECENT_REPORTS); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Widow'); @@ -369,11 +387,11 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[3].text).toBe('The Incredible Hulk'); // Then the result which has an existing report should also have the reportID attached - const personalDetailWithExistingReport = _.find(results.personalDetails, (personalDetail) => personalDetail.login === 'peterparker@expensify.com'); - expect(personalDetailWithExistingReport.reportID).toBe(2); + const personalDetailWithExistingReport = results.personalDetails.find((personalDetail) => personalDetail.login === 'peterparker@expensify.com'); + expect(personalDetailWithExistingReport?.reportID).toBe('2'); // When we only pass personal details - results = OptionsListUtils.getFilteredOptions([], PERSONAL_DETAILS, [], ''); + results = OptionsListUtils.getFilteredOptions({}, PERSONAL_DETAILS, [], ''); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Panther'); @@ -414,28 +432,28 @@ describe('OptionsListUtils', () => { // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the 5 that are already showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 1 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CONCIERGE).length - 1 - MAX_RECENT_REPORTS); expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CONCIERGE).length - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Chronos from the results results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CHRONOS) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CHRONOS).length - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); // Test by excluding Receipts from the results results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_RECEIPTS) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_RECEIPTS).length - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); }); @@ -448,7 +466,7 @@ describe('OptionsListUtils', () => { // And we should expect all the personalDetails to show (minus the 5 that are already // showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 6); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS).length - 6); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Widow'); @@ -457,8 +475,8 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[3].text).toBe('The Incredible Hulk'); // And none of our personalDetails should include any of the users with recent reports - const reportLogins = _.map(results.recentReports, (reportOption) => reportOption.login); - const personalDetailsOverlapWithReports = _.every(results.personalDetails, (personalDetailOption) => _.contains(reportLogins, personalDetailOption.login)); + const reportLogins = results.recentReports.map((reportOption) => reportOption.login); + const personalDetailsOverlapWithReports = results.personalDetails.every((personalDetailOption) => reportLogins.includes(personalDetailOption.login)); expect(personalDetailsOverlapWithReports).toBe(false); // When we search for an option that is only in a personalDetail with no existing report @@ -487,15 +505,15 @@ describe('OptionsListUtils', () => { // Then one of our older report options (not in our five most recent) should appear in the personalDetails // but not in recentReports - expect(_.every(results.recentReports, (option) => option.login !== 'peterparker@expensify.com')).toBe(true); - expect(_.every(results.personalDetails, (option) => option.login !== 'peterparker@expensify.com')).toBe(false); + expect(results.recentReports.every((option) => option.login !== 'peterparker@expensify.com')).toBe(true); + expect(results.personalDetails.every((option) => option.login !== 'peterparker@expensify.com')).toBe(false); // When we provide a "selected" option to getFilteredOptions() results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '', [{login: 'peterparker@expensify.com'}]); // Then the option should not appear anywhere in either list - expect(_.every(results.recentReports, (option) => option.login !== 'peterparker@expensify.com')).toBe(true); - expect(_.every(results.personalDetails, (option) => option.login !== 'peterparker@expensify.com')).toBe(true); + expect(results.recentReports.every((option) => option.login !== 'peterparker@expensify.com')).toBe(true); + expect(results.personalDetails.every((option) => option.login !== 'peterparker@expensify.com')).toBe(true); // When we add a search term for which no options exist and the searchValue itself // is not a potential email or phone @@ -531,7 +549,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(0); expect(results.personalDetails.length).toBe(0); expect(results.userToInvite).not.toBe(null); - expect(results.userToInvite.login).toBe('+15005550006'); + expect(results.userToInvite?.login).toBe('+15005550006'); // When we add a search term for which no options exist and the searchValue itself // is a potential phone number with country code added @@ -542,7 +560,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(0); expect(results.personalDetails.length).toBe(0); expect(results.userToInvite).not.toBe(null); - expect(results.userToInvite.login).toBe('+15005550006'); + expect(results.userToInvite?.login).toBe('+15005550006'); // When we add a search term for which no options exist and the searchValue itself // is a potential phone number with special characters added @@ -553,7 +571,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(0); expect(results.personalDetails.length).toBe(0); expect(results.userToInvite).not.toBe(null); - expect(results.userToInvite.login).toBe('+18003243233'); + expect(results.userToInvite?.login).toBe('+18003243233'); // When we use a search term for contact number that contains alphabet characters results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '998243aaaa'); @@ -568,7 +586,7 @@ describe('OptionsListUtils', () => { // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the 5 that are already showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 6); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CONCIERGE).length - 6); expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results @@ -576,7 +594,7 @@ describe('OptionsListUtils', () => { // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 7); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CONCIERGE).length - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); @@ -585,7 +603,7 @@ describe('OptionsListUtils', () => { // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CHRONOS) - 7); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CHRONOS).length - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); @@ -594,26 +612,25 @@ describe('OptionsListUtils', () => { // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_RECEIPTS) - 7); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_RECEIPTS).length - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); }); it('getShareDestinationsOptions()', () => { // Filter current REPORTS as we do in the component, before getting share destination options - const filteredReports = {}; - _.keys(REPORTS).forEach((reportKey) => { - if (!ReportUtils.canUserPerformWriteAction(REPORTS[reportKey]) || ReportUtils.isExpensifyOnlyParticipantInReport(REPORTS[reportKey])) { - return; + const filteredReports = Object.entries(REPORTS).reduce>((reports, [reportKey, report]) => { + if (!ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { + return reports; } - filteredReports[reportKey] = REPORTS[reportKey]; - }); + return {...reports, [reportKey]: report}; + }, {}); // When we pass an empty search value let results = OptionsListUtils.getShareDestinationOptions(filteredReports, PERSONAL_DETAILS, [], ''); // Then we should expect all the recent reports to show but exclude the archived rooms - expect(results.recentReports.length).toBe(_.size(REPORTS) - 1); + expect(results.recentReports.length).toBe(Object.values(REPORTS).length - 1); // When we pass a search value that doesn't match the group chat name results = OptionsListUtils.getShareDestinationOptions(filteredReports, PERSONAL_DETAILS, [], 'mutants'); @@ -628,20 +645,19 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(1); // Filter current REPORTS_WITH_WORKSPACE_ROOMS as we do in the component, before getting share destination options - const filteredReportsWithWorkspaceRooms = {}; - _.keys(REPORTS_WITH_WORKSPACE_ROOMS).forEach((reportKey) => { - if (!ReportUtils.canUserPerformWriteAction(REPORTS_WITH_WORKSPACE_ROOMS[reportKey]) || ReportUtils.isExpensifyOnlyParticipantInReport(REPORTS_WITH_WORKSPACE_ROOMS[reportKey])) { - return; + const filteredReportsWithWorkspaceRooms = Object.entries(REPORTS_WITH_WORKSPACE_ROOMS).reduce>((reports, [reportKey, report]) => { + if (!ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { + return reports; } - filteredReportsWithWorkspaceRooms[reportKey] = REPORTS_WITH_WORKSPACE_ROOMS[reportKey]; - }); + return {...reports, [reportKey]: report}; + }, {}); // When we also have a policy to return rooms in the results results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, PERSONAL_DETAILS, [], ''); // Then we should expect the DMS, the group chats and the workspace room to show // We should expect all the recent reports to show, excluding the archived rooms - expect(results.recentReports.length).toBe(_.size(REPORTS_WITH_WORKSPACE_ROOMS) - 1); + expect(results.recentReports.length).toBe(Object.values(REPORTS_WITH_WORKSPACE_ROOMS).length - 1); // When we search for a workspace room results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, PERSONAL_DETAILS, [], 'Avengers Room'); @@ -691,22 +707,38 @@ describe('OptionsListUtils', () => { enabled: true, }, ]; - const smallCategoriesList = { + const smallCategoriesList: PolicyCategories = { Taxi: { enabled: false, name: 'Taxi', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, Restaurant: { enabled: true, name: 'Restaurant', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, Food: { enabled: true, name: 'Food', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Food: Meat': { enabled: true, name: 'Food: Meat', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, }; const smallResultList = [ @@ -775,62 +807,118 @@ describe('OptionsListUtils', () => { data: [], }, ]; - const largeCategoriesList = { + const largeCategoriesList: PolicyCategories = { Taxi: { enabled: false, name: 'Taxi', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, Restaurant: { enabled: true, name: 'Restaurant', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, Food: { enabled: true, name: 'Food', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Food: Meat': { enabled: true, name: 'Food: Meat', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Food: Milk': { enabled: true, name: 'Food: Milk', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Food: Vegetables': { enabled: false, name: 'Food: Vegetables', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Cars: Audi': { enabled: true, name: 'Cars: Audi', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Cars: BMW': { enabled: false, name: 'Cars: BMW', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Cars: Mercedes-Benz': { enabled: true, name: 'Cars: Mercedes-Benz', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, Medical: { enabled: false, name: 'Medical', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Travel: Meals': { enabled: true, name: 'Travel: Meals', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Travel: Meals: Breakfast': { enabled: true, name: 'Travel: Meals: Breakfast', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Travel: Meals: Dinner': { enabled: false, name: 'Travel: Meals: Dinner', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Travel: Meals: Lunch': { enabled: true, name: 'Travel: Meals: Lunch', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, }; const largeResultList = [ @@ -1089,22 +1177,26 @@ describe('OptionsListUtils', () => { name: 'Medical', }, ]; - const smallTagsList = { + const smallTagsList: Record = { Engineering: { enabled: false, name: 'Engineering', + accountID: null, }, Medical: { enabled: true, name: 'Medical', + accountID: null, }, Accounting: { enabled: true, name: 'Accounting', + accountID: null, }, HR: { enabled: true, name: 'HR', + accountID: null, }, }; const smallResultList = [ @@ -1162,50 +1254,61 @@ describe('OptionsListUtils', () => { data: [], }, ]; - const largeTagsList = { + const largeTagsList: Record = { Engineering: { enabled: false, name: 'Engineering', + accountID: null, }, Medical: { enabled: true, name: 'Medical', + accountID: null, }, Accounting: { enabled: true, name: 'Accounting', + accountID: null, }, HR: { enabled: true, name: 'HR', + accountID: null, }, Food: { enabled: true, name: 'Food', + accountID: null, }, Traveling: { enabled: false, name: 'Traveling', + accountID: null, }, Cleaning: { enabled: true, name: 'Cleaning', + accountID: null, }, Software: { enabled: true, name: 'Software', + accountID: null, }, OfficeSupplies: { enabled: false, name: 'Office Supplies', + accountID: null, }, Taxes: { enabled: true, name: 'Taxes', + accountID: null, }, Benefits: { enabled: true, name: 'Benefits', + accountID: null, }, }; const largeResultList = [ @@ -2063,7 +2166,7 @@ describe('OptionsListUtils', () => { const emptySearch = ''; const wrongSearch = 'bla bla'; - const policyTaxRatesWithDefault = { + const policyTaxRatesWithDefault: PolicyTaxRateWithDefault = { name: 'Tax', defaultExternalID: 'CODE1', defaultValue: '0%', @@ -2072,14 +2175,20 @@ describe('OptionsListUtils', () => { CODE2: { name: 'Tax rate 2', value: '3%', + code: '', + modifiedName: '', }, CODE3: { name: 'Tax option 3', value: '5%', + code: '', + modifiedName: '', }, CODE1: { name: 'Tax exempt 1', value: '0%', + code: '', + modifiedName: '', }, }, }; @@ -2201,7 +2310,7 @@ describe('OptionsListUtils', () => { }); it('formatMemberForList()', () => { - const formattedMembers = _.map(PERSONAL_DETAILS, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail)); + const formattedMembers = Object.values(PERSONAL_DETAILS).map((personalDetail) => OptionsListUtils.formatMemberForList(personalDetail)); // We're only formatting items inside the array, so the order should be the same as the original PERSONAL_DETAILS array expect(formattedMembers[0].text).toBe('Mister Fantastic'); @@ -2212,9 +2321,9 @@ describe('OptionsListUtils', () => { expect(formattedMembers[0].isSelected).toBe(true); // And all the others to be unselected - expect(_.every(formattedMembers.slice(1), (personalDetail) => !personalDetail.isSelected)).toBe(true); + expect(formattedMembers.slice(1).every((personalDetail) => !personalDetail.isSelected)).toBe(true); // `isDisabled` is always false - expect(_.every(formattedMembers, (personalDetail) => !personalDetail.isDisabled)).toBe(true); + expect(formattedMembers.every((personalDetail) => !personalDetail.isDisabled)).toBe(true); }); }); diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.ts similarity index 80% rename from tests/unit/ReportUtilsTest.js rename to tests/unit/ReportUtilsTest.ts index a5b0a5d3c151..b8375700304c 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.ts @@ -1,52 +1,59 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import CONST from '../../src/CONST'; -import * as ReportUtils from '../../src/libs/ReportUtils'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; // Be sure to include the mocked permissions library or else the beta tests won't work -jest.mock('../../src/libs/Permissions'); +jest.mock('@libs/Permissions'); const currentUserEmail = 'bjorn@vikings.net'; const currentUserAccountID = 5; const participantsPersonalDetails = { - 1: { + '1': { accountID: 1, displayName: 'Ragnar Lothbrok', firstName: 'Ragnar', login: 'ragnar@vikings.net', }, - 2: { + '2': { accountID: 2, login: 'floki@vikings.net', displayName: 'floki@vikings.net', }, - 3: { + '3': { accountID: 3, displayName: 'Lagertha Lothbrok', firstName: 'Lagertha', login: 'lagertha@vikings.net', pronouns: 'She/her', }, - 4: { + '4': { accountID: 4, login: '+18332403627@expensify.sms', displayName: '(833) 240-3627', }, - 5: { + '5': { accountID: 5, displayName: 'Lagertha Lothbrok', firstName: 'Lagertha', login: 'lagertha2@vikings.net', pronouns: 'She/her', }, -}; +} as const; + const policy = { - policyID: 1, + id: '1', name: 'Vikings Policy', -}; + role: 'user', + type: 'free', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, +} as const; Onyx.init({keys: ONYXKEYS}); @@ -56,7 +63,7 @@ describe('ReportUtils', () => { [ONYXKEYS.PERSONAL_DETAILS_LIST]: participantsPersonalDetails, [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID: currentUserAccountID}, [ONYXKEYS.COUNTRY_CODE]: 1, - [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, + [`${ONYXKEYS.COLLECTION.POLICY}${policy.id}` as const]: policy, }); return waitForBatchedUpdates(); }); @@ -106,6 +113,7 @@ describe('ReportUtils', () => { test('with displayName', () => { expect( ReportUtils.getReportName({ + reportID: '', participantAccountIDs: [currentUserAccountID, 1], }), ).toBe('Ragnar Lothbrok'); @@ -114,6 +122,7 @@ describe('ReportUtils', () => { test('no displayName', () => { expect( ReportUtils.getReportName({ + reportID: '', participantAccountIDs: [currentUserAccountID, 2], }), ).toBe('floki@vikings.net'); @@ -122,6 +131,7 @@ describe('ReportUtils', () => { test('SMS', () => { expect( ReportUtils.getReportName({ + reportID: '', participantAccountIDs: [currentUserAccountID, 4], }), ).toBe('(833) 240-3627'); @@ -131,6 +141,7 @@ describe('ReportUtils', () => { test('Group DM', () => { expect( ReportUtils.getReportName({ + reportID: '', participantAccountIDs: [currentUserAccountID, 1, 2, 3, 4], }), ).toBe('Ragnar, floki@vikings.net, Lagertha, (833) 240-3627'); @@ -138,6 +149,7 @@ describe('ReportUtils', () => { describe('Default Policy Room', () => { const baseAdminsRoom = { + reportID: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, reportName: '#admins', }; @@ -161,6 +173,7 @@ describe('ReportUtils', () => { describe('User-Created Policy Room', () => { const baseUserCreatedRoom = { + reportID: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, reportName: '#VikingsChat', }; @@ -187,8 +200,9 @@ describe('ReportUtils', () => { test('as member', () => { expect( ReportUtils.getReportName({ + reportID: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, - policyID: policy.policyID, + policyID: policy.id, isOwnPolicyExpenseChat: true, ownerAccountID: 1, }), @@ -198,8 +212,9 @@ describe('ReportUtils', () => { test('as admin', () => { expect( ReportUtils.getReportName({ + reportID: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, - policyID: policy.policyID, + policyID: policy.id, isOwnPolicyExpenseChat: false, ownerAccountID: 1, }), @@ -209,9 +224,10 @@ describe('ReportUtils', () => { describe('Archived', () => { const baseArchivedPolicyExpenseChat = { + reportID: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, ownerAccountID: 1, - policyID: policy.policyID, + policyID: policy.id, oldPolicyName: policy.name, statusNum: CONST.REPORT.STATUS_NUM.CLOSED, stateNum: CONST.REPORT.STATE_NUM.APPROVED, @@ -248,18 +264,18 @@ describe('ReportUtils', () => { describe('requiresAttentionFromCurrentUser', () => { it('returns false when there is no report', () => { - expect(ReportUtils.requiresAttentionFromCurrentUser()).toBe(false); + expect(ReportUtils.requiresAttentionFromCurrentUser(null)).toBe(false); }); it('returns false when the matched IOU report does not have an owner accountID', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), ownerAccountID: undefined, }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(false); }); it('returns false when the linked iou report has an oustanding IOU', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), iouReportID: '1', }; Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, { @@ -271,7 +287,7 @@ describe('ReportUtils', () => { }); it('returns false when the report has no outstanding IOU but is waiting for a bank account and the logged user is the report owner', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), ownerAccountID: currentUserAccountID, isWaitingOnBankAccount: true, }; @@ -279,7 +295,7 @@ describe('ReportUtils', () => { }); it('returns false when the report has outstanding IOU and is not waiting for a bank account and the logged user is the report owner', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), ownerAccountID: currentUserAccountID, isWaitingOnBankAccount: false, }; @@ -287,7 +303,7 @@ describe('ReportUtils', () => { }); it('returns false when the report has no oustanding IOU but is waiting for a bank account and the logged user is not the report owner', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), ownerAccountID: 97, isWaitingOnBankAccount: true, }; @@ -295,14 +311,14 @@ describe('ReportUtils', () => { }); it('returns true when the report has an unread mention', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), isUnreadWithMention: true, }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(true); }); it('returns true when the report is an outstanding task', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), type: CONST.REPORT.TYPE.TASK, managerID: currentUserAccountID, isUnreadWithMention: false, @@ -313,7 +329,7 @@ describe('ReportUtils', () => { }); it('returns true when the report has oustanding child request', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), ownerAccountID: 99, hasOutstandingChildRequest: true, isWaitingOnBankAccount: false, @@ -323,7 +339,7 @@ describe('ReportUtils', () => { }); describe('getMoneyRequestOptions', () => { - const participantsAccountIDs = _.keys(participantsPersonalDetails); + const participantsAccountIDs = Object.keys(participantsPersonalDetails).map(Number); beforeAll(() => { Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { @@ -338,8 +354,8 @@ describe('ReportUtils', () => { describe('return empty iou options if', () => { it('participants aray contains excluded expensify iou emails', () => { - const allEmpty = _.every(CONST.EXPENSIFY_ACCOUNT_IDS, (accountID) => { - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions({}, {}, [currentUserAccountID, accountID]); + const allEmpty = CONST.EXPENSIFY_ACCOUNT_IDS.every((accountID) => { + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(null, null, [currentUserAccountID, accountID]); return moneyRequestOptions.length === 0; }); expect(allEmpty).toBe(true); @@ -347,51 +363,51 @@ describe('ReportUtils', () => { it('it is a room with no participants except self', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); it('its not your policy expense chat', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, isOwnPolicyExpenseChat: false, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); it('its paid IOU report', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), type: CONST.REPORT.TYPE.IOU, statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); it('its approved Expense report', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.APPROVED, statusNum: CONST.REPORT.STATUS_NUM.APPROVED, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); it('its paid Expense report', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), type: CONST.REPORT.TYPE.EXPENSE, statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); @@ -401,11 +417,11 @@ describe('ReportUtils', () => { isOwnPolicyExpenseChat: false, }).then(() => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), parentReportID: '100', type: CONST.REPORT.TYPE.EXPENSE, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); }); @@ -417,7 +433,7 @@ describe('ReportUtils', () => { isOwnPolicyExpenseChat: true, }).then(() => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, @@ -425,7 +441,13 @@ describe('ReportUtils', () => { }; const paidPolicy = { type: CONST.POLICY.TYPE.TEAM, - }; + id: '', + name: '', + role: 'user', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, + } as const; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(0); }); @@ -434,47 +456,49 @@ describe('ReportUtils', () => { describe('return only iou split option if', () => { it('it is a chat room with more than one participant', () => { - const onlyHaveSplitOption = _.every( - [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL, CONST.REPORT.CHAT_TYPE.POLICY_ROOM], - (chatType) => { - const report = { - ...LHNTestUtils.getFakeReport(), - chatType, - }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]); - return moneyRequestOptions.length === 1 && moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT); - }, - ); + const onlyHaveSplitOption = [ + CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, + CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, + CONST.REPORT.CHAT_TYPE.DOMAIN_ALL, + CONST.REPORT.CHAT_TYPE.POLICY_ROOM, + ].every((chatType) => { + const report = { + ...(LHNTestUtils.getFakeReport() as Report), + chatType, + }; + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); + return moneyRequestOptions.length === 1 && moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT); + }); expect(onlyHaveSplitOption).toBe(true); }); it('has multiple participants excluding self', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, ...participantsAccountIDs]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); }); it('user has send money permission', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, ...participantsAccountIDs]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); }); it("it's a group DM report", () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), type: CONST.REPORT.TYPE.CHAT, participantsAccountIDs: [currentUserAccountID, ...participantsAccountIDs], }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, ...participantsAccountIDs]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs.map(Number)]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); }); @@ -488,11 +512,11 @@ describe('ReportUtils', () => { isOwnPolicyExpenseChat: true, }).then(() => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), parentReportID: '102', type: CONST.REPORT.TYPE.EXPENSE, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); }); @@ -505,7 +529,7 @@ describe('ReportUtils', () => { isOwnPolicyExpenseChat: true, }).then(() => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN, @@ -513,32 +537,38 @@ describe('ReportUtils', () => { }; const paidPolicy = { type: CONST.POLICY.TYPE.TEAM, - }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]], true); + id: '', + name: '', + role: 'user', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, + } as const; + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(1); }); }); it('it is an IOU report in submitted state', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), type: CONST.REPORT.TYPE.IOU, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); }); it('it is an IOU report in submitted state even with send money permissions', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), type: CONST.REPORT.TYPE.IOU, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); }); @@ -547,11 +577,11 @@ describe('ReportUtils', () => { describe('return multiple money request option if', () => { it("it is user's own policy expense chat", () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, isOwnPolicyExpenseChat: true, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, ...participantsAccountIDs]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs]); expect(moneyRequestOptions.length).toBe(2); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); @@ -559,10 +589,10 @@ describe('ReportUtils', () => { it('it is a 1:1 DM', () => { const report = { - ...LHNTestUtils.getFakeReport(), + ...(LHNTestUtils.getFakeReport() as Report), type: CONST.REPORT.TYPE.CHAT, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(2); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND)).toBe(true); @@ -589,20 +619,20 @@ describe('ReportUtils', () => { describe('sortReportsByLastRead', () => { it('should filter out report without reportID & lastReadTime and sort lastReadTime in ascending order', () => { const reports = [ - {reportID: 1, lastReadTime: '2023-07-08 07:15:44.030'}, - {reportID: 2, lastReadTime: null}, - {reportID: 3, lastReadTime: '2023-07-06 07:15:44.030'}, - {reportID: 4, lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU}, - {lastReadTime: '2023-07-09 07:15:44.030'}, - {reportID: 6}, - {}, + {reportID: '1', lastReadTime: '2023-07-08 07:15:44.030'}, + {reportID: '2', lastReadTime: undefined}, + {reportID: '3', lastReadTime: '2023-07-06 07:15:44.030'}, + {reportID: '4', lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU}, + {lastReadTime: '2023-07-09 07:15:44.030'} as Report, + {reportID: '6'}, + null, ]; const sortedReports = [ - {reportID: 3, lastReadTime: '2023-07-06 07:15:44.030'}, - {reportID: 4, lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU}, - {reportID: 1, lastReadTime: '2023-07-08 07:15:44.030'}, - ]; - expect(ReportUtils.sortReportsByLastRead(reports)).toEqual(sortedReports); + {reportID: '3', lastReadTime: '2023-07-06 07:15:44.030'}, + {reportID: '4', lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU}, + {reportID: '1', lastReadTime: '2023-07-08 07:15:44.030'}, + ] as const; + expect(ReportUtils.sortReportsByLastRead(reports, null)).toEqual(sortedReports); }); }); @@ -613,26 +643,26 @@ describe('ReportUtils', () => { {reportID: '3', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '2', parentReportID: '2', reportName: 'Report'}, {reportID: '4', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '3', parentReportID: '3', reportName: 'Report'}, {reportID: '5', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '4', parentReportID: '4', reportName: 'Report'}, - ]; + ] as const; const reportActions = [ - {reportActionID: '1', created: '2024-02-01 04:42:22.965'}, - {reportActionID: '2', created: '2024-02-01 04:42:28.003'}, - {reportActionID: '3', created: '2024-02-01 04:42:31.742'}, - {reportActionID: '4', created: '2024-02-01 04:42:35.619'}, - ]; + {reportActionID: '1', created: '2024-02-01 04:42:22.965', actionName: 'MARKEDREIMBURSED'}, + {reportActionID: '2', created: '2024-02-01 04:42:28.003', actionName: 'MARKEDREIMBURSED'}, + {reportActionID: '3', created: '2024-02-01 04:42:31.742', actionName: 'MARKEDREIMBURSED'}, + {reportActionID: '4', created: '2024-02-01 04:42:35.619', actionName: 'MARKEDREIMBURSED'}, + ] as const; beforeAll(() => { Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT}${reports[0].reportID}`]: reports[0], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[1].reportID}`]: reports[1], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[2].reportID}`]: reports[2], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[3].reportID}`]: reports[3], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[4].reportID}`]: reports[4], - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[0].reportID}`]: {[reportActions[0].reportActionID]: reportActions[0]}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[1].reportID}`]: {[reportActions[1].reportActionID]: reportActions[1]}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[2].reportID}`]: {[reportActions[2].reportActionID]: reportActions[2]}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[3].reportID}`]: {[reportActions[3].reportActionID]: reportActions[3]}, + [`${ONYXKEYS.COLLECTION.REPORT}${reports[0].reportID}` as const]: reports[0], + [`${ONYXKEYS.COLLECTION.REPORT}${reports[1].reportID}` as const]: reports[1], + [`${ONYXKEYS.COLLECTION.REPORT}${reports[2].reportID}` as const]: reports[2], + [`${ONYXKEYS.COLLECTION.REPORT}${reports[3].reportID}` as const]: reports[3], + [`${ONYXKEYS.COLLECTION.REPORT}${reports[4].reportID}` as const]: reports[4], + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[0].reportID}` as const]: {[reportActions[0].reportActionID]: reportActions[0]}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[1].reportID}` as const]: {[reportActions[1].reportActionID]: reportActions[1]}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[2].reportID}` as const]: {[reportActions[2].reportActionID]: reportActions[2]}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[3].reportID}` as const]: {[reportActions[3].reportActionID]: reportActions[3]}, }); return waitForBatchedUpdates(); }); From 88c5fd8e5c29cc7dc1b70d863483abe0fc7db86a Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Tue, 27 Feb 2024 11:53:59 +0100 Subject: [PATCH 047/218] add missing props to memo --- src/pages/home/sidebar/SidebarLinksData.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 0c8125cae79f..59bf74586b4b 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -345,6 +345,7 @@ export default compose( prevProps.onLinkClick === nextProps.onLinkClick && _.isEqual(prevProps.policyMembers, nextProps.policyMembers) && _.isEqual(prevProps.transactionViolations, nextProps.transactionViolations) && - _.isEqual(prevProps.currentUserPersonalDetails, nextProps.currentUserPersonalDetails), + _.isEqual(prevProps.currentUserPersonalDetails, nextProps.currentUserPersonalDetails) && + prevProps.currentReportID === nextProps.currentReportID, ), ); From 3aa217604ce487b9748412e60d1827822c74f1f7 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Tue, 27 Feb 2024 14:13:51 +0100 Subject: [PATCH 048/218] chore: add comments for memoization --- src/pages/home/sidebar/SidebarLinksData.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 59bf74586b4b..c5749893d7fb 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -331,6 +331,12 @@ export default compose( }, }), )( + /* + While working on audit on the App Start App metric we noticed that by memoizing SidebarLinksData we can avoid 1 additional run of getOrderedReportIDs. + With that we can reduce app start up time by ~2.5s on heavy account. + After finding and fixing core issues with getOrderedReportIDs performance we might remove the memoization + More details - https://github.com/Expensify/App/issues/35234#issuecomment-1926914534 + */ memo( SidebarLinksData, (prevProps, nextProps) => From 1a63d65a52d36c8bdf781711b4d9fc6095f6a820 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Wed, 28 Feb 2024 08:20:43 +0000 Subject: [PATCH 049/218] [TS migration] Migrate compare, currencyUtilsTest, IOUUtilsTest, SideBarTest and TestHelper --- tests/e2e/compare/{compare.js => compare.ts} | 60 +++-- tests/e2e/measure/math.ts | 1 + ...rencyUtilsTest.js => CurrencyUtilsTest.ts} | 17 +- .../unit/{IOUUtilsTest.js => IOUUtilsTest.ts} | 43 +-- tests/unit/{SidebarTest.js => SidebarTest.ts} | 86 +++--- tests/utils/TestHelper.js | 244 ------------------ tests/utils/TestHelper.ts | 242 +++++++++++++++++ 7 files changed, 356 insertions(+), 337 deletions(-) rename tests/e2e/compare/{compare.js => compare.ts} (68%) rename tests/unit/{CurrencyUtilsTest.js => CurrencyUtilsTest.ts} (92%) rename tests/unit/{IOUUtilsTest.js => IOUUtilsTest.ts} (81%) rename tests/unit/{SidebarTest.js => SidebarTest.ts} (62%) delete mode 100644 tests/utils/TestHelper.js create mode 100644 tests/utils/TestHelper.ts diff --git a/tests/e2e/compare/compare.js b/tests/e2e/compare/compare.ts similarity index 68% rename from tests/e2e/compare/compare.js rename to tests/e2e/compare/compare.ts index 7feaa8b266d1..cf74b1240bf8 100644 --- a/tests/e2e/compare/compare.js +++ b/tests/e2e/compare/compare.ts @@ -1,9 +1,25 @@ -import _ from 'underscore'; +import type {Stats} from '../measure/math'; import getStats from '../measure/math'; import * as math from './math'; import printToConsole from './output/console'; import writeToMarkdown from './output/markdown'; +type Entry = { + name: string; + baseline?: Stats; + current?: Stats; + diff?: number; + relativeDurationDiff?: number; + isDurationDiffOfSignificance?: boolean; + mean?: number; +}; + +type Result = { + name: string; + current?: Entry; + baseline?: Entry; +}; + /* * base implementation from: https://github.com/callstack/reassure/blob/main/packages/reassure-compare/src/compare.ts * This module reads from the baseline and compare files and compares the results. @@ -25,14 +41,7 @@ const PROBABILITY_CONSIDERED_SIGNIFICANCE = 0.02; */ const DURATION_DIFF_THRESHOLD_SIGNIFICANCE = 100; -/** - * - * @param {string} name - * @param {Object} compare - * @param {Object} baseline - * @returns {Object} - */ -function buildCompareEntry(name, compare, baseline) { +function buildCompareEntry(name: string, compare: Stats, baseline: Stats): Entry { const diff = compare.mean - baseline.mean; const relativeDurationDiff = diff / baseline.mean; @@ -53,20 +62,16 @@ function buildCompareEntry(name, compare, baseline) { /** * Compare results between baseline and current entries and categorize. - * - * @param {Object} compareEntries - * @param {Object} baselineEntries - * @returns {Object} */ -function compareResults(compareEntries, baselineEntries) { +function compareResults(compareEntries: Record, baselineEntries: Record) { // Unique test scenario names - const names = [...new Set([..._.keys(compareEntries), ..._.keys(baselineEntries || {})])]; + const names: string[] = [...new Set([...Object(compareEntries).keys(), ...Object(baselineEntries ?? {}).keys()])]; - const compared = []; - const added = []; - const removed = []; + const compared: Entry[] = []; + const added: Result[] = []; + const removed: Result[] = []; - names.forEach((name) => { + names.forEach((name: string) => { const current = compareEntries[name]; const baseline = baselineEntries[name]; @@ -88,15 +93,12 @@ function compareResults(compareEntries, baselineEntries) { } }); - const significance = _.chain(compared) - .filter((item) => item.isDurationDiffOfSignificance) - .value(); - const meaningless = _.chain(compared) - .filter((item) => !item.isDurationDiffOfSignificance) - .value(); + const significance = compared.filter((item) => item.isDurationDiffOfSignificance); - added.sort((a, b) => b.current.mean - a.current.mean); - removed.sort((a, b) => b.baseline.mean - a.baseline.mean); + const meaningless = compared.filter((item) => !item.isDurationDiffOfSignificance); + + added.sort((a, b) => (b?.current?.mean ?? 0) - (a.current?.mean ?? 0)); + removed.sort((a, b) => (b.baseline?.mean ?? 0) - (a.baseline?.mean ?? 0)); return { significance, @@ -106,7 +108,7 @@ function compareResults(compareEntries, baselineEntries) { }; } -export default (main, delta, outputFile, outputFormat = 'all') => { +export default (main: Record, delta: Record, outputFile: string, outputFormat = 'all') => { // IMPORTANT NOTE: make sure you are passing the delta/compare results first, then the main/baseline results: const outputData = compareResults(delta, main); @@ -118,3 +120,5 @@ export default (main, delta, outputFile, outputFormat = 'all') => { return writeToMarkdown(outputFile, outputData); } }; + +export type {Entry}; diff --git a/tests/e2e/measure/math.ts b/tests/e2e/measure/math.ts index e1c0cb981a0c..c9c0219ef1fd 100644 --- a/tests/e2e/measure/math.ts +++ b/tests/e2e/measure/math.ts @@ -49,3 +49,4 @@ const getStats = (entries: Entries): Stats => { }; export default getStats; +export type {Stats} diff --git a/tests/unit/CurrencyUtilsTest.js b/tests/unit/CurrencyUtilsTest.ts similarity index 92% rename from tests/unit/CurrencyUtilsTest.js rename to tests/unit/CurrencyUtilsTest.ts index 89e1e2ffb3be..246af64b1d87 100644 --- a/tests/unit/CurrencyUtilsTest.js +++ b/tests/unit/CurrencyUtilsTest.ts @@ -1,9 +1,9 @@ import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import CONST from '../../src/CONST'; -import * as CurrencyUtils from '../../src/libs/CurrencyUtils'; -import LocaleListener from '../../src/libs/Localize/LocaleListener'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import CONST from '@src/CONST'; +import * as CurrencyUtils from '@src/libs/CurrencyUtils'; +import LocaleListener from '@src/libs/Localize/LocaleListener'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; // This file can get outdated. In that case, you can follow these steps to update it: // - open your browser console and navigate to the Network tab @@ -13,7 +13,7 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; // - update currencyList.json import currencyList from './currencyList.json'; -const currencyCodeList = _.keys(currencyList); +const currencyCodeList = Object.keys(currencyList); const AVAILABLE_LOCALES = [CONST.LOCALES.EN, CONST.LOCALES.ES]; describe('CurrencyUtils', () => { @@ -37,7 +37,10 @@ describe('CurrencyUtils', () => { describe('getLocalizedCurrencySymbol', () => { test.each(AVAILABLE_LOCALES)('Returns non empty string for all currencyCode with preferredLocale %s', (prefrredLocale) => Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, prefrredLocale).then(() => { - _.forEach(currencyCodeList, (currencyCode) => { + if (isEmptyObject(currencyCodeList)) { + return; + } + currencyCodeList.forEach((currencyCode: string) => { const localizedSymbol = CurrencyUtils.getLocalizedCurrencySymbol(currencyCode); expect(localizedSymbol).toBeTruthy(); diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.ts similarity index 81% rename from tests/unit/IOUUtilsTest.js rename to tests/unit/IOUUtilsTest.ts index ac04b74a0ca5..b390d0cd70a3 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.ts @@ -1,8 +1,10 @@ +import type {NullishDeep} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import * as IOUUtils from '../../src/libs/IOUUtils'; -import * as ReportUtils from '../../src/libs/ReportUtils'; -import * as TransactionUtils from '../../src/libs/TransactionUtils'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import * as IOUUtils from '@src/libs/IOUUtils'; +import * as ReportUtils from '@src/libs/ReportUtils'; +import * as TransactionUtils from '@src/libs/TransactionUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type Transaction from '@src/types/onyx/Transaction'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import currencyList from './currencyList.json'; @@ -25,34 +27,35 @@ describe('IOUUtils', () => { }); test('Requesting money offline in a different currency will show the pending conversion message', () => { - const iouReport = ReportUtils.buildOptimisticIOUReport(1, 2, 100, 1, 'USD'); + const iouReport = ReportUtils.buildOptimisticIOUReport(1, 2, 100, '1', 'USD'); const usdPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', iouReport.reportID); const aedPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'AED', iouReport.reportID); + const MergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`, NullishDeep> = {}; + MergeQueries[`${ONYXKEYS.COLLECTION.TRANSACTION}${usdPendingTransaction.transactionID}`] = usdPendingTransaction; + MergeQueries[`${ONYXKEYS.COLLECTION.TRANSACTION}${aedPendingTransaction.transactionID}`] = aedPendingTransaction; - return Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION, { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${usdPendingTransaction.transactionID}`]: usdPendingTransaction, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${aedPendingTransaction.transactionID}`]: aedPendingTransaction, - }).then(() => { + return Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION, MergeQueries).then(() => { // We requested money offline in a different currency, we don't know the total of the iouReport until we're back online expect(IOUUtils.isIOUReportPendingCurrencyConversion(iouReport)).toBe(true); }); }); test('Requesting money online in a different currency will not show the pending conversion message', () => { - const iouReport = ReportUtils.buildOptimisticIOUReport(2, 3, 100, 1, 'USD'); + const iouReport = ReportUtils.buildOptimisticIOUReport(2, 3, 100, '1', 'USD'); const usdPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', iouReport.reportID); const aedPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'AED', iouReport.reportID); - return Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION, { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${usdPendingTransaction.transactionID}`]: { - ...usdPendingTransaction, - pendingAction: null, - }, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${aedPendingTransaction.transactionID}`]: { - ...aedPendingTransaction, - pendingAction: null, - }, - }).then(() => { + const MergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`, NullishDeep> = {}; + MergeQueries[`${ONYXKEYS.COLLECTION.TRANSACTION}${usdPendingTransaction.transactionID}`] = { + ...usdPendingTransaction, + pendingAction: null, + }; + MergeQueries[`${ONYXKEYS.COLLECTION.TRANSACTION}${aedPendingTransaction.transactionID}`] = { + ...aedPendingTransaction, + pendingAction: null, + }; + + return Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION, MergeQueries).then(() => { // We requested money online in a different currency, we know the iouReport total and there's no need to show the pending conversion message expect(IOUUtils.isIOUReportPendingCurrencyConversion(iouReport)).toBe(false); }); diff --git a/tests/unit/SidebarTest.js b/tests/unit/SidebarTest.ts similarity index 62% rename from tests/unit/SidebarTest.js rename to tests/unit/SidebarTest.ts index 6a813ef1fa8c..4a3be66ebe37 100644 --- a/tests/unit/SidebarTest.js +++ b/tests/unit/SidebarTest.ts @@ -1,35 +1,23 @@ import {cleanup, screen} from '@testing-library/react-native'; -import lodashGet from 'lodash/get'; +import type {NullishDeep} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import CONST from '../../src/CONST'; -import * as Localize from '../../src/libs/Localize'; +import CONST from '@src/CONST'; +import * as Localize from '@src/libs/Localize'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; // Be sure to include the mocked Permissions and Expensicons libraries as well as the usePermissions hook or else the beta tests won't work -jest.mock('../../src/libs/Permissions'); -jest.mock('../../src/hooks/usePermissions.ts'); -jest.mock('../../src/components/Icon/Expensicons'); - -const ONYXKEYS = { - PERSONAL_DETAILS_LIST: 'personalDetailsList', - IS_LOADING_APP: 'isLoadingApp', - NVP_PRIORITY_MODE: 'nvp_priorityMode', - SESSION: 'session', - BETAS: 'betas', - COLLECTION: { - REPORT: 'report_', - REPORT_ACTIONS: 'reportActions_', - }, - NETWORK: 'network', -}; +jest.mock('@src/libs/Permissions'); +jest.mock('@src/hooks/usePermissions.ts'); +jest.mock('@src/components/Icon/Expensicons'); describe('Sidebar', () => { beforeAll(() => Onyx.init({ keys: ONYXKEYS, - registerStorageEventListener: () => {}, safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], }), ); @@ -50,6 +38,7 @@ describe('Sidebar', () => { describe('archived chats', () => { it('renders the archive reason as the preview message of the chat', () => { const report = { + // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com'], 3, true), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, statusNum: CONST.REPORT.STATUS_NUM.CLOSED, @@ -57,6 +46,7 @@ describe('Sidebar', () => { }; const action = { + // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. ...LHNTestUtils.getFakeReportAction('email1@test.com', 3, true), actionName: 'CLOSED', originalMessage: { @@ -70,29 +60,40 @@ describe('Sidebar', () => { return ( waitForBatchedUpdates() // When Onyx is updated with the data and the sidebar re-renders - .then(() => - Onyx.multiSet({ + .then(() => { + const reportCollection = { + // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. + [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + } as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep>; + + const reportAction = { + // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionId]: action}, + } as Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`, NullishDeep>; + + return Onyx.multiSet({ [ONYXKEYS.BETAS]: betas, [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_APP]: false, - [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionId]: action}, - }), - ) + ...reportCollection, + ...reportAction, + }); + }) .then(() => { const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNames = screen.queryAllByLabelText(hintText); - expect(lodashGet(displayNames, [0, 'props', 'children', 0])).toBe('Report (archived)'); + expect(displayNames[0].props.children[0]).toBe('Report (archived)'); const hintMessagePreviewText = Localize.translateLocal('accessibilityHints.lastChatMessagePreview'); const messagePreviewTexts = screen.queryAllByLabelText(hintMessagePreviewText); - expect(lodashGet(messagePreviewTexts, [0, 'props', 'children'])).toBe('This chat room has been archived.'); + expect(messagePreviewTexts[0].props.children).toBe('This chat room has been archived.'); }) ); }); it('renders the policy deleted archive reason as the preview message of the chat', () => { const report = { + // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com'], 3, true), policyName: 'Vikings Policy', chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, @@ -100,6 +101,7 @@ describe('Sidebar', () => { stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; const action = { + // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. ...LHNTestUtils.getFakeReportAction('email1@test.com', 3, true), actionName: 'CLOSED', originalMessage: { @@ -114,26 +116,34 @@ describe('Sidebar', () => { return ( waitForBatchedUpdates() // When Onyx is updated with the data and the sidebar re-renders - .then(() => - Onyx.multiSet({ + .then(() => { + const reportCollection = { + // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. + [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + } as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep>; + + const reportAction = { + // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionId]: action}, + } as Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`, NullishDeep>; + + return Onyx.multiSet({ [ONYXKEYS.BETAS]: betas, [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_APP]: false, - [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionId]: action}, - }), - ) + ...reportCollection, + ...reportAction, + }); + }) .then(() => { const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNames = screen.queryAllByLabelText(hintText); - expect(lodashGet(displayNames, [0, 'props', 'children', 0])).toBe('Report (archived)'); + expect(displayNames[0].props.children[0]).toBe('Report (archived)'); const hintMessagePreviewText = Localize.translateLocal('accessibilityHints.lastChatMessagePreview'); const messagePreviewTexts = screen.queryAllByLabelText(hintMessagePreviewText); - expect(lodashGet(messagePreviewTexts, [0, 'props', 'children'])).toBe( - 'This workspace chat is no longer active because Vikings Policy is no longer an active workspace.', - ); + expect(messagePreviewTexts[0].props.children).toBe('This workspace chat is no longer active because Vikings Policy is no longer an active workspace.'); }) ); }); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js deleted file mode 100644 index 9059041afd19..000000000000 --- a/tests/utils/TestHelper.js +++ /dev/null @@ -1,244 +0,0 @@ -import Str from 'expensify-common/lib/str'; -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import CONST from '../../src/CONST'; -import * as Session from '../../src/libs/actions/Session'; -import HttpUtils from '../../src/libs/HttpUtils'; -import * as NumberUtils from '../../src/libs/NumberUtils'; -import ONYXKEYS from '../../src/ONYXKEYS'; -import waitForBatchedUpdates from './waitForBatchedUpdates'; - -/** - * @param {String} login - * @param {Number} accountID - * @param {String} [firstName] - * @returns {Object} - */ -function buildPersonalDetails(login, accountID, firstName = 'Test') { - return { - accountID, - login, - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_7.png', - avatarThumbnail: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_7.png', - displayName: `${firstName} User`, - firstName, - lastName: 'User', - pronouns: '', - timezone: CONST.DEFAULT_TIME_ZONE, - phoneNumber: '', - }; -} - -/** - * Simulate signing in and make sure all API calls in this flow succeed. Every time we add - * a mockImplementationOnce() we are altering what Network.post() will return. - * - * @param {Number} [accountID] - * @param {String} [login] - * @param {String} [password] - * @param {String} [authToken] - * @param {String} [firstName] - * @return {Promise} - */ -function signInWithTestUser(accountID = 1, login = 'test@user.com', password = 'Password1', authToken = 'asdfqwerty', firstName = 'Test') { - const originalXhr = HttpUtils.xhr; - HttpUtils.xhr = jest.fn(); - HttpUtils.xhr.mockResolvedValue({ - onyxData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CREDENTIALS, - value: { - login, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.ACCOUNT, - value: { - validated: true, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: { - [accountID]: buildPersonalDetails(login, accountID, firstName), - }, - }, - ], - jsonCode: 200, - }); - - // Simulate user entering their login and populating the credentials.login - Session.beginSignIn(login); - return waitForBatchedUpdates() - .then(() => { - // Response is the same for calls to Authenticate and BeginSignIn - HttpUtils.xhr.mockResolvedValue({ - onyxData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.SESSION, - value: { - authToken, - accountID, - email: login, - encryptedAuthToken: authToken, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CREDENTIALS, - value: { - autoGeneratedLogin: Str.guid('expensify.cash-'), - autoGeneratedPassword: Str.guid(), - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.USER, - value: { - isUsingExpensifyCard: false, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.BETAS, - value: ['all'], - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID, - value: 'randomID', - }, - ], - jsonCode: 200, - }); - Session.signIn(password); - return waitForBatchedUpdates(); - }) - .then(() => { - HttpUtils.xhr = originalXhr; - }); -} - -function signOutTestUser() { - const originalXhr = HttpUtils.xhr; - HttpUtils.xhr = jest.fn(); - HttpUtils.xhr.mockResolvedValue({jsonCode: 200}); - Session.signOutAndRedirectToSignIn(); - return waitForBatchedUpdates().then(() => (HttpUtils.xhr = originalXhr)); -} - -/** - * Use for situations where fetch() is required. This mock is stateful and has some additional methods to control its behavior: - * - * - pause() – stop resolving promises until you call resume() - * - resume() - flush the queue of promises, and start resolving new promises immediately - * - fail() - start returning a failure response - * - success() - go back to returning a success response - * - * @example - * - * beforeAll(() => { - * global.fetch = TestHelper.getGlobalFetchMock(); - * }); - * - * @returns {Function} - */ -function getGlobalFetchMock() { - const queue = []; - let isPaused = false; - let shouldFail = false; - - const getResponse = () => - shouldFail - ? { - ok: true, - json: () => Promise.resolve({jsonCode: 400}), - } - : { - ok: true, - json: () => Promise.resolve({jsonCode: 200}), - }; - - const mockFetch = jest.fn().mockImplementation(() => { - if (!isPaused) { - return Promise.resolve(getResponse()); - } - return new Promise((resolve) => queue.push(resolve)); - }); - - mockFetch.pause = () => (isPaused = true); - mockFetch.resume = () => { - isPaused = false; - _.each(queue, (resolve) => resolve(getResponse())); - return waitForBatchedUpdates(); - }; - mockFetch.fail = () => (shouldFail = true); - mockFetch.succeed = () => (shouldFail = false); - - return mockFetch; -} - -/** - * @param {String} login - * @param {Number} accountID - * @returns {Promise} - */ -function setPersonalDetails(login, accountID) { - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { - [accountID]: buildPersonalDetails(login, accountID), - }); - return waitForBatchedUpdates(); -} - -/** - * @param {String} created - * @param {Number} actorAccountID - * @param {String} actionID - * @returns {Object} - */ -function buildTestReportComment(created, actorAccountID, actionID = null) { - const reportActionID = actionID || NumberUtils.rand64(); - return { - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - person: [{type: 'TEXT', style: 'strong', text: 'User B'}], - created, - message: [{type: 'COMMENT', html: `Comment ${actionID}`, text: `Comment ${actionID}`}], - reportActionID, - actorAccountID, - }; -} - -function assertFormDataMatchesObject(formData, obj) { - expect(_.reduce(Array.from(formData.entries()), (memo, x) => ({...memo, [x[0]]: x[1]}), {})).toEqual(expect.objectContaining(obj)); -} - -/** - * This is a helper function to create a mock for the addListener function of the react-navigation library. - * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate - * the transitionEnd event that is triggered when the screen transition animation is completed. - * - * @returns {Object} An object with two functions: triggerTransitionEnd and addListener - */ -const createAddListenerMock = () => { - const transitionEndListeners = []; - const triggerTransitionEnd = () => { - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - }; - - const addListener = jest.fn().mockImplementation((listener, callback) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - _.filter(transitionEndListeners, (cb) => cb !== callback); - }; - }); - - return {triggerTransitionEnd, addListener}; -}; - -export {getGlobalFetchMock, signInWithTestUser, signOutTestUser, setPersonalDetails, buildPersonalDetails, buildTestReportComment, assertFormDataMatchesObject, createAddListenerMock}; diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts new file mode 100644 index 000000000000..edef0bd55e5f --- /dev/null +++ b/tests/utils/TestHelper.ts @@ -0,0 +1,242 @@ +import Str from 'expensify-common/lib/str'; +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import * as Session from '@src/libs/actions/Session'; +import HttpUtils from '@src/libs/HttpUtils'; +import * as NumberUtils from '@src/libs/NumberUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {PersonalDetails, Report} from '@src/types/onyx'; +import waitForBatchedUpdates from './waitForBatchedUpdates'; + +type MockFetch = ReturnType & {pause?: () => void; fail?: () => void; succeed?: () => void; resume?: () => Promise}; + +type Response = {ok: boolean; json: () => Promise<{jsonCode: number}>}; +type QueueItem = (value: Response | PromiseLike) => void; + +type FormData = { + entries: () => Array<[string, string | Blob]>; +}; + +type Listener = () => void; + +function buildPersonalDetails(login: string, accountID: number, firstName = 'Test'): PersonalDetails { + return { + accountID, + login, + avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_7.png', + avatarThumbnail: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_7.png', + displayName: `${firstName} User`, + firstName, + lastName: 'User', + pronouns: '', + timezone: CONST.DEFAULT_TIME_ZONE, + phoneNumber: '', + }; +} + +/** + * Simulate signing in and make sure all API calls in this flow succeed. Every time we add + * a mockImplementationOnce() we are altering what Network.post() will return. + */ +function signInWithTestUser(accountID = 1, login = 'test@user.com', password = 'Password1', authToken = 'asdfqwerty', firstName = 'Test') { + const originalXhr = HttpUtils.xhr; + + HttpUtils.xhr = jest.fn().mockImplementation(() => { + // Your mocked response object + const mockedResponse = { + onyxData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CREDENTIALS, + value: { + login, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + validated: true, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [accountID]: buildPersonalDetails(login, accountID, firstName), + }, + }, + ], + jsonCode: 200, + }; + + // Return a Promise that resolves with the mocked response + return Promise.resolve(mockedResponse); + }); + + // Simulate user entering their login and populating the credentials.login + Session.beginSignIn(login); + return waitForBatchedUpdates() + .then(() => { + // Response is the same for calls to Authenticate and BeginSignIn + HttpUtils.xhr = jest.fn().mockImplementation(() => { + // Your mocked response object + const mockedResponse = { + onyxData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SESSION, + value: { + authToken, + accountID, + email: login, + encryptedAuthToken: authToken, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CREDENTIALS, + value: { + autoGeneratedLogin: Str.guid('expensify.cash-'), + autoGeneratedPassword: Str.guid(), + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.USER, + value: { + isUsingExpensifyCard: false, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.BETAS, + value: ['all'], + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID, + value: 'randomID', + }, + ], + jsonCode: 200, + }; + + // Return a Promise that resolves with the mocked response + return Promise.resolve(mockedResponse); + }); + Session.signIn(password); + return waitForBatchedUpdates(); + }) + .then(() => { + HttpUtils.xhr = originalXhr; + }); +} + +function signOutTestUser() { + const originalXhr = HttpUtils.xhr; + HttpUtils.xhr = jest.fn().mockImplementation(() => { + // Your mocked response object + const mockedResponse = { + jsonCode: 200, + }; + + // Return a Promise that resolves with the mocked response + return Promise.resolve(mockedResponse); + }); + Session.signOutAndRedirectToSignIn(); + return waitForBatchedUpdates().then(() => (HttpUtils.xhr = originalXhr)); +} + +/** + * Use for situations where fetch() is required. This mock is stateful and has some additional methods to control its behavior: + * + * - pause() – stop resolving promises until you call resume() + * - resume() - flush the queue of promises, and start resolving new promises immediately + * - fail() - start returning a failure response + * - success() - go back to returning a success response + */ +function getGlobalFetchMock() { + const queue: QueueItem[] = []; + let isPaused = false; + let shouldFail = false; + + const getResponse = () => + shouldFail + ? { + ok: true, + json: () => Promise.resolve({jsonCode: 400}), + } + : { + ok: true, + json: () => Promise.resolve({jsonCode: 200}), + }; + + const mockFetch: MockFetch = jest.fn().mockImplementation(() => { + if (!isPaused) { + return Promise.resolve(getResponse()); + } + return new Promise((resolve) => queue.push(resolve)); + }); + + mockFetch.pause = () => (isPaused = true); + mockFetch.resume = () => { + isPaused = false; + queue.forEach((resolve) => resolve(getResponse())); + return waitForBatchedUpdates(); + }; + mockFetch.fail = () => (shouldFail = true); + mockFetch.succeed = () => (shouldFail = false); + + return mockFetch; +} + +function setPersonalDetails(login: string, accountID: number) { + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [accountID]: buildPersonalDetails(login, accountID), + }); + return waitForBatchedUpdates(); +} + +function buildTestReportComment(created: string, actorAccountID: number, actionID: string | null = null) { + const reportActionID = actionID ?? NumberUtils.rand64(); + return { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + person: [{type: 'TEXT', style: 'strong', text: 'User B'}], + created, + message: [{type: 'COMMENT', html: `Comment ${actionID}`, text: `Comment ${actionID}`}], + reportActionID, + actorAccountID, + }; +} + +function assertFormDataMatchesObject(formData: FormData, obj: Report) { + expect(Array.from(formData.entries()).reduce((memo, x) => ({...memo, [x[0]]: x[1]}), {})).toEqual(expect.objectContaining(obj)); +} + +/** + * This is a helper function to create a mock for the addListener function of the react-navigation library. + * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate + * the transitionEnd event that is triggered when the screen transition animation is completed. + * + * @returns An object with two functions: triggerTransitionEnd and addListener + */ +const createAddListenerMock = () => { + const transitionEndListeners: Listener[] = []; + const triggerTransitionEnd = () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); + }; + + const addListener = jest.fn().mockImplementation((listener, callback) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + transitionEndListeners.filter((cb) => cb !== callback); + }; + }); + + return {triggerTransitionEnd, addListener}; +}; + +export {getGlobalFetchMock, signInWithTestUser, signOutTestUser, setPersonalDetails, buildPersonalDetails, buildTestReportComment, assertFormDataMatchesObject, createAddListenerMock}; From 703bd25dd4354abcddac108d5f7b6384691d68c1 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Wed, 28 Feb 2024 18:31:21 +0000 Subject: [PATCH 050/218] [TS migration] Fixed compare --- tests/e2e/compare/compare.ts | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/tests/e2e/compare/compare.ts b/tests/e2e/compare/compare.ts index cf74b1240bf8..bc3d7fae23ea 100644 --- a/tests/e2e/compare/compare.ts +++ b/tests/e2e/compare/compare.ts @@ -14,11 +14,7 @@ type Entry = { mean?: number; }; -type Result = { - name: string; - current?: Entry; - baseline?: Entry; -}; +type Metric = Record; /* * base implementation from: https://github.com/callstack/reassure/blob/main/packages/reassure-compare/src/compare.ts @@ -63,13 +59,13 @@ function buildCompareEntry(name: string, compare: Stats, baseline: Stats): Entry /** * Compare results between baseline and current entries and categorize. */ -function compareResults(compareEntries: Record, baselineEntries: Record) { +function compareResults(compareEntries: Metric, baselineEntries: Metric) { // Unique test scenario names - const names: string[] = [...new Set([...Object(compareEntries).keys(), ...Object(baselineEntries ?? {}).keys()])]; + const compareKeys = Object.keys(compareEntries); + const baselineKeys = baselineEntries ? Object.keys(baselineEntries) : []; + const names = Array.from(new Set([...compareKeys, ...baselineKeys])); const compared: Entry[] = []; - const added: Result[] = []; - const removed: Result[] = []; names.forEach((name: string) => { const current = compareEntries[name]; @@ -80,16 +76,6 @@ function compareResults(compareEntries: Record, baselineEntries: if (baseline && current) { compared.push(buildCompareEntry(name, deltaStats, currentStats)); - } else if (current) { - added.push({ - name, - current, - }); - } else if (baseline) { - removed.push({ - name, - baseline, - }); } }); @@ -97,18 +83,13 @@ function compareResults(compareEntries: Record, baselineEntries: const meaningless = compared.filter((item) => !item.isDurationDiffOfSignificance); - added.sort((a, b) => (b?.current?.mean ?? 0) - (a.current?.mean ?? 0)); - removed.sort((a, b) => (b.baseline?.mean ?? 0) - (a.baseline?.mean ?? 0)); - return { significance, meaningless, - added, - removed, }; } -export default (main: Record, delta: Record, outputFile: string, outputFormat = 'all') => { +export default (main: Metric, delta: Metric, outputFile: string, outputFormat = 'all') => { // IMPORTANT NOTE: make sure you are passing the delta/compare results first, then the main/baseline results: const outputData = compareResults(delta, main); From 6b655b588b8c36e80909c6e12dd020ef46cfef39 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Thu, 29 Feb 2024 09:41:04 +0000 Subject: [PATCH 051/218] [TS migratio] Lint --- tests/e2e/measure/math.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/measure/math.ts b/tests/e2e/measure/math.ts index c9c0219ef1fd..1cb6661007ea 100644 --- a/tests/e2e/measure/math.ts +++ b/tests/e2e/measure/math.ts @@ -49,4 +49,4 @@ const getStats = (entries: Entries): Stats => { }; export default getStats; -export type {Stats} +export type {Stats}; From 4e3edb22f06a3c2a43afa5c34744f037ada71479 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 29 Feb 2024 13:41:52 +0100 Subject: [PATCH 052/218] fix: typecheck --- src/components/MoneyRequestConfirmationList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 773e98b6462e..ff355985b071 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -240,7 +240,7 @@ function MoneyRequestConfirmationList({ const taxRates = policy?.taxRates; // A flag for showing the categories field - const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(policyCategories ?? {})); // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); From c66339c5ee848f074ef98f3e84a2267d9f98e05d Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 29 Feb 2024 17:02:27 +0000 Subject: [PATCH 053/218] refactor(typescript): apply pull request suggestions --- src/types/utils/CollectionDataSet.ts | 19 +++++ tests/perf-test/SidebarLinks.perf-test.tsx | 5 +- tests/unit/OptionsListUtilsTest.ts | 4 +- tests/unit/ReportUtilsTest.ts | 92 +++++++++++----------- 4 files changed, 71 insertions(+), 49 deletions(-) diff --git a/src/types/utils/CollectionDataSet.ts b/src/types/utils/CollectionDataSet.ts index fc95a69dc9bf..05a0843b6e9b 100644 --- a/src/types/utils/CollectionDataSet.ts +++ b/src/types/utils/CollectionDataSet.ts @@ -1,6 +1,25 @@ +import type {OnyxEntry} from 'react-native-onyx'; import type {OnyxCollectionKey, OnyxCollectionValuesMapping} from '@src/ONYXKEYS'; /** Helps with typing a collection item update inside Onyx.multiSet call */ type CollectionDataSet = Record<`${TCollectionKey}${string}`, OnyxCollectionValuesMapping[TCollectionKey]>; +const toCollectionDataSet = ( + collectionKey: TCollectionKey, + collection: Array>, + idSelector: (collectionValue: OnyxCollectionValuesMapping[TCollectionKey]) => string, +) => { + const collectionDataSet = collection.reduce>((result, collectionValue) => { + if (collectionValue) { + // eslint-disable-next-line no-param-reassign + result[`${collectionKey}${idSelector(collectionValue)}`] = collectionValue; + } + return result; + }, {} as CollectionDataSet); + + return collectionDataSet; +}; + export default CollectionDataSet; + +export {toCollectionDataSet}; diff --git a/tests/perf-test/SidebarLinks.perf-test.tsx b/tests/perf-test/SidebarLinks.perf-test.tsx index c19a34517f45..473e854322d5 100644 --- a/tests/perf-test/SidebarLinks.perf-test.tsx +++ b/tests/perf-test/SidebarLinks.perf-test.tsx @@ -16,9 +16,8 @@ jest.mock('@components/Icon/Expensicons'); jest.mock('@react-navigation/native'); const getMockedReportsMap = (length = 100) => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const mockReports = Array.from({length}, (_, i) => { - const reportID = i + 1; + const mockReports = Array.from({length}, (value, index) => { + const reportID = index + 1; const participants = [1, 2]; const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; const report = LHNTestUtils.getFakeReport(participants, 1, true); diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index dfc953577bfa..02402e7d7bd2 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -623,7 +623,9 @@ describe('OptionsListUtils', () => { if (!ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { return reports; } - return {...reports, [reportKey]: report}; + // eslint-disable-next-line no-param-reassign + reports[reportKey] = report; + return reports; }, {}); // When we pass an empty search value diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index b8375700304c..ce60ffeaad68 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -1,9 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report} from '@src/types/onyx'; +import type {Policy, Report, ReportAction} from '@src/types/onyx'; +import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -45,7 +47,7 @@ const participantsPersonalDetails = { }, } as const; -const policy = { +const policy: Policy = { id: '1', name: 'Vikings Policy', role: 'user', @@ -53,17 +55,18 @@ const policy = { owner: '', outputCurrency: '', isPolicyExpenseChatEnabled: false, -} as const; +}; Onyx.init({keys: ONYXKEYS}); describe('ReportUtils', () => { beforeAll(() => { + const policyCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.POLICY, [policy], (current) => current.id); Onyx.multiSet({ [ONYXKEYS.PERSONAL_DETAILS_LIST]: participantsPersonalDetails, [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID: currentUserAccountID}, [ONYXKEYS.COUNTRY_CODE]: 1, - [`${ONYXKEYS.COLLECTION.POLICY}${policy.id}` as const]: policy, + ...policyCollectionDataSet, }); return waitForBatchedUpdates(); }); @@ -268,14 +271,14 @@ describe('ReportUtils', () => { }); it('returns false when the matched IOU report does not have an owner accountID', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), ownerAccountID: undefined, }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(false); }); it('returns false when the linked iou report has an oustanding IOU', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), iouReportID: '1', }; Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, { @@ -287,7 +290,7 @@ describe('ReportUtils', () => { }); it('returns false when the report has no outstanding IOU but is waiting for a bank account and the logged user is the report owner', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), ownerAccountID: currentUserAccountID, isWaitingOnBankAccount: true, }; @@ -295,7 +298,7 @@ describe('ReportUtils', () => { }); it('returns false when the report has outstanding IOU and is not waiting for a bank account and the logged user is the report owner', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), ownerAccountID: currentUserAccountID, isWaitingOnBankAccount: false, }; @@ -303,7 +306,7 @@ describe('ReportUtils', () => { }); it('returns false when the report has no oustanding IOU but is waiting for a bank account and the logged user is not the report owner', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), ownerAccountID: 97, isWaitingOnBankAccount: true, }; @@ -311,14 +314,14 @@ describe('ReportUtils', () => { }); it('returns true when the report has an unread mention', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), isUnreadWithMention: true, }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(true); }); it('returns true when the report is an outstanding task', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.TASK, managerID: currentUserAccountID, isUnreadWithMention: false, @@ -329,7 +332,7 @@ describe('ReportUtils', () => { }); it('returns true when the report has oustanding child request', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), ownerAccountID: 99, hasOutstandingChildRequest: true, isWaitingOnBankAccount: false, @@ -363,7 +366,7 @@ describe('ReportUtils', () => { it('it is a room with no participants except self', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); @@ -372,7 +375,7 @@ describe('ReportUtils', () => { it('its not your policy expense chat', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, isOwnPolicyExpenseChat: false, }; @@ -382,7 +385,7 @@ describe('ReportUtils', () => { it('its paid IOU report', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.IOU, statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, }; @@ -392,7 +395,7 @@ describe('ReportUtils', () => { it('its approved Expense report', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.APPROVED, statusNum: CONST.REPORT.STATUS_NUM.APPROVED, @@ -403,7 +406,7 @@ describe('ReportUtils', () => { it('its paid Expense report', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.EXPENSE, statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, }; @@ -417,7 +420,7 @@ describe('ReportUtils', () => { isOwnPolicyExpenseChat: false, }).then(() => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), parentReportID: '100', type: CONST.REPORT.TYPE.EXPENSE, }; @@ -433,7 +436,7 @@ describe('ReportUtils', () => { isOwnPolicyExpenseChat: true, }).then(() => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, @@ -463,7 +466,7 @@ describe('ReportUtils', () => { CONST.REPORT.CHAT_TYPE.POLICY_ROOM, ].every((chatType) => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), chatType, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); @@ -474,7 +477,7 @@ describe('ReportUtils', () => { it('has multiple participants excluding self', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs]); @@ -484,7 +487,7 @@ describe('ReportUtils', () => { it('user has send money permission', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs]); @@ -494,7 +497,7 @@ describe('ReportUtils', () => { it("it's a group DM report", () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.CHAT, participantsAccountIDs: [currentUserAccountID, ...participantsAccountIDs], }; @@ -512,7 +515,7 @@ describe('ReportUtils', () => { isOwnPolicyExpenseChat: true, }).then(() => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), parentReportID: '102', type: CONST.REPORT.TYPE.EXPENSE, }; @@ -529,7 +532,7 @@ describe('ReportUtils', () => { isOwnPolicyExpenseChat: true, }).then(() => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN, @@ -551,7 +554,7 @@ describe('ReportUtils', () => { it('it is an IOU report in submitted state', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.IOU, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, @@ -563,7 +566,7 @@ describe('ReportUtils', () => { it('it is an IOU report in submitted state even with send money permissions', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.IOU, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, @@ -577,7 +580,7 @@ describe('ReportUtils', () => { describe('return multiple money request option if', () => { it("it is user's own policy expense chat", () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, isOwnPolicyExpenseChat: true, }; @@ -589,7 +592,7 @@ describe('ReportUtils', () => { it('it is a 1:1 DM', () => { const report = { - ...(LHNTestUtils.getFakeReport() as Report), + ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.CHAT, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); @@ -618,7 +621,7 @@ describe('ReportUtils', () => { describe('sortReportsByLastRead', () => { it('should filter out report without reportID & lastReadTime and sort lastReadTime in ascending order', () => { - const reports = [ + const reports: Array> = [ {reportID: '1', lastReadTime: '2023-07-08 07:15:44.030'}, {reportID: '2', lastReadTime: undefined}, {reportID: '3', lastReadTime: '2023-07-06 07:15:44.030'}, @@ -627,42 +630,41 @@ describe('ReportUtils', () => { {reportID: '6'}, null, ]; - const sortedReports = [ + const sortedReports: Array> = [ {reportID: '3', lastReadTime: '2023-07-06 07:15:44.030'}, {reportID: '4', lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU}, {reportID: '1', lastReadTime: '2023-07-08 07:15:44.030'}, - ] as const; + ]; expect(ReportUtils.sortReportsByLastRead(reports, null)).toEqual(sortedReports); }); }); describe('getAllAncestorReportActions', () => { - const reports = [ + const reports: Report[] = [ {reportID: '1', lastReadTime: '2024-02-01 04:56:47.233', reportName: 'Report'}, {reportID: '2', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '1', parentReportID: '1', reportName: 'Report'}, {reportID: '3', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '2', parentReportID: '2', reportName: 'Report'}, {reportID: '4', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '3', parentReportID: '3', reportName: 'Report'}, {reportID: '5', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '4', parentReportID: '4', reportName: 'Report'}, - ] as const; + ]; - const reportActions = [ + const reportActions: ReportAction[] = [ {reportActionID: '1', created: '2024-02-01 04:42:22.965', actionName: 'MARKEDREIMBURSED'}, {reportActionID: '2', created: '2024-02-01 04:42:28.003', actionName: 'MARKEDREIMBURSED'}, {reportActionID: '3', created: '2024-02-01 04:42:31.742', actionName: 'MARKEDREIMBURSED'}, {reportActionID: '4', created: '2024-02-01 04:42:35.619', actionName: 'MARKEDREIMBURSED'}, - ] as const; + ]; beforeAll(() => { + const reportCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.REPORT, reports, (report) => report.reportID); + const reportActionCollectionDataSet = toCollectionDataSet( + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + reportActions.map((reportAction) => ({[reportAction.reportActionID]: reportAction})), + (actions) => Object.values(actions)[0].reportActionID, + ); Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT}${reports[0].reportID}` as const]: reports[0], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[1].reportID}` as const]: reports[1], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[2].reportID}` as const]: reports[2], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[3].reportID}` as const]: reports[3], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[4].reportID}` as const]: reports[4], - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[0].reportID}` as const]: {[reportActions[0].reportActionID]: reportActions[0]}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[1].reportID}` as const]: {[reportActions[1].reportActionID]: reportActions[1]}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[2].reportID}` as const]: {[reportActions[2].reportActionID]: reportActions[2]}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[3].reportID}` as const]: {[reportActions[3].reportActionID]: reportActions[3]}, + ...reportCollectionDataSet, + ...reportActionCollectionDataSet, }); return waitForBatchedUpdates(); }); From 2a76abbd50660501041cc54a1c32a594594b8036 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 29 Feb 2024 17:08:34 +0000 Subject: [PATCH 054/218] refactor(typescript): resolve type error --- src/libs/DateUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 6da5c8af1ff2..0c8a32cbad23 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -265,7 +265,7 @@ function formatToLongDateWithWeekday(datetime: string | Date): string { * * @returns Sunday */ -function formatToDayOfWeek(datetime: Date): string { +function formatToDayOfWeek(datetime: string | Date): string { return format(new Date(datetime), CONST.DATE.WEEKDAY_TIME_FORMAT); } From af3fd5581859a9e408a15daba5a27d1c069e9186 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Fri, 1 Mar 2024 12:04:48 +0000 Subject: [PATCH 055/218] [TS migration][G3] Feedback --- src/types/onyx/ReportAction.ts | 6 +++++- src/types/onyx/Transaction.ts | 18 +++++++++++++++++- tests/e2e/compare/compare.ts | 5 ++--- tests/unit/CurrencyUtilsTest.ts | 4 ---- tests/unit/IOUUtilsTest.ts | 7 +++---- tests/unit/SidebarTest.ts | 30 ++++++++++++------------------ tests/utils/TestHelper.ts | 10 +++++++++- 7 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index bb5bf50ec6cf..0971fb6b77e1 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -2,6 +2,8 @@ import type {ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; import type {AvatarSource} from '@libs/UserUtils'; import type CONST from '@src/CONST'; +import type ONYXKEYS from '@src/ONYXKEYS'; +import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import type * as OnyxCommon from './OnyxCommon'; import type {Decision, Reaction} from './OriginalMessage'; @@ -224,5 +226,7 @@ type ReportAction = ReportActionBase & OriginalMessage; type ReportActions = Record; +type ReportActionCollectionDataSet = CollectionDataSet; + export default ReportAction; -export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage}; +export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage, ReportActionCollectionDataSet}; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 1a7541955720..709597c16140 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -1,5 +1,7 @@ import type {KeysOfUnion, ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; +import type ONYXKEYS from '@src/ONYXKEYS'; +import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import type {Participant, Split} from './IOU'; import type * as OnyxCommon from './OnyxCommon'; import type RecentWaypoint from './RecentWaypoint'; @@ -224,5 +226,19 @@ type AdditionalTransactionChanges = { type TransactionChanges = Partial & AdditionalTransactionChanges; +type TransactionCollectionDataSet = CollectionDataSet; + export default Transaction; -export type {WaypointCollection, Comment, Receipt, Waypoint, ReceiptError, ReceiptErrors, TransactionPendingFieldsKey, TransactionChanges, TaxRate, ReceiptSource}; +export type { + WaypointCollection, + Comment, + Receipt, + Waypoint, + ReceiptError, + ReceiptErrors, + TransactionPendingFieldsKey, + TransactionChanges, + TaxRate, + ReceiptSource, + TransactionCollectionDataSet, +}; diff --git a/tests/e2e/compare/compare.ts b/tests/e2e/compare/compare.ts index bc3d7fae23ea..2de54b416828 100644 --- a/tests/e2e/compare/compare.ts +++ b/tests/e2e/compare/compare.ts @@ -61,9 +61,8 @@ function buildCompareEntry(name: string, compare: Stats, baseline: Stats): Entry */ function compareResults(compareEntries: Metric, baselineEntries: Metric) { // Unique test scenario names - const compareKeys = Object.keys(compareEntries); - const baselineKeys = baselineEntries ? Object.keys(baselineEntries) : []; - const names = Array.from(new Set([...compareKeys, ...baselineKeys])); + const baselineKeys = Object.keys(baselineEntries ?? {}); + const names = Array.from(new Set([...baselineKeys])); const compared: Entry[] = []; diff --git a/tests/unit/CurrencyUtilsTest.ts b/tests/unit/CurrencyUtilsTest.ts index 246af64b1d87..a1e4b03fa715 100644 --- a/tests/unit/CurrencyUtilsTest.ts +++ b/tests/unit/CurrencyUtilsTest.ts @@ -3,7 +3,6 @@ import CONST from '@src/CONST'; import * as CurrencyUtils from '@src/libs/CurrencyUtils'; import LocaleListener from '@src/libs/Localize/LocaleListener'; import ONYXKEYS from '@src/ONYXKEYS'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; // This file can get outdated. In that case, you can follow these steps to update it: // - open your browser console and navigate to the Network tab @@ -37,9 +36,6 @@ describe('CurrencyUtils', () => { describe('getLocalizedCurrencySymbol', () => { test.each(AVAILABLE_LOCALES)('Returns non empty string for all currencyCode with preferredLocale %s', (prefrredLocale) => Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, prefrredLocale).then(() => { - if (isEmptyObject(currencyCodeList)) { - return; - } currencyCodeList.forEach((currencyCode: string) => { const localizedSymbol = CurrencyUtils.getLocalizedCurrencySymbol(currencyCode); diff --git a/tests/unit/IOUUtilsTest.ts b/tests/unit/IOUUtilsTest.ts index b390d0cd70a3..33dd1ff05e13 100644 --- a/tests/unit/IOUUtilsTest.ts +++ b/tests/unit/IOUUtilsTest.ts @@ -1,10 +1,9 @@ -import type {NullishDeep} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as IOUUtils from '@src/libs/IOUUtils'; import * as ReportUtils from '@src/libs/ReportUtils'; import * as TransactionUtils from '@src/libs/TransactionUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type Transaction from '@src/types/onyx/Transaction'; +import type {TransactionCollectionDataSet} from '@src/types/onyx/Transaction'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import currencyList from './currencyList.json'; @@ -30,7 +29,7 @@ describe('IOUUtils', () => { const iouReport = ReportUtils.buildOptimisticIOUReport(1, 2, 100, '1', 'USD'); const usdPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', iouReport.reportID); const aedPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'AED', iouReport.reportID); - const MergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`, NullishDeep> = {}; + const MergeQueries: TransactionCollectionDataSet = {}; MergeQueries[`${ONYXKEYS.COLLECTION.TRANSACTION}${usdPendingTransaction.transactionID}`] = usdPendingTransaction; MergeQueries[`${ONYXKEYS.COLLECTION.TRANSACTION}${aedPendingTransaction.transactionID}`] = aedPendingTransaction; @@ -45,7 +44,7 @@ describe('IOUUtils', () => { const usdPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', iouReport.reportID); const aedPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'AED', iouReport.reportID); - const MergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`, NullishDeep> = {}; + const MergeQueries: TransactionCollectionDataSet = {}; MergeQueries[`${ONYXKEYS.COLLECTION.TRANSACTION}${usdPendingTransaction.transactionID}`] = { ...usdPendingTransaction, pendingAction: null, diff --git a/tests/unit/SidebarTest.ts b/tests/unit/SidebarTest.ts index 4a3be66ebe37..89e7a86e095a 100644 --- a/tests/unit/SidebarTest.ts +++ b/tests/unit/SidebarTest.ts @@ -5,6 +5,8 @@ import CONST from '@src/CONST'; import * as Localize from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; +import type {ReportCollectionDataSet} from '@src/types/onyx/Report'; +import type {ReportActionCollectionDataSet} from '@src/types/onyx/ReportAction'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; @@ -38,16 +40,14 @@ describe('Sidebar', () => { describe('archived chats', () => { it('renders the archive reason as the preview message of the chat', () => { const report = { - // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. - ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com'], 3, true), + ...LHNTestUtils.getFakeReport([1, 2], 3, true), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, statusNum: CONST.REPORT.STATUS_NUM.CLOSED, stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; const action = { - // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. - ...LHNTestUtils.getFakeReportAction('email1@test.com', 3, true), + ...LHNTestUtils.getFakeReportAction('email1@test.com', 3), actionName: 'CLOSED', originalMessage: { reason: CONST.REPORT.ARCHIVE_REASON.DEFAULT, @@ -62,14 +62,12 @@ describe('Sidebar', () => { // When Onyx is updated with the data and the sidebar re-renders .then(() => { const reportCollection = { - // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, - } as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep>; + } as ReportCollectionDataSet; const reportAction = { - // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionId]: action}, - } as Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`, NullishDeep>; + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionID]: action}, + } as ReportActionCollectionDataSet; return Onyx.multiSet({ [ONYXKEYS.BETAS]: betas, @@ -93,16 +91,14 @@ describe('Sidebar', () => { }); it('renders the policy deleted archive reason as the preview message of the chat', () => { const report = { - // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. - ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com'], 3, true), + ...LHNTestUtils.getFakeReport([1, 2], 3, true), policyName: 'Vikings Policy', chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, statusNum: CONST.REPORT.STATUS_NUM.CLOSED, stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; const action = { - // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. - ...LHNTestUtils.getFakeReportAction('email1@test.com', 3, true), + ...LHNTestUtils.getFakeReportAction('email1@test.com', 3), actionName: 'CLOSED', originalMessage: { policyName: 'Vikings Policy', @@ -118,14 +114,12 @@ describe('Sidebar', () => { // When Onyx is updated with the data and the sidebar re-renders .then(() => { const reportCollection = { - // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, - } as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep>; + } as ReportCollectionDataSet; const reportAction = { - // @ts-expect-error TODO: Remove this once LHNTestUtils (https://github.com/Expensify/App/issues/25320) is migrated to TypeScript. - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionId]: action}, - } as Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`, NullishDeep>; + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionID]: action}, + } as ReportActionCollectionDataSet; return Onyx.multiSet({ [ONYXKEYS.BETAS]: betas, diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index edef0bd55e5f..7f78d5080967 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -8,7 +8,15 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, Report} from '@src/types/onyx'; import waitForBatchedUpdates from './waitForBatchedUpdates'; -type MockFetch = ReturnType & {pause?: () => void; fail?: () => void; succeed?: () => void; resume?: () => Promise}; +type MockFetch = ReturnType & { + pause?: () => void; + + fail?: () => void; + + succeed?: () => void; + + resume?: () => Promise; +}; type Response = {ok: boolean; json: () => Promise<{jsonCode: number}>}; type QueueItem = (value: Response | PromiseLike) => void; From 8022a87f172b44351504cda0fc726745d3575e69 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Fri, 1 Mar 2024 12:12:10 +0000 Subject: [PATCH 056/218] [TS migration][G3] Lint fix --- tests/unit/SidebarTest.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/SidebarTest.ts b/tests/unit/SidebarTest.ts index 89e7a86e095a..c913f5a2900d 100644 --- a/tests/unit/SidebarTest.ts +++ b/tests/unit/SidebarTest.ts @@ -1,10 +1,8 @@ import {cleanup, screen} from '@testing-library/react-native'; -import type {NullishDeep} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import * as Localize from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; import type {ReportCollectionDataSet} from '@src/types/onyx/Report'; import type {ReportActionCollectionDataSet} from '@src/types/onyx/ReportAction'; import * as LHNTestUtils from '../utils/LHNTestUtils'; From 77e3d046b51e8b48a655001432713b1dff794977 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 4 Mar 2024 10:30:46 +0100 Subject: [PATCH 057/218] fix: resolve comments --- src/components/TagPicker/index.tsx | 2 +- src/libs/OptionsListUtils.ts | 5 +++-- src/types/onyx/PolicyTag.ts | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index b553e183a37f..f13c27beadc3 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -14,7 +14,7 @@ import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/ type SelectedTagOption = { name: string; - enabled?: boolean; + enabled: boolean; accountID: number | null; }; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 832e04d98051..fc765564c900 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -20,6 +20,7 @@ import type { PolicyCategories, PolicyTag, PolicyTagList, + PolicyTags, Report, ReportAction, ReportActions, @@ -125,7 +126,7 @@ type GetOptionsConfig = { categories?: PolicyCategories; recentlyUsedCategories?: string[]; includeTags?: boolean; - tags?: PolicyTagList | Array; + tags?: PolicyTags | Array; recentlyUsedTags?: string[]; canInviteUser?: boolean; includeSelectedOptions?: boolean; @@ -1814,7 +1815,7 @@ function getFilteredOptions( categories: PolicyCategories = {}, recentlyUsedCategories: string[] = [], includeTags = false, - tags: PolicyTagList | Array = {}, + tags: PolicyTags | Array = {}, recentlyUsedTags: string[] = [], canInviteUser = true, includeSelectedOptions = false, diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 0b7ccd93fcc3..b2873538cb4d 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -3,13 +3,13 @@ type PolicyTag = { name: string; /** Flag that determines if a tag is active and able to be selected */ - enabled?: boolean; + enabled: boolean; /** "General Ledger code" that corresponds to this tag in an accounting system. Similar to an ID. */ // eslint-disable-next-line @typescript-eslint/naming-convention 'GL Code'?: string; - /** Nested tags */ + /** List of tags */ tags: PolicyTags; /** Flag that determines if a tag is required */ @@ -27,7 +27,7 @@ type PolicyTagList = Record< /** Flag that determines if tags are required */ required: boolean; - /** Nested tags */ + /** List of tags */ tags: PolicyTags; /** Index by which the tag appears in the hierarchy of tags */ From 1825d3a3c0357e6a2ef29d867758f843c9df66ec Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Mon, 4 Mar 2024 09:51:58 +0000 Subject: [PATCH 058/218] [TS migration][G3] Fixed lint issue --- tests/utils/TestHelper.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 7f78d5080967..165ffc670902 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -184,7 +184,10 @@ function getGlobalFetchMock() { if (!isPaused) { return Promise.resolve(getResponse()); } - return new Promise((resolve) => queue.push(resolve)); + return new Promise((resolve) => { + queue.push(resolve); + }); + }); mockFetch.pause = () => (isPaused = true); From de503429dfab4d0bd6d8d33460a4b52304e401a7 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Mon, 4 Mar 2024 10:04:26 +0000 Subject: [PATCH 059/218] [TS migration][G3] TS fix --- tests/e2e/compare/compare.ts | 13 +------------ tests/utils/TestHelper.ts | 1 - 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/tests/e2e/compare/compare.ts b/tests/e2e/compare/compare.ts index 2de54b416828..443265802a98 100644 --- a/tests/e2e/compare/compare.ts +++ b/tests/e2e/compare/compare.ts @@ -1,19 +1,10 @@ import type {Stats} from '../measure/math'; import getStats from '../measure/math'; import * as math from './math'; +import type {Entry} from './output/console'; import printToConsole from './output/console'; import writeToMarkdown from './output/markdown'; -type Entry = { - name: string; - baseline?: Stats; - current?: Stats; - diff?: number; - relativeDurationDiff?: number; - isDurationDiffOfSignificance?: boolean; - mean?: number; -}; - type Metric = Record; /* @@ -100,5 +91,3 @@ export default (main: Metric, delta: Metric, outputFile: string, outputFormat = return writeToMarkdown(outputFile, outputData); } }; - -export type {Entry}; diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 165ffc670902..a9f72a40cf3f 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -187,7 +187,6 @@ function getGlobalFetchMock() { return new Promise((resolve) => { queue.push(resolve); }); - }); mockFetch.pause = () => (isPaused = true); From 451f83d3be867c631be0503e06dcfb12ad60753e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Mon, 4 Mar 2024 11:48:39 +0100 Subject: [PATCH 060/218] Fix resuming video after regaining internet connection --- src/components/VideoPlayer/BaseVideoPlayer.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js index 360095ec7eb4..8efc6604dfa1 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.js +++ b/src/components/VideoPlayer/BaseVideoPlayer.js @@ -8,6 +8,7 @@ import Hoverable from '@components/Hoverable'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import VideoPopoverMenu from '@components/VideoPopoverMenu'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as Browser from '@libs/Browser'; @@ -44,6 +45,7 @@ function BaseVideoPlayer({ const styles = useThemeStyles(); const {pauseVideo, playVideo, currentlyPlayingURL, updateSharedElements, sharedElement, originalParent, shareVideoPlayerElements, currentVideoPlayerRef, updateCurrentlyPlayingURL} = usePlaybackContext(); + const {isOffline} = useNetwork(); const [duration, setDuration] = useState(videoDuration * 1000); const [position, setPosition] = useState(0); const [isPlaying, setIsPlaying] = useState(false); @@ -214,13 +216,20 @@ function BaseVideoPlayer({ style={[styles.w100, styles.h100, videoPlayerStyle]} videoStyle={[styles.w100, styles.h100, videoStyle]} source={{ - uri: sourceURL, + // if video is loading and is offline, we want to change uri to null to + // reset the video player after connection is back + uri: !isLoading || (isLoading && !isOffline) ? sourceURL : null, }} shouldPlay={false} useNativeControls={false} resizeMode={resizeMode} isLooping={isLooping} - onReadyForDisplay={onVideoLoaded} + onReadyForDisplay={(e) => { + if (isCurrentlyURLSet) { + playVideo(); + } + onVideoLoaded(e); + }} onPlaybackStatusUpdate={handlePlaybackStatusUpdate} onFullscreenUpdate={handleFullscreenUpdate} /> From d104cb08a10e2404101752e15431128cb88ee083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Mon, 4 Mar 2024 11:54:07 +0100 Subject: [PATCH 061/218] Fix playing video in preview when uploading --- src/components/VideoPlayer/BaseVideoPlayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js index 8efc6604dfa1..16a210a398b4 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.js +++ b/src/components/VideoPlayer/BaseVideoPlayer.js @@ -225,7 +225,7 @@ function BaseVideoPlayer({ resizeMode={resizeMode} isLooping={isLooping} onReadyForDisplay={(e) => { - if (isCurrentlyURLSet) { + if (isCurrentlyURLSet && !isUploading) { playVideo(); } onVideoLoaded(e); From 6aa6a28e1e7c446442aea364ca06caf24bd217d4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Mar 2024 17:30:44 +0100 Subject: [PATCH 062/218] fix: remove unnecessary field from PolicyTag type --- src/types/onyx/PolicyTag.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index b2873538cb4d..8c509c66825b 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -8,12 +8,6 @@ type PolicyTag = { /** "General Ledger code" that corresponds to this tag in an accounting system. Similar to an ID. */ // eslint-disable-next-line @typescript-eslint/naming-convention 'GL Code'?: string; - - /** List of tags */ - tags: PolicyTags; - - /** Flag that determines if a tag is required */ - required: boolean; }; type PolicyTags = Record; From 41c834697fbf9625de0072827e1912e88767eaa9 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 6 Mar 2024 17:39:23 +0700 Subject: [PATCH 063/218] Fix immediately prompt for Camera permission in scan request flow --- .../step/IOURequestStepScan/index.native.js | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index 338444d473c6..de50d25a2e78 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -65,18 +65,17 @@ function IOURequestStepScan({ const camera = useRef(null); const [flash, setFlash] = useState(false); const [cameraPermissionStatus, setCameraPermissionStatus] = useState(undefined); - const askedForPermission = useRef(false); const {translate} = useLocalize(); - const askForPermissions = (showPermissionsAlert = true) => { + const askForPermissions = () => { // There's no way we can check for the BLOCKED status without requesting the permission first // https://github.com/zoontek/react-native-permissions/blob/a836e114ce3a180b2b23916292c79841a267d828/README.md?plain=1#L670 CameraPermission.requestCameraPermission() .then((status) => { setCameraPermissionStatus(status); - if (status === RESULTS.BLOCKED && showPermissionsAlert) { + if (status === RESULTS.BLOCKED) { FileUtils.showCameraPermissionsAlert(); } }) @@ -121,22 +120,14 @@ function IOURequestStepScan({ }); useEffect(() => { - const refreshCameraPermissionStatus = (shouldAskForPermission = false) => { + const refreshCameraPermissionStatus = () => { CameraPermission.getCameraPermissionStatus() - .then((res) => { - // In android device app data, the status is not set to blocked until denied twice, - // due to that the app will ask for permission twice whenever users opens uses the scan tab - setCameraPermissionStatus(res); - if (shouldAskForPermission && !askedForPermission.current) { - askedForPermission.current = true; - askForPermissions(false); - } - }) + .then(setCameraPermissionStatus) .catch(() => setCameraPermissionStatus(RESULTS.UNAVAILABLE)); }; // Check initial camera permission status - refreshCameraPermissionStatus(true); + refreshCameraPermissionStatus(); // Refresh permission status when app gain focus const subscription = AppState.addEventListener('change', (appState) => { @@ -225,7 +216,7 @@ function IOURequestStepScan({ const capturePhoto = useCallback(() => { if (!camera.current && (cameraPermissionStatus === RESULTS.DENIED || cameraPermissionStatus === RESULTS.BLOCKED)) { - askForPermissions(cameraPermissionStatus !== RESULTS.DENIED); + askForPermissions(); return; } From c90ce7e8d360788fa31be13fe94f31a123689a4f Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 6 Mar 2024 18:23:50 +0300 Subject: [PATCH 064/218] add Pdf thumbnail --- src/components/ReceiptImage.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index cd70413b7a2e..8dc82cb53aea 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -5,6 +5,7 @@ import type IconAsset from '@src/types/utils/IconAsset'; import EReceiptThumbnail from './EReceiptThumbnail'; import type {IconSize} from './EReceiptThumbnail'; import Image from './Image'; +import PDFThumbnail from './PDFThumbnail'; import ThumbnailImage from './ThumbnailImage'; type Style = {height: number; borderRadius: number; margin: number}; @@ -22,18 +23,30 @@ type ReceiptImageProps = ( /** Url of the receipt image */ source?: string; + + /** Whether it is a pdf thumbnail we are displaying */ + isPDFThumbnail?: boolean; } | { transactionID: string; isEReceipt?: boolean; isThumbnail: boolean; source?: string; + isPDFThumbnail?: boolean; + } + | { + transactionID?: string; + isEReceipt?: boolean; + isThumbnail?: boolean; + source: string; + isPDFThumbnail?: boolean; } | { transactionID?: string; isEReceipt?: boolean; isThumbnail?: boolean; source: string; + isPDFThumbnail: string; } ) & { /** Whether we should display the receipt with ThumbnailImage component */ @@ -60,6 +73,7 @@ type ReceiptImageProps = ( function ReceiptImage({ transactionID, + isPDFThumbnail = false, isThumbnail = false, shouldUseThumbnailImage = false, isEReceipt = false, @@ -73,6 +87,15 @@ function ReceiptImage({ }: ReceiptImageProps) { const styles = useThemeStyles(); + if (isPDFThumbnail) { + return ( + + ); + } + if (isEReceipt || isThumbnail) { const props = isThumbnail && {borderRadius: style?.borderRadius, fileExtension, isReceiptThumbnail: true}; return ( From 9c2cf34bfe95a1d9e73ba4b711db484c0557ab4a Mon Sep 17 00:00:00 2001 From: Rohan Sasne Date: Wed, 6 Mar 2024 21:26:28 +0530 Subject: [PATCH 065/218] Update workspace creation to paid --- src/libs/actions/Policy.ts | 6 +++--- tests/actions/PolicyTest.js | 2 +- tests/unit/SidebarFilterTest.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index aa64611b210f..00acffb81f40 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -1486,7 +1486,7 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, value: { id: policyID, - type: CONST.POLICY.TYPE.FREE, + type: CONST.POLICY.TYPE.TEAM, name: workspaceName, role: CONST.POLICY.ROLE.ADMIN, owner: sessionEmail, @@ -1547,7 +1547,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { id: policyID, - type: CONST.POLICY.TYPE.FREE, + type: CONST.POLICY.TYPE.TEAM, name: workspaceName, role: CONST.POLICY.ROLE.ADMIN, owner: sessionEmail, @@ -1742,7 +1742,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName ownerEmail: policyOwnerEmail, makeMeAdmin, policyName: workspaceName, - type: CONST.POLICY.TYPE.FREE, + type: CONST.POLICY.TYPE.TEAM, announceCreatedReportActionID, adminsCreatedReportActionID, expenseCreatedReportActionID, diff --git a/tests/actions/PolicyTest.js b/tests/actions/PolicyTest.js index 5a994aaf600e..7b9a77a7e89b 100644 --- a/tests/actions/PolicyTest.js +++ b/tests/actions/PolicyTest.js @@ -52,7 +52,7 @@ describe('actions/Policy', () => { // check if policy was created with correct values expect(policy.id).toBe(policyID); expect(policy.name).toBe(WORKSPACE_NAME); - expect(policy.type).toBe(CONST.POLICY.TYPE.FREE); + expect(policy.type).toBe(CONST.POLICY.TYPE.TEAM); expect(policy.role).toBe(CONST.POLICY.ROLE.ADMIN); expect(policy.owner).toBe(ESH_EMAIL); expect(policy.isPolicyExpenseChatEnabled).toBe(true); diff --git a/tests/unit/SidebarFilterTest.ts b/tests/unit/SidebarFilterTest.ts index 58ec66698b83..05771980dcf3 100644 --- a/tests/unit/SidebarFilterTest.ts +++ b/tests/unit/SidebarFilterTest.ts @@ -238,7 +238,7 @@ xdescribe('Sidebar', () => { // and the user not being in any betas const policy = { policyID: '1', - type: CONST.POLICY.TYPE.FREE, + type: CONST.POLICY.TYPE.TEAM, }; const report: Report = { ...LHNTestUtils.getFakeReport(), From 9f38e25e51dcfa089429348fa683bd78f7a4ea88 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 6 Mar 2024 19:07:12 +0300 Subject: [PATCH 066/218] updated to fixed color code depending on file extensions --- src/components/EReceiptThumbnail.tsx | 5 +++-- src/styles/utils/index.ts | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index f6d5beb7e24d..fb1209aa6fed 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -56,10 +56,11 @@ const backgroundImages = { function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptThumbnail = false, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const colorCode = isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction); - const backgroundImage = useMemo(() => backgroundImages[StyleUtils.getEReceiptColorCode(transaction)], [StyleUtils, transaction]); + const backgroundImage = useMemo(() => backgroundImages[colorCode], [colorCode]); - const colorStyles = StyleUtils.getEReceiptColorStyles(StyleUtils.getEReceiptColorCode(transaction)); + const colorStyles = StyleUtils.getEReceiptColorStyles(colorCode); const primaryColor = colorStyles?.backgroundColor; const secondaryColor = colorStyles?.color; const transactionDetails = ReportUtils.getTransactionDetails(transaction); diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 21af5398232f..5d3da627b411 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -287,6 +287,20 @@ function getEReceiptColorCode(transaction: OnyxEntry): EReceiptColo return eReceiptColors[colorHash]; } +/** + * Helper method to return eReceipt color code for Receipt Thumbnails + */ +function getFileExtensionColorCode(fileExtension?: string): EReceiptColorName { + switch (fileExtension) { + case CONST.IOU.FILE_TYPES.DOC: + return CONST.ERECEIPT_COLORS.PINK; + case CONST.IOU.FILE_TYPES.HTML: + return CONST.ERECEIPT_COLORS.TANGERINE; + default: + return CONST.ERECEIPT_COLORS.GREEN; + } +} + /** * Helper method to return eReceipt color styles */ @@ -1084,6 +1098,7 @@ const staticStyleUtils = { parseStyleFromFunction, getEReceiptColorStyles, getEReceiptColorCode, + getFileExtensionColorCode, getNavigationModalCardStyle, getCardStyles, getOpacityStyle, From 68c532703e40358c9e8ea387683ae6e54ccffa1b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 7 Mar 2024 10:35:57 +0100 Subject: [PATCH 067/218] fix: changed prop name to tagListName --- src/components/TagPicker/index.tsx | 6 +++--- src/pages/EditRequestTagPage.js | 2 +- src/pages/iou/request/step/IOURequestStepTag.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index f13c27beadc3..4998751309ee 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -36,7 +36,7 @@ type TagPickerProps = TagPickerOnyxProps & { selectedTag: string; /** The name of tag list we are getting tags for */ - tag: string; + tagListName: string; /** Callback to submit the selected tag */ onSubmit: () => void; @@ -54,13 +54,13 @@ type TagPickerProps = TagPickerOnyxProps & { tagIndex: number; }; -function TagPicker({selectedTag, tag, policyTags, tagIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, insets, onSubmit}: TagPickerProps) { +function TagPicker({selectedTag, tagListName, policyTags, tagIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, insets, onSubmit}: TagPickerProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); - const policyRecentlyUsedTagsList = useMemo(() => policyRecentlyUsedTags?.[tag] ?? [], [policyRecentlyUsedTags, tag]); + const policyRecentlyUsedTagsList = useMemo(() => policyRecentlyUsedTags?.[tagListName] ?? [], [policyRecentlyUsedTags, tagListName]); const policyTagList = PolicyUtils.getTagList(policyTags, tagIndex); const policyTagsCount = PolicyUtils.getCountOfEnabledTagsOfList(policyTagList.tags); const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD; diff --git a/src/pages/EditRequestTagPage.js b/src/pages/EditRequestTagPage.js index 74643afa347f..762bb76a7f57 100644 --- a/src/pages/EditRequestTagPage.js +++ b/src/pages/EditRequestTagPage.js @@ -52,7 +52,7 @@ function EditRequestTagPage({defaultTag, policyID, tagName, tagIndex, onSubmit}) {translate('iou.tagSelection', {tagName: tagName || translate('common.tag')})} {translate('iou.tagSelection', {tagName: policyTagListName})} Date: Fri, 8 Mar 2024 13:39:53 +0100 Subject: [PATCH 068/218] fix: changed prop name --- Gemfile.lock | 16 +++++++++++----- ios/Podfile.lock | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/languages/types.ts | 2 +- src/pages/EditRequestTagPage.js | 12 ++++++------ src/pages/iou/request/step/IOURequestStepTag.js | 2 +- 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index beb2c1762936..9a322c52606d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,11 +3,12 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (7.0.8) + activesupport (6.1.7.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) + zeitwerk (~> 2.3) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) @@ -80,7 +81,8 @@ GEM declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20240107) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) dotenv (2.8.1) emoji_regex (3.2.3) escape (0.0.4) @@ -187,11 +189,11 @@ GEM google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.3.1) - google-cloud-storage (1.47.0) + google-cloud-storage (1.37.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) + google-apis-storage_v1 (~> 0.1) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -260,6 +262,9 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.9.1) unicode-display_width (2.5.0) word_wrap (1.0.0) xcodeproj (1.23.0) @@ -273,6 +278,7 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) + zeitwerk (2.6.13) PLATFORMS arm64-darwin-21 @@ -292,4 +298,4 @@ RUBY VERSION ruby 2.6.10p210 BUNDLED WITH - 2.4.19 + 2.4.18 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 491ec28b59e5..88c18ffedda0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2007,7 +2007,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 0a6794d1974aed5d653d0d0cb900493e2583e35a - Yoga: 13c8ef87792450193e117976337b8527b49e8c03 + Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2 diff --git a/src/languages/en.ts b/src/languages/en.ts index 3575854ee7e2..505ba7a0f4f0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -653,7 +653,7 @@ export default { `changed the distance to ${newDistanceToDisplay} (previously ${oldDistanceToDisplay}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'request'}`, threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, - tagSelection: ({tagName}: TagSelectionParams) => `Select a ${tagName} to add additional organization to your money.`, + tagSelection: ({tagListName}: TagSelectionParams) => `Select a ${tagListName} to add additional organization to your money.`, categorySelection: 'Select a category to add additional organization to your money.', error: { invalidCategoryLength: 'The length of the category chosen exceeds the maximum allowed (255). Please choose a different or shorten the category name first.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 51a83e55fee2..2c876fb0b4dc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -648,7 +648,7 @@ export default { `cambió la distancia a ${newDistanceToDisplay} (previamente ${oldDistanceToDisplay}), lo que cambió el importe a ${newAmountToDisplay} (previamente ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${comment ? `${formattedAmount} para ${comment}` : `Solicitud de ${formattedAmount}`}`, threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, - tagSelection: ({tagName}: TagSelectionParams) => `Seleccione una ${tagName} para organizar mejor tu dinero.`, + tagSelection: ({tagListName}: TagSelectionParams) => `Seleccione una ${tagListName} para organizar mejor tu dinero.`, categorySelection: 'Seleccione una categoría para organizar mejor tu dinero.', error: { invalidCategoryLength: 'El largo de la categoría escogida excede el máximo permitido (255). Por favor, escoge otra categoría o acorta la categoría primero.', diff --git a/src/languages/types.ts b/src/languages/types.ts index 2bb05b614483..009e5b0796b3 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -210,7 +210,7 @@ type UpdatedTheDistanceParams = {newDistanceToDisplay: string; oldDistanceToDisp type FormattedMaxLengthParams = {formattedMaxLength: string}; -type TagSelectionParams = {tagName: string}; +type TagSelectionParams = {tagListName: string}; type WalletProgramParams = {walletProgram: string}; diff --git a/src/pages/EditRequestTagPage.js b/src/pages/EditRequestTagPage.js index 762bb76a7f57..3eda5184263a 100644 --- a/src/pages/EditRequestTagPage.js +++ b/src/pages/EditRequestTagPage.js @@ -16,7 +16,7 @@ const propTypes = { policyID: PropTypes.string.isRequired, /** The tag name to which the default tag belongs to */ - tagName: PropTypes.string, + tagListName: PropTypes.string, /** The index of a tag list */ tagIndex: PropTypes.number.isRequired, @@ -26,10 +26,10 @@ const propTypes = { }; const defaultProps = { - tagName: '', + tagListName: '', }; -function EditRequestTagPage({defaultTag, policyID, tagName, tagIndex, onSubmit}) { +function EditRequestTagPage({defaultTag, policyID, tagListName, tagIndex, onSubmit}) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -46,13 +46,13 @@ function EditRequestTagPage({defaultTag, policyID, tagName, tagIndex, onSubmit}) {({insets}) => ( <> - {translate('iou.tagSelection', {tagName: tagName || translate('common.tag')})} + {translate('iou.tagSelection', {tagListName: tagListName || translate('common.tag')})} {({insets}) => ( <> - {translate('iou.tagSelection', {tagName: policyTagListName})} + {translate('iou.tagSelection', {tagListName: policyTagListName})} Date: Fri, 8 Mar 2024 13:41:27 +0100 Subject: [PATCH 069/218] fix: reverted unnecessary changes --- Gemfile.lock | 16 +++++----------- ios/Podfile.lock | 2 +- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9a322c52606d..beb2c1762936 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,12 +3,11 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (6.1.7.7) + activesupport (7.0.8) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) @@ -81,8 +80,7 @@ GEM declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) escape (0.0.4) @@ -189,11 +187,11 @@ GEM google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.3.1) - google-cloud-storage (1.37.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -262,9 +260,6 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.9.1) unicode-display_width (2.5.0) word_wrap (1.0.0) xcodeproj (1.23.0) @@ -278,7 +273,6 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.6.13) PLATFORMS arm64-darwin-21 @@ -298,4 +292,4 @@ RUBY VERSION ruby 2.6.10p210 BUNDLED WITH - 2.4.18 + 2.4.19 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 88c18ffedda0..491ec28b59e5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2007,7 +2007,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 0a6794d1974aed5d653d0d0cb900493e2583e35a - Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 + Yoga: 13c8ef87792450193e117976337b8527b49e8c03 PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2 From e05df18496b72f40c2b39f4699d5a2c8b2f7d73f Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 8 Mar 2024 15:43:56 +0300 Subject: [PATCH 070/218] remove unnecessary condition --- src/components/AttachmentModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 459252e1ddd2..eed40d75387e 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -540,7 +540,7 @@ function AttachmentModal({ setDownloadButtonVisibility={setDownloadButtonVisibility} /> ) : ( - (!!sourceForAttachmentView || TransactionUtils.isFetchingWaypointsFromServer(transaction)) && + !!sourceForAttachmentView && shouldLoadAttachment && !isLoading && !shouldShowNotFoundPage && ( From b6fd6f47574589ddc067155a56964d81c0180b7e Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 8 Mar 2024 16:46:46 +0300 Subject: [PATCH 071/218] final refactor changes --- src/components/MoneyRequestConfirmationList.js | 1 - .../MoneyTemporaryForRefactorRequestConfirmationList.js | 3 +-- src/components/ReceiptImage.tsx | 2 +- .../MoneyRequestPreview/MoneyRequestPreviewContent.tsx | 2 +- src/components/ReportActionItem/ReportActionItemImage.tsx | 4 +--- src/components/ReportActionItem/ReportPreview.tsx | 2 +- 6 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 1f6868a31d00..60a9ad51db56 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -629,7 +629,6 @@ function MoneyRequestConfirmationList(props) { // So if we have a thumbnail, it means we're retrieving the image from the server isAuthTokenRequired={!_.isEmpty(receiptThumbnail)} fileExtension={fileExtension} - transactionID={props.transactionID || (props.transaction && props.transaction.transactionID)} /> ) : ( // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index c22889ad2077..99eb3b83db75 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -909,11 +909,10 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // but we don't need it to load the blob:// or file:// image when starting a money request / split bill // So if we have a thumbnail, it means we're retrieving the image from the server isAuthTokenRequired={!_.isEmpty(receiptThumbnail)} - transactionID={transaction && transaction.transactionID} fileExtension={fileExtension} /> ), - [isLocalFile, receiptFilename, receiptImage, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, receiptThumbnail, transaction, fileExtension], + [isLocalFile, receiptFilename, receiptImage, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, receiptThumbnail, fileExtension], ); return ( diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 8dc82cb53aea..08892f11b021 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -28,7 +28,7 @@ type ReceiptImageProps = ( isPDFThumbnail?: boolean; } | { - transactionID: string; + transactionID?: string; isEReceipt?: boolean; isThumbnail: boolean; source?: string; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 2ff0170809e3..a5fb03a22dca 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -114,7 +114,7 @@ function MoneyRequestPreviewContent({ merchantOrDescription = description || ''; } - const receiptImages = hasReceipt ? [{...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction}] : []; + const receiptImages = hasReceipt ? [{...ReceiptUtils.getThumbnailAndImageURIs(transaction)}] : []; const hasPendingWaypoints = transaction?.pendingFields?.waypoints; const showMapAsImage = isDistanceRequest && hasPendingWaypoints; diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 8c9cbb4a4e64..0ea3c0694861 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -90,9 +90,7 @@ function ReportActionItemImage({ } else { propsObj = { isThumbnail, - ...(isThumbnail && {iconSize: isSingleImage ? 'medium' : ('small' as IconSize)}), - fileExtension, - transactionID: transaction?.transactionID, + ...(isThumbnail && {iconSize: isSingleImage ? 'medium' : ('small' as IconSize), fileExtension}), source: thumbnail ?? image ?? '', }; } diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index d46e0a6436b2..7e29fd8ea203 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -139,7 +139,7 @@ function ReportPreview({ const isScanning = hasReceipts && areAllRequestsBeingSmartScanned; const hasErrors = hasMissingSmartscanFields || (canUseViolations && ReportUtils.hasViolations(iouReportID, transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); - const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction})); + const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction)})); let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; if (TransactionUtils.isPartialMerchant(formattedMerchant ?? '')) { formattedMerchant = null; From 4d181211394923d2f8e30c0995c6c3e9527ddf6d Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 8 Mar 2024 17:12:23 +0300 Subject: [PATCH 072/218] minor fix --- .../MoneyRequestPreview/MoneyRequestPreviewContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index a5fb03a22dca..8577c9fa5f97 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -114,7 +114,7 @@ function MoneyRequestPreviewContent({ merchantOrDescription = description || ''; } - const receiptImages = hasReceipt ? [{...ReceiptUtils.getThumbnailAndImageURIs(transaction)}] : []; + const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(transaction)] : []; const hasPendingWaypoints = transaction?.pendingFields?.waypoints; const showMapAsImage = isDistanceRequest && hasPendingWaypoints; From aa290ff57fe0047371d79a3aa94392412f59290b Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 8 Mar 2024 17:14:31 +0300 Subject: [PATCH 073/218] minor fix --- src/components/ReportActionItem/ReportPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 7e29fd8ea203..f6ffd54249fb 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -139,7 +139,7 @@ function ReportPreview({ const isScanning = hasReceipts && areAllRequestsBeingSmartScanned; const hasErrors = hasMissingSmartscanFields || (canUseViolations && ReportUtils.hasViolations(iouReportID, transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); - const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction)})); + const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; if (TransactionUtils.isPartialMerchant(formattedMerchant ?? '')) { formattedMerchant = null; From b6a111a9ff3d05b60ba1f1d2334e5895fbb8c375 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 8 Mar 2024 17:15:14 +0300 Subject: [PATCH 074/218] minor --- src/components/ReportActionItem/ReportPreview.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index f6ffd54249fb..381302489699 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -140,6 +140,7 @@ function ReportPreview({ const hasErrors = hasMissingSmartscanFields || (canUseViolations && ReportUtils.hasViolations(iouReportID, transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); + let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; if (TransactionUtils.isPartialMerchant(formattedMerchant ?? '')) { formattedMerchant = null; From e6cdbd96e714be5486721540fdf60e7ae7b40991 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Fri, 8 Mar 2024 14:35:54 +0000 Subject: [PATCH 075/218] [TS migration][G3] Feedback --- tests/unit/SidebarTest.ts | 8 ++++---- tests/utils/TestHelper.ts | 11 +++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/unit/SidebarTest.ts b/tests/unit/SidebarTest.ts index c913f5a2900d..846c714ce19d 100644 --- a/tests/unit/SidebarTest.ts +++ b/tests/unit/SidebarTest.ts @@ -59,11 +59,11 @@ describe('Sidebar', () => { waitForBatchedUpdates() // When Onyx is updated with the data and the sidebar re-renders .then(() => { - const reportCollection = { + const reportCollection: ReportActionCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, } as ReportCollectionDataSet; - const reportAction = { + const reportAction: ReportActionCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionID]: action}, } as ReportActionCollectionDataSet; @@ -111,11 +111,11 @@ describe('Sidebar', () => { waitForBatchedUpdates() // When Onyx is updated with the data and the sidebar re-renders .then(() => { - const reportCollection = { + const reportCollection: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, } as ReportCollectionDataSet; - const reportAction = { + const reportAction: ReportActionCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionID]: action}, } as ReportActionCollectionDataSet; diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index a9f72a40cf3f..097433e8813d 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,4 +1,5 @@ import Str from 'expensify-common/lib/str'; +import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import * as Session from '@src/libs/actions/Session'; @@ -18,7 +19,13 @@ type MockFetch = ReturnType & { resume?: () => Promise; }; -type Response = {ok: boolean; json: () => Promise<{jsonCode: number}>}; +type Response = { + ok?: boolean; + json?: () => Promise<{jsonCode: number}>; + jsonCode?: number; + onyxData?: OnyxUpdate[]; +}; + type QueueItem = (value: Response | PromiseLike) => void; type FormData = { @@ -51,7 +58,7 @@ function signInWithTestUser(accountID = 1, login = 'test@user.com', password = ' HttpUtils.xhr = jest.fn().mockImplementation(() => { // Your mocked response object - const mockedResponse = { + const mockedResponse: Response = { onyxData: [ { onyxMethod: Onyx.METHOD.MERGE, From f726b6b0e192f5b3e8ffdf3e76cf8e648122468a Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Mon, 11 Mar 2024 13:24:30 +0100 Subject: [PATCH 076/218] chore: run prettier --- src/pages/home/sidebar/SidebarLinksData.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 613402e04d35..eac46480152d 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -151,8 +151,21 @@ function SidebarLinksData({ const isLoading = isLoadingApp; const optionItemsMemoized = useMemo( - () => SidebarUtils.getOrderedReportIDs(null, chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs), - [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs], + () => + SidebarUtils.getOrderedReportIDs( + null, + chatReports, + betas, + policies, + priorityMode, + allReportActions, + transactionViolations, + activeWorkspaceID, + policyMemberAccountIDs, + reportIDsWithErrors, + canUseViolations, + ), + [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, reportIDsWithErrors, canUseViolations], ); const optionListItems = useMemo(() => { From 7c1cf629aa902962e3f738c9f84669135c03afac Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Mon, 11 Mar 2024 20:55:23 +0300 Subject: [PATCH 077/218] fix typescript --- src/pages/TransactionReceiptPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/TransactionReceiptPage.tsx b/src/pages/TransactionReceiptPage.tsx index 8db9e05a5139..f6f2c90e5d2c 100644 --- a/src/pages/TransactionReceiptPage.tsx +++ b/src/pages/TransactionReceiptPage.tsx @@ -28,7 +28,7 @@ type TransactionReceiptProps = TransactionReceiptOnyxProps & StackScreenProps Date: Tue, 12 Mar 2024 03:20:52 +0000 Subject: [PATCH 078/218] refactor(typescript): resolve type issues --- tests/e2e/compare/output/console.ts | 4 +- tests/e2e/compare/output/markdown.ts | 27 ++++++------- tests/e2e/compare/types.ts | 57 ---------------------------- tests/e2e/measure/math.ts | 11 +++++- 4 files changed, 26 insertions(+), 73 deletions(-) delete mode 100644 tests/e2e/compare/types.ts diff --git a/tests/e2e/compare/output/console.ts b/tests/e2e/compare/output/console.ts index de8e5d913893..3da0100b603f 100644 --- a/tests/e2e/compare/output/console.ts +++ b/tests/e2e/compare/output/console.ts @@ -13,6 +13,8 @@ type Entry = { type Data = { significance: Entry[]; meaningless: Entry[]; + errors: string[]; + warnings: string[]; }; const printRegularLine = (entry: Entry) => { @@ -36,4 +38,4 @@ export default (data: Data) => { console.debug(''); }; -export type {Entry}; +export type {Data, Entry}; diff --git a/tests/e2e/compare/output/markdown.ts b/tests/e2e/compare/output/markdown.ts index 07cf9897e570..716dbd6d09a4 100644 --- a/tests/e2e/compare/output/markdown.ts +++ b/tests/e2e/compare/output/markdown.ts @@ -1,16 +1,17 @@ // From: https://raw.githubusercontent.com/callstack/reassure/main/packages/reassure-compare/src/output/markdown.ts import fs from 'node:fs/promises'; import path from 'path'; +import type {Stats} from 'tests/e2e/measure/math'; import * as Logger from '../../utils/logger'; -import type {AddedEntry, CompareEntry, CompareResult, PerformanceEntry, RemovedEntry} from '../types'; +import type {Data, Entry} from './console'; import * as format from './format'; import markdownTable from './markdownTable'; const tableHeader = ['Name', 'Duration']; -const collapsibleSection = (title: string, content: string): string => `
\n${title}\n\n${content}\n
\n\n`; +const collapsibleSection = (title: string, content: string) => `
\n${title}\n\n${content}\n
\n\n`; -const buildDurationDetails = (title: string, entry: PerformanceEntry): string => { +const buildDurationDetails = (title: string, entry: Stats) => { const relativeStdev = entry.stdev / entry.mean; return [ @@ -23,25 +24,25 @@ const buildDurationDetails = (title: string, entry: PerformanceEntry): string => .join('
'); }; -const buildDurationDetailsEntry = (entry: CompareEntry | AddedEntry | RemovedEntry): string => +const buildDurationDetailsEntry = (entry: Entry) => ['baseline' in entry ? buildDurationDetails('Baseline', entry.baseline) : '', 'current' in entry ? buildDurationDetails('Current', entry.current) : ''] .filter(Boolean) .join('

'); -const formatEntryDuration = (entry: CompareEntry | AddedEntry | RemovedEntry): string => { - if ('baseline' in entry && 'current' in entry) { +const formatEntryDuration = (entry: Entry) => { + if (entry.baseline && entry.current) { return format.formatDurationDiffChange(entry); } - if ('baseline' in entry) { + if (entry.baseline) { return format.formatDuration(entry.baseline.mean); } - if ('current' in entry) { + if (entry.current) { return format.formatDuration(entry.current.mean); } return ''; }; -const buildDetailsTable = (entries: Array): string => { +const buildDetailsTable = (entries: Entry[]) => { if (!entries.length) { return ''; } @@ -52,7 +53,7 @@ const buildDetailsTable = (entries: Array, collapse = false): string => { +const buildSummaryTable = (entries: Entry[], collapse = false) => { if (!entries.length) { return '_There are no entries_'; } @@ -63,7 +64,7 @@ const buildSummaryTable = (entries: Array { +const buildMarkdown = (data: Data) => { let result = '## Performance Comparison Report 📊'; if (data.errors?.length) { @@ -91,7 +92,7 @@ const buildMarkdown = (data: CompareResult): string => { return result; }; -const writeToFile = (filePath: string, content: string): Promise => +const writeToFile = (filePath: string, content: string) => fs .writeFile(filePath, content) .then(() => { @@ -105,7 +106,7 @@ const writeToFile = (filePath: string, content: string): Promise => throw error; }); -const writeToMarkdown = (filePath: string, data: CompareResult): Promise => { +const writeToMarkdown = (filePath: string, data: Data) => { const markdown = buildMarkdown(data); return writeToFile(filePath, markdown).catch((error) => { console.error(error); diff --git a/tests/e2e/compare/types.ts b/tests/e2e/compare/types.ts deleted file mode 100644 index e0be36716977..000000000000 --- a/tests/e2e/compare/types.ts +++ /dev/null @@ -1,57 +0,0 @@ -type Entries = number[]; - -/** Metadata information for performance results. */ - -/** Entry in the performance results file. */ -type PerformanceEntry = { - /** Number of times the measurement test was run. */ - runs: number; - - /** Arithmetic average of measured render/execution durations for each run. */ - mean: number; - - /** Standard deviation of measured render/execution durations for each run. */ - stdev: number; - - /** Array of measured render/execution durations for each run. */ - entries: Entries; -}; - -/** - * Compare entry for tests that have both baseline and current entry - */ -type CompareEntry = { - name: string; - current: PerformanceEntry; - baseline: PerformanceEntry; - durationDiff: number; - relativeDurationDiff: number; -}; - -/** - * Compare entry for tests that have only current entry - */ -type AddedEntry = { - name: string; - current: PerformanceEntry; -}; - -/** - * Compare entry for tests that have only baseline entry - */ -type RemovedEntry = { - name: string; - baseline: PerformanceEntry; -}; - -/** Output of compare function. */ -type CompareResult = { - significance: CompareEntry[]; - meaningless: CompareEntry[]; - added: AddedEntry[]; - removed: RemovedEntry[]; - errors?: string[]; - warnings?: string[]; -}; - -export type {Entries, PerformanceEntry, CompareEntry, AddedEntry, RemovedEntry, CompareResult}; diff --git a/tests/e2e/measure/math.ts b/tests/e2e/measure/math.ts index e632e9bec232..d444ab0e79da 100644 --- a/tests/e2e/measure/math.ts +++ b/tests/e2e/measure/math.ts @@ -1,4 +1,11 @@ -import type {Entries, PerformanceEntry} from '../compare/types'; +type Entries = number[]; + +type Stats = { + mean: number; + stdev: number; + runs: number; + entries: Entries; +}; const filterOutliersViaIQR = (data: Entries): Entries => { let q1; @@ -28,7 +35,7 @@ const std = (arr: Entries): number => { return Math.sqrt(arr.map((i) => (i - avg) ** 2).reduce((a, b) => a + b) / arr.length); }; -const getStats = (entries: Entries): PerformanceEntry => { +const getStats = (entries: Entries): Stats => { const cleanedEntries = filterOutliersViaIQR(entries); const meanDuration = mean(cleanedEntries); const stdevDuration = std(cleanedEntries); From 000491f03e57981cc5a1ba6e7e1643f50458f428 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Tue, 12 Mar 2024 09:49:51 +0000 Subject: [PATCH 079/218] [TS migration][G3] Feedback --- tests/utils/TestHelper.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 097433e8813d..10313ad46d47 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,12 +1,11 @@ import Str from 'expensify-common/lib/str'; -import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import * as Session from '@src/libs/actions/Session'; import HttpUtils from '@src/libs/HttpUtils'; import * as NumberUtils from '@src/libs/NumberUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Report} from '@src/types/onyx'; +import type {Response as OnyxResponse, PersonalDetails, Report} from '@src/types/onyx'; import waitForBatchedUpdates from './waitForBatchedUpdates'; type MockFetch = ReturnType & { @@ -19,14 +18,7 @@ type MockFetch = ReturnType & { resume?: () => Promise; }; -type Response = { - ok?: boolean; - json?: () => Promise<{jsonCode: number}>; - jsonCode?: number; - onyxData?: OnyxUpdate[]; -}; - -type QueueItem = (value: Response | PromiseLike) => void; +type QueueItem = (value: Partial | PromiseLike>) => void; type FormData = { entries: () => Array<[string, string | Blob]>; @@ -58,7 +50,7 @@ function signInWithTestUser(accountID = 1, login = 'test@user.com', password = ' HttpUtils.xhr = jest.fn().mockImplementation(() => { // Your mocked response object - const mockedResponse: Response = { + const mockedResponse: OnyxResponse = { onyxData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -176,7 +168,7 @@ function getGlobalFetchMock() { let isPaused = false; let shouldFail = false; - const getResponse = () => + const getResponse = (): Partial => shouldFail ? { ok: true, @@ -256,4 +248,4 @@ const createAddListenerMock = () => { return {triggerTransitionEnd, addListener}; }; -export {getGlobalFetchMock, signInWithTestUser, signOutTestUser, setPersonalDetails, buildPersonalDetails, buildTestReportComment, assertFormDataMatchesObject, createAddListenerMock}; +export {assertFormDataMatchesObject, buildPersonalDetails, buildTestReportComment, createAddListenerMock, getGlobalFetchMock, setPersonalDetails, signInWithTestUser, signOutTestUser}; From 075f940a96e195fbb64ea73f825dbf68cc683b1c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 12 Mar 2024 11:09:11 +0100 Subject: [PATCH 080/218] fix: adjust descriptions --- src/components/TagPicker/index.tsx | 2 +- src/pages/EditRequestTagPage.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index 4998751309ee..f1700205f487 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -19,7 +19,7 @@ type SelectedTagOption = { }; type TagPickerOnyxProps = { - /** Collection of tags attached to a policy */ + /** Collection of tag list on a policy */ policyTags: OnyxEntry; /** List of recently used tags */ diff --git a/src/pages/EditRequestTagPage.js b/src/pages/EditRequestTagPage.js index 3eda5184263a..ca5bdc05530d 100644 --- a/src/pages/EditRequestTagPage.js +++ b/src/pages/EditRequestTagPage.js @@ -15,7 +15,7 @@ const propTypes = { /** The policyID we are getting tags for */ policyID: PropTypes.string.isRequired, - /** The tag name to which the default tag belongs to */ + /** The tag list name to which the default tag belongs to */ tagListName: PropTypes.string, /** The index of a tag list */ From fd7802b60ce2ca3348ae8d2a4bc77145777780d9 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Tue, 12 Mar 2024 10:21:44 +0000 Subject: [PATCH 081/218] [TS migration][G3] Feedback --- tests/unit/SidebarTest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/SidebarTest.ts b/tests/unit/SidebarTest.ts index fd068336d545..499af3031553 100644 --- a/tests/unit/SidebarTest.ts +++ b/tests/unit/SidebarTest.ts @@ -59,13 +59,13 @@ describe('Sidebar', () => { waitForBatchedUpdates() // When Onyx is updated with the data and the sidebar re-renders .then(() => { - const reportCollection: ReportActionCollectionDataSet = { + const reportCollection: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, - } as ReportCollectionDataSet; + } const reportAction: ReportActionCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionID]: action}, - } as ReportActionCollectionDataSet; + } as ReportActionCollectionDataSet return Onyx.multiSet({ [ONYXKEYS.BETAS]: betas, @@ -113,11 +113,11 @@ describe('Sidebar', () => { .then(() => { const reportCollection: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, - } as ReportCollectionDataSet; + } const reportAction: ReportActionCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionID]: action}, - } as ReportActionCollectionDataSet; + } as ReportActionCollectionDataSet return Onyx.multiSet({ [ONYXKEYS.BETAS]: betas, From 873493f83b6060b3ddca3877518d7e5f76cc1c55 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Tue, 12 Mar 2024 10:34:28 +0000 Subject: [PATCH 082/218] [TS migration][G3] Updated response type --- tests/utils/TestHelper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 323d48e08044..bf83845a81be 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -82,7 +82,7 @@ function signInWithTestUser(accountID = 1, login = 'test@user.com', password = ' return waitForBatchedUpdates() .then(() => { HttpUtils.xhr = jest.fn().mockImplementation(() => { - const mockedResponse = { + const mockedResponse: OnyxResponse = { onyxData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -137,7 +137,7 @@ function signInWithTestUser(accountID = 1, login = 'test@user.com', password = ' function signOutTestUser() { const originalXhr = HttpUtils.xhr; HttpUtils.xhr = jest.fn().mockImplementation(() => { - const mockedResponse = { + const mockedResponse: OnyxResponse = { jsonCode: 200, }; From 576ac31863d134f32530a81931dfb4516fea62a9 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 12 Mar 2024 15:43:00 +0100 Subject: [PATCH 083/218] fix: adjusted tagIndex prop comment --- src/components/TagPicker/index.tsx | 2 +- src/pages/EditRequestPage.js | 2 +- src/pages/EditRequestTagPage.js | 2 +- src/pages/iou/request/step/IOURequestStepRoutePropTypes.js | 3 +++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index f1700205f487..39574299063e 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -50,7 +50,7 @@ type TagPickerProps = TagPickerOnyxProps & { /** Should show the selected option that is disabled? */ shouldShowDisabledAndSelectedOption?: boolean; - /** The index of a tag list */ + /** Indicates which tag list index was selected */ tagIndex: number; }; diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index de17d16a7c38..ebe4c886d6c5 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -38,7 +38,7 @@ const propTypes = { /** reportID for the "transaction thread" */ threadReportID: PropTypes.string, - /** The index of a tag list */ + /** Indicates which tag list index was selected */ tagIndex: PropTypes.string, }), }).isRequired, diff --git a/src/pages/EditRequestTagPage.js b/src/pages/EditRequestTagPage.js index ca5bdc05530d..fe1526af1973 100644 --- a/src/pages/EditRequestTagPage.js +++ b/src/pages/EditRequestTagPage.js @@ -18,7 +18,7 @@ const propTypes = { /** The tag list name to which the default tag belongs to */ tagListName: PropTypes.string, - /** The index of a tag list */ + /** Indicates which tag list index was selected */ tagIndex: PropTypes.number.isRequired, /** Callback to fire when the Save button is pressed */ diff --git a/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js b/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js index dbcf83bda62a..8b191fa0b58e 100644 --- a/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js +++ b/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js @@ -22,5 +22,8 @@ export default PropTypes.shape({ /** A path to go to when the user presses the back button */ backTo: PropTypes.string, + + /** Indicates which tag list index was selected */ + tagIndex: PropTypes.string, }), }); From 3973b76ab96a16ee49835129fc8d9987a71d02d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Tue, 12 Mar 2024 17:34:51 +0100 Subject: [PATCH 084/218] Enable live markdown input on web --- package-lock.json | 155 +----------------------------- package.json | 2 +- src/components/Composer/index.tsx | 9 +- 3 files changed, 11 insertions(+), 155 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc373abcd9b0..cdd6b05b301e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.5", + "@expensify/react-native-live-markdown": "0.1.23", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", @@ -3406,9 +3406,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.5.tgz", - "integrity": "sha512-Z1tduU1C2BDgZNMrDvXtiWUhQoroMasvwucLBdLSypAMB4Kls4G038A/yZEbD00YVXjXv2tBUiqvUmCMuRdlqw==", + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.23.tgz", + "integrity": "sha512-FYD5Hq9v58HQKxpT0grovH3soTrADpkjreNlE8sCx4D6Ga3I9wLYTCKOUzBCKLtfEpidrOXDkZm2cOzzYz9Z4g==", "engines": { "node": ">= 18.0.0" }, @@ -8045,153 +8045,6 @@ "integrity": "sha512-C9Br1BQqm6io6lvYHptlLcOHbzlaqxp9tS35P8Qj3pdiiYRTzU3KPvZ61rQ+ZnZ4FOQ6MwPsKsmB8+6WHkAY6Q==", "license": "MIT" }, - "node_modules/@onfido/active-video-capture": { - "version": "0.28.6", - "resolved": "https://registry.npmjs.org/@onfido/active-video-capture/-/active-video-capture-0.28.6.tgz", - "integrity": "sha512-RFUeKaOSjj/amPp6VzhVkq/7kIkutEnnttT9n5KDeD3Vx8a09KD3a/xvxdQppveHlDAYsdBP6LrJwSSpjXiprg==", - "dependencies": { - "@mediapipe/face_detection": "^0.4.1646425229", - "@mediapipe/face_mesh": "^0.4.1633559619", - "@onfido/castor": "^2.2.2", - "@onfido/castor-icons": "^2.12.0", - "@tensorflow-models/face-detection": "^1.0.1", - "@tensorflow-models/face-landmarks-detection": "^1.0.2", - "@tensorflow/tfjs-backend-wasm": "3.20.0", - "@tensorflow/tfjs-backend-webgl": "3.20.0", - "@tensorflow/tfjs-converter": "3.20.0", - "@tensorflow/tfjs-core": "3.20.0", - "preact": "10.11.3", - "react-webcam": "^7.2.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow-models/face-landmarks-detection": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@tensorflow-models/face-landmarks-detection/-/face-landmarks-detection-1.0.5.tgz", - "integrity": "sha512-54XJPi8g29/MknJ33ZBrLsEzr9kw/dJtrJMMD3xrCrnRlfFQPIKQ5PI2Wml55Fz2p4U2hemzBB0/H+S94JddIQ==", - "dependencies": { - "rimraf": "^3.0.2" - }, - "peerDependencies": { - "@mediapipe/face_mesh": "~0.4.0", - "@tensorflow-models/face-detection": "~1.0.0", - "@tensorflow/tfjs-backend-webgl": "^3.12.0", - "@tensorflow/tfjs-converter": "^3.12.0", - "@tensorflow/tfjs-core": "^3.12.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-cpu": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.20.0.tgz", - "integrity": "sha512-gf075YaBLwSAAiUwa0D4GvYyUBhbJ1BVSivUNQmUfGKvIr2lIhF0qstBr033YTc3lhkbFSHEEPAHh/EfpqyjXQ==", - "dependencies": { - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-wasm": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-wasm/-/tfjs-backend-wasm-3.20.0.tgz", - "integrity": "sha512-k+sDcrcPtGToLjKRffgtSqlcN4MC6g4hXWRarZfgvvyvFqpxVfVqrGYHGTirXdN47sKYhmcTSMvbM2quGaaQnA==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "3.20.0", - "@types/emscripten": "~0.0.34" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-webgl": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.20.0.tgz", - "integrity": "sha512-SucbyQ08re3HvRgVfarRtKFIjNM4JvIAzcXmw4vaE/HrCtPEePkGO1VrmfQoN470EdUmGiwgqAjoyBvM2VOlVg==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "3.20.0", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "@types/webgl-ext": "0.0.30", - "@types/webgl2": "0.0.6", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-converter": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.20.0.tgz", - "integrity": "sha512-8EIYqtQwvSYw9GFNW2OFU8Qnl/FQF/kKAsQJoORYaZ419WJo+FIZWbAWDtCpJSAgkgoHH1jYWgV9H313cVmqxg==", - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-core": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-3.20.0.tgz", - "integrity": "sha512-L16JyVA4a8jFJXFgB9/oYZxcGq/GfLypt5dMVTyedznARZZ9SiY/UMMbo3IKl9ZylG1dOVVTpjzV3EvBYfeJXw==", - "dependencies": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "@types/webgl-ext": "0.0.30", - "@webgpu/types": "0.1.16", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@webgpu/types": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.16.tgz", - "integrity": "sha512-9E61voMP4+Rze02jlTXud++Htpjyyk8vw5Hyw9FGRrmhHQg2GqbuOfwf5Klrb8vTxc2XWI3EfO7RUHMpxTj26A==" - }, - "node_modules/@onfido/castor": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@onfido/castor/-/castor-2.3.0.tgz", - "integrity": "sha512-FkydkjedS6b2g3SqgZMYnVRZvUs/MkaEuXXJWG9+LNc7DMFT1K8smOnNuHzkiM3cJhXL6yAADdKE0mg+ZIrucQ==", - "dependencies": { - "@onfido/castor-tokens": "^1.0.0-beta.6", - "csstype": "^3.1.1" - }, - "peerDependencies": { - "@onfido/castor-icons": ">=1.0.0" - } - }, - "node_modules/@onfido/castor-icons": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@onfido/castor-icons/-/castor-icons-2.22.0.tgz", - "integrity": "sha512-7OnCvu5xqVWcBLqovZyb99NP0oHw7sjkVYXZhi438i0U6Pgecrhu/14Gc/IN/kvgDxWj9qmiYdd0qdjNaVckrQ==", - "peerDependencies": { - "react": ">=17 || ^16.14 || ^15.7 || ^0.14.10" - } - }, - "node_modules/@onfido/castor-tokens": { - "version": "1.0.0-beta.6", - "resolved": "https://registry.npmjs.org/@onfido/castor-tokens/-/castor-tokens-1.0.0-beta.6.tgz", - "integrity": "sha512-MfwuSlNdM0Ay0cI3LLyqZGsHW0e1Y1R/0IdQKVU575PdWQx1Q/538aOZMo/a3/oSW0pMEgfOm+mNqPx057cvWA==" - }, - "node_modules/@onfido/opencv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@onfido/opencv/-/opencv-2.1.1.tgz", - "integrity": "sha512-Bwo0YsZrrdm+p5hpNFZ7yrqNVWJxOUbQW9aWDEUtkDWUL+nX2RHIR6F4lBGVmbqnG24anadS/+nEvy80SwD3tQ==", - "dependencies": { - "mirada": "^0.0.15" - } - }, "node_modules/@onfido/react-native-sdk": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-10.6.0.tgz", diff --git a/package.json b/package.json index 1c4f23700bc4..6ec907260bab 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.5", + "@expensify/react-native-live-markdown": "0.1.23", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index b6443f3ca385..008d4e7f2bbf 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -1,3 +1,4 @@ +import {MarkdownTextInput} from '@expensify/react-native-live-markdown'; import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; @@ -5,10 +6,10 @@ import {flushSync} from 'react-dom'; import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; -import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; import useHtmlPaste from '@hooks/useHtmlPaste'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; +import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -79,6 +80,7 @@ function Composer( ) { const theme = useTheme(); const styles = useThemeStyles(); + const markdownStyle = useMarkdownStyle(); const StyleUtils = useStyleUtils(); const {windowWidth} = useWindowDimensions(); const textRef = useRef(null); @@ -328,13 +330,14 @@ function Composer( return ( <> - (textInput.current = el)} + ref={(el) => (textInput.current = el as AnimatedTextInputRef)} selection={selection} style={inputStyleMemo} + markdownStyle={markdownStyle} value={value} defaultValue={defaultValue} autoFocus={autoFocus} From 60a8cec972225b0fbce8b9dd544105c86e74206f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Tue, 12 Mar 2024 20:29:32 +0100 Subject: [PATCH 085/218] Bump expensify-common version --- package-lock.json | 6 +++--- package.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index cdd6b05b301e..61aa1deb5be1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#77d0b150ba6bfbe7a64b3c3e30b65592b2e58c4a", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -30570,8 +30570,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", - "integrity": "sha512-3d/JHWgeS+LFPRahCAXdLwnBYQk4XUYybtgCm7VsdmMDtCeGUTksLsEY7F1Zqm+ULqZjmCtYwAi8IPKy0fsSOw==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#77d0b150ba6bfbe7a64b3c3e30b65592b2e58c4a", + "integrity": "sha512-x8W172bdClKzuTj2J1Bf2ZZy8Bdcbj9M6AZ/jedSxd48oRl4p/pFVoAfOfIi7UuRFhrg01SE03qHnseYTPqcKQ==", "license": "MIT", "dependencies": { "classnames": "2.5.0", diff --git a/package.json b/package.json index 6ec907260bab..12ea9526de5d 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.23", + "@expensify/react-native-live-markdown": "0.1.24", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", @@ -103,7 +103,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#77d0b150ba6bfbe7a64b3c3e30b65592b2e58c4a", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", From c868943b368e191820bd43a64a7e3a1dca70e799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 13 Mar 2024 11:22:13 +0100 Subject: [PATCH 086/218] Bump react-native-live-markdown verion to 0.1.26 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61aa1deb5be1..c11d7bcde0d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.23", + "@expensify/react-native-live-markdown": "0.1.26", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", @@ -3406,9 +3406,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.23", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.23.tgz", - "integrity": "sha512-FYD5Hq9v58HQKxpT0grovH3soTrADpkjreNlE8sCx4D6Ga3I9wLYTCKOUzBCKLtfEpidrOXDkZm2cOzzYz9Z4g==", + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.26.tgz", + "integrity": "sha512-R1WxI+KqBffwLCg1Gk2bjdAxzp+dsa7VRTmXPYVAaFbJndQ/y3g0pubgxrzYjd/j++yfbDuDovR4xrp/8I6GfA==", "engines": { "node": ">= 18.0.0" }, diff --git a/package.json b/package.json index 3ac7c0f4a7a4..7c5a176d3f4d 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.23", + "@expensify/react-native-live-markdown": "0.1.26", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", From f5e43db353de3945a09744dbb0a5a5f6618b7c4b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Mar 2024 12:53:16 +0100 Subject: [PATCH 087/218] fix: rerun pipeline --- src/components/TagPicker/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index 39574299063e..8d9044e603ad 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -59,6 +59,7 @@ function TagPicker({selectedTag, tagListName, policyTags, tagIndex, policyRecent const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); + console.log('hello'); const policyRecentlyUsedTagsList = useMemo(() => policyRecentlyUsedTags?.[tagListName] ?? [], [policyRecentlyUsedTags, tagListName]); const policyTagList = PolicyUtils.getTagList(policyTags, tagIndex); From c6e10e43fbd805efe6711d5245138b85d3643bb3 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Mar 2024 12:53:37 +0100 Subject: [PATCH 088/218] fix: remove log --- src/components/TagPicker/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index 8d9044e603ad..39574299063e 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -59,7 +59,6 @@ function TagPicker({selectedTag, tagListName, policyTags, tagIndex, policyRecent const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); - console.log('hello'); const policyRecentlyUsedTagsList = useMemo(() => policyRecentlyUsedTags?.[tagListName] ?? [], [policyRecentlyUsedTags, tagListName]); const policyTagList = PolicyUtils.getTagList(policyTags, tagIndex); From cf5f86bc702c806bc08e49511ca2508a923c5865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 13 Mar 2024 14:46:25 +0100 Subject: [PATCH 089/218] Change Composer markdown component to RNMarkdownTextInput --- src/components/Composer/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 008d4e7f2bbf..41834625bdbd 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -1,4 +1,3 @@ -import {MarkdownTextInput} from '@expensify/react-native-live-markdown'; import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; @@ -21,6 +20,7 @@ import * as FileUtils from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import CONST from '@src/CONST'; +import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import type {ComposerProps} from './types'; /** @@ -330,11 +330,11 @@ function Composer( return ( <> - (textInput.current = el as AnimatedTextInputRef)} + ref={(el) => (textInput.current = el as AnimatedTextInputRef | null)} selection={selection} style={inputStyleMemo} markdownStyle={markdownStyle} From 124ac6c990f452bb4d1a14c160816cd77fa39d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 13 Mar 2024 15:11:19 +0100 Subject: [PATCH 090/218] Change ref type --- src/components/Composer/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 41834625bdbd..8537fa2618d7 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -4,7 +4,6 @@ import {flushSync} from 'react-dom'; // eslint-disable-next-line no-restricted-imports import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; -import type {AnimatedTextInputRef} from '@components/RNTextInput'; import Text from '@components/Text'; import useHtmlPaste from '@hooks/useHtmlPaste'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; @@ -20,6 +19,7 @@ import * as FileUtils from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import CONST from '@src/CONST'; +import type { AnimatedMarkdownTextInputRef } from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import type {ComposerProps} from './types'; @@ -84,7 +84,7 @@ function Composer( const StyleUtils = useStyleUtils(); const {windowWidth} = useWindowDimensions(); const textRef = useRef(null); - const textInput = useRef(null); + const textInput = useRef(null); const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp); const [selection, setSelection] = useState< | { @@ -334,7 +334,7 @@ function Composer( autoComplete="off" autoCorrect={!Browser.isMobileSafari()} placeholderTextColor={theme.placeholderText} - ref={(el) => (textInput.current = el as AnimatedTextInputRef | null)} + ref={(el) => (textInput.current = el)} selection={selection} style={inputStyleMemo} markdownStyle={markdownStyle} From 52f68dc1577b743bb2e5ba06e9bcffe1daa13c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 13 Mar 2024 15:33:46 +0100 Subject: [PATCH 091/218] Fix prettier --- src/components/Composer/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 8537fa2618d7..a95ad0f4a838 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -4,6 +4,8 @@ import {flushSync} from 'react-dom'; // eslint-disable-next-line no-restricted-imports import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; +import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; +import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import Text from '@components/Text'; import useHtmlPaste from '@hooks/useHtmlPaste'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; @@ -19,8 +21,6 @@ import * as FileUtils from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import CONST from '@src/CONST'; -import type { AnimatedMarkdownTextInputRef } from '@components/RNMarkdownTextInput'; -import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import type {ComposerProps} from './types'; /** From 8333bb66880a7a19ceabd5aa70d74bcd9cbb0cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 13 Mar 2024 15:37:10 +0100 Subject: [PATCH 092/218] Fix Podfile --- ios/Podfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d0007ec51668..513986071ad5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1359,7 +1359,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.5): + - RNLiveMarkdown (0.1.26): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1896,7 +1896,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 25b969a1ffc806b9f9ad2e170d4a3b049c6af85e RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 35eeecf7e57eb26fdc279d5d4815982a9a9f7beb + RNLiveMarkdown: 4abc843dc43d32c5dc29da8d0793d3544a62ecbb RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: fcf7f1cbdc8bd7569c267d07284e8a5c7bee06ed RNPermissions: 9b086c8f05b2e2faa587fdc31f4c5ab4509728aa From e5d3ef0982f1225294adf4d02db9951f3608c48f Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 13 Mar 2024 15:51:21 +0000 Subject: [PATCH 093/218] fix: wrong data type conversion --- tests/perf-test/SidebarLinks.perf-test.tsx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/perf-test/SidebarLinks.perf-test.tsx b/tests/perf-test/SidebarLinks.perf-test.tsx index 473e854322d5..2848015d5c63 100644 --- a/tests/perf-test/SidebarLinks.perf-test.tsx +++ b/tests/perf-test/SidebarLinks.perf-test.tsx @@ -16,16 +16,18 @@ jest.mock('@components/Icon/Expensicons'); jest.mock('@react-navigation/native'); const getMockedReportsMap = (length = 100) => { - const mockReports = Array.from({length}, (value, index) => { - const reportID = index + 1; - const participants = [1, 2]; - const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; - const report = LHNTestUtils.getFakeReport(participants, 1, true); - - return {[reportKey]: report}; - }); - - return {...mockReports}; + const mockReports = Object.fromEntries( + Array.from({length}, (value, index) => { + const reportID = index + 1; + const participants = [1, 2]; + const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; + const report = LHNTestUtils.getFakeReport(participants, 1, true); + + return [reportKey, report]; + }), + ); + + return mockReports; }; const mockedResponseMap = getMockedReportsMap(500); From ab6d0f6e8dddeae74fe7cad96bc455839ed5feca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Thu, 14 Mar 2024 10:35:56 +0100 Subject: [PATCH 094/218] Update react-native-live-markdown and expensify-common versions --- package-lock.json | 14 +++++++------- package.json | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37ade5a32f6f..b720c6be517c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.26", + "@expensify/react-native-live-markdown": "0.1.28", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", @@ -52,7 +52,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#dc8ea983f9e06308cdb9151a9cdfa82bd11502e1", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -3406,9 +3406,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.26", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.26.tgz", - "integrity": "sha512-R1WxI+KqBffwLCg1Gk2bjdAxzp+dsa7VRTmXPYVAaFbJndQ/y3g0pubgxrzYjd/j++yfbDuDovR4xrp/8I6GfA==", + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.28.tgz", + "integrity": "sha512-rYG1wvSyJ0ZBTHvXsHA2osB1ZdC1IuBE0IRRBfCCAOnj6Es8ZX1p7wtdFO19BsKGQxLoGO6w5W1iyAkJruHKug==", "engines": { "node": ">= 18.0.0" }, @@ -30570,8 +30570,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9", - "integrity": "sha512-v6UnN9yAW6p2996Fvd4AZnMRnisVfjg6ijWzUQue/6JsjSY+MW10oP74hSjD6x32fRrNmMctjy6d5a79bQFdPA==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#dc8ea983f9e06308cdb9151a9cdfa82bd11502e1", + "integrity": "sha512-Vtbe8BsZ/j8Zop+Q9XeOKw/HuOrq5IdUq+NNkGQrrzTH4JWvwTthJcFzBVGQ7o3chR9G/DUT69JTKeqEUejvqQ==", "license": "MIT", "dependencies": { "classnames": "2.5.0", diff --git a/package.json b/package.json index bc196f4c33c4..ae0df484ade9 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.26", + "@expensify/react-native-live-markdown": "0.1.28", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", @@ -103,7 +103,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#dc8ea983f9e06308cdb9151a9cdfa82bd11502e1", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", From 6db2f6b9badca84221d1c1dcb291f4fe8fd2a12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Thu, 14 Mar 2024 10:37:41 +0100 Subject: [PATCH 095/218] Update Podfile --- ios/Podfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 513986071ad5..ce935c872c3b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1359,7 +1359,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.26): + - RNLiveMarkdown (0.1.28): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1896,7 +1896,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 25b969a1ffc806b9f9ad2e170d4a3b049c6af85e RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 4abc843dc43d32c5dc29da8d0793d3544a62ecbb + RNLiveMarkdown: 1608aa096bc493fc2f91a91015e96b1592a3a874 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: fcf7f1cbdc8bd7569c267d07284e8a5c7bee06ed RNPermissions: 9b086c8f05b2e2faa587fdc31f4c5ab4509728aa From 95090762b1abf64eb920af7044ca44a8510f8caa Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Thu, 14 Mar 2024 16:45:57 +0100 Subject: [PATCH 096/218] fix: flushSync error --- src/components/Composer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index a95ad0f4a838..d8bde63aea8e 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -125,7 +125,7 @@ function Composer( const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent) => { const webEvent = event as BaseSyntheticEvent; - if (shouldCalculateCaretPosition) { + if (shouldCalculateCaretPosition && textInput.current) { // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state flushSync(() => { setValueBeforeCaret(webEvent.target.value.slice(0, webEvent.nativeEvent.selection.start)); From c5b2ada306f9397b1b7eff5a9cf5815348fca572 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 14 Mar 2024 19:00:17 +0000 Subject: [PATCH 097/218] refactor(typescript): apply pull request suggestions --- src/libs/OptionsListUtils.ts | 2 +- src/libs/ReportUtils.ts | 4 ++ tests/unit/OptionsListUtilsTest.ts | 108 ++++++++++++++++++----------- tests/unit/ReportUtilsTest.ts | 6 +- 4 files changed, 75 insertions(+), 45 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ecf9b68961ca..6c30f9c3568e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2076,4 +2076,4 @@ export { getShareLogOptions, }; -export type {MemberForList, CategorySection, GetOptions, PayeePersonalDetails, Tag}; +export type {MemberForList, CategorySection, CategoryTreeSection, GetOptions, PayeePersonalDetails, Tag}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 76e0267b6f35..53cb6d849ec7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -29,6 +29,7 @@ import type { ReportMetadata, Session, Task, + TaxRate, Transaction, TransactionViolation, UserWallet, @@ -404,6 +405,9 @@ type OptionData = { isDisabled?: boolean | null; name?: string | null; isSelfDM?: boolean | null; + reportID?: string; + enabled?: boolean; + data?: Partial; } & Report; type OnyxDataTaskAssigneeChat = { diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 02402e7d7bd2..bfb9514d7b92 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -1,16 +1,19 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import type {Tag} from '@src/libs/OptionsListUtils'; import * as OptionsListUtils from '@src/libs/OptionsListUtils'; import * as ReportUtils from '@src/libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyCategories, Report, TaxRatesWithDefault} from '@src/types/onyx'; +import type {PersonalDetails, Policy, PolicyCategories, Report, TaxRatesWithDefault} from '@src/types/onyx'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +type PersonalDetailsList = Record; + describe('OptionsListUtils', () => { // Given a set of reports with both single participants and multiple participants some pinned and some not - const REPORTS: Record = { + const REPORTS: OnyxCollection = { '1': { lastReadTime: '2021-01-14 11:25:39.295', lastVisibleActionCreated: '2022-11-22 03:26:02.015', @@ -133,7 +136,7 @@ describe('OptionsListUtils', () => { }; // And a set of personalDetails some with existing reports and some without - const PERSONAL_DETAILS = { + const PERSONAL_DETAILS: PersonalDetailsList = { // These exist in our reports '1': { accountID: 1, @@ -200,7 +203,7 @@ describe('OptionsListUtils', () => { }, }; - const REPORTS_WITH_CONCIERGE = { + const REPORTS_WITH_CONCIERGE: OnyxCollection = { ...REPORTS, '11': { @@ -215,7 +218,7 @@ describe('OptionsListUtils', () => { }, }; - const REPORTS_WITH_CHRONOS = { + const REPORTS_WITH_CHRONOS: OnyxCollection = { ...REPORTS, '12': { lastReadTime: '2021-01-14 11:25:39.302', @@ -229,7 +232,7 @@ describe('OptionsListUtils', () => { }, }; - const REPORTS_WITH_RECEIPTS = { + const REPORTS_WITH_RECEIPTS: OnyxCollection = { ...REPORTS, '13': { lastReadTime: '2021-01-14 11:25:39.302', @@ -243,7 +246,7 @@ describe('OptionsListUtils', () => { }, }; - const REPORTS_WITH_WORKSPACE_ROOMS = { + const REPORTS_WITH_WORKSPACE_ROOMS: OnyxCollection = { ...REPORTS, '14': { lastReadTime: '2021-01-14 11:25:39.302', @@ -254,62 +257,67 @@ describe('OptionsListUtils', () => { visibleChatMemberAccountIDs: [1, 10, 3], reportName: '', oldPolicyName: 'Avengers Room', - isArchivedRoom: false, chatType: CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, isOwnPolicyExpenseChat: true, type: CONST.REPORT.TYPE.CHAT, }, }; - const PERSONAL_DETAILS_WITH_CONCIERGE = { + const PERSONAL_DETAILS_WITH_CONCIERGE: PersonalDetailsList = { ...PERSONAL_DETAILS, '999': { accountID: 999, displayName: 'Concierge', login: 'concierge@expensify.com', + reportID: '', }, }; - const PERSONAL_DETAILS_WITH_CHRONOS = { + const PERSONAL_DETAILS_WITH_CHRONOS: PersonalDetailsList = { ...PERSONAL_DETAILS, '1000': { accountID: 1000, displayName: 'Chronos', login: 'chronos@expensify.com', + reportID: '', }, }; - const PERSONAL_DETAILS_WITH_RECEIPTS = { + const PERSONAL_DETAILS_WITH_RECEIPTS: PersonalDetailsList = { ...PERSONAL_DETAILS, '1001': { accountID: 1001, displayName: 'Receipts', login: 'receipts@expensify.com', + reportID: '', }, }; - const PERSONAL_DETAILS_WITH_PERIODS = { + const PERSONAL_DETAILS_WITH_PERIODS: PersonalDetailsList = { ...PERSONAL_DETAILS, '1002': { accountID: 1002, displayName: 'The Flash', login: 'barry.allen@expensify.com', + reportID: '', }, }; - const POLICY = { - id: 'ABC123', + const policyID = 'ABC123'; + + const POLICY: Policy = { + id: policyID, name: 'Hero Policy', role: 'user', type: 'free', owner: '', outputCurrency: '', isPolicyExpenseChatEnabled: false, - } as const; + }; // Set the currently logged in user, report data, and personal details beforeAll(() => { @@ -322,7 +330,7 @@ describe('OptionsListUtils', () => { ownerAccountID: 8, total: 1000, }, - [`${ONYXKEYS.COLLECTION.POLICY}${POLICY.id}` as const]: POLICY, + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const]: POLICY, }, }); Onyx.registerLogger(() => {}); @@ -619,7 +627,7 @@ describe('OptionsListUtils', () => { it('getShareDestinationsOptions()', () => { // Filter current REPORTS as we do in the component, before getting share destination options - const filteredReports = Object.entries(REPORTS).reduce>((reports, [reportKey, report]) => { + const filteredReports = Object.entries(REPORTS).reduce>((reports, [reportKey, report]) => { if (!ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { return reports; } @@ -647,7 +655,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(1); // Filter current REPORTS_WITH_WORKSPACE_ROOMS as we do in the component, before getting share destination options - const filteredReportsWithWorkspaceRooms = Object.entries(REPORTS_WITH_WORKSPACE_ROOMS).reduce>((reports, [reportKey, report]) => { + const filteredReportsWithWorkspaceRooms = Object.entries(REPORTS_WITH_WORKSPACE_ROOMS).reduce>((reports, [reportKey, report]) => { if (!ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { return reports; } @@ -703,7 +711,7 @@ describe('OptionsListUtils', () => { const emptySearch = ''; const wrongSearch = 'bla bla'; const recentlyUsedCategories = ['Taxi', 'Restaurant']; - const selectedOptions = [ + const selectedOptions: Array> = [ { name: 'Medical', enabled: true, @@ -713,6 +721,7 @@ describe('OptionsListUtils', () => { Taxi: { enabled: false, name: 'Taxi', + unencodedName: 'Taxi', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -721,6 +730,7 @@ describe('OptionsListUtils', () => { Restaurant: { enabled: true, name: 'Restaurant', + unencodedName: 'Restaurant', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -729,6 +739,7 @@ describe('OptionsListUtils', () => { Food: { enabled: true, name: 'Food', + unencodedName: 'Food', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -737,13 +748,14 @@ describe('OptionsListUtils', () => { 'Food: Meat': { enabled: true, name: 'Food: Meat', + unencodedName: 'Food: Meat', areCommentsRequired: false, 'GL Code': '', externalID: '', origin: '', }, }; - const smallResultList = [ + const smallResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: false, @@ -776,7 +788,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const smallSearchResultList = [ + const smallSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -801,7 +813,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const smallWrongSearchResultList = [ + const smallWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -813,6 +825,7 @@ describe('OptionsListUtils', () => { Taxi: { enabled: false, name: 'Taxi', + unencodedName: 'Taxi', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -821,6 +834,7 @@ describe('OptionsListUtils', () => { Restaurant: { enabled: true, name: 'Restaurant', + unencodedName: 'Restaurant', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -829,6 +843,7 @@ describe('OptionsListUtils', () => { Food: { enabled: true, name: 'Food', + unencodedName: 'Food', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -837,6 +852,7 @@ describe('OptionsListUtils', () => { 'Food: Meat': { enabled: true, name: 'Food: Meat', + unencodedName: 'Food: Meat', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -845,6 +861,7 @@ describe('OptionsListUtils', () => { 'Food: Milk': { enabled: true, name: 'Food: Milk', + unencodedName: 'Food: Milk', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -853,6 +870,7 @@ describe('OptionsListUtils', () => { 'Food: Vegetables': { enabled: false, name: 'Food: Vegetables', + unencodedName: 'Food: Vegetables', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -861,6 +879,7 @@ describe('OptionsListUtils', () => { 'Cars: Audi': { enabled: true, name: 'Cars: Audi', + unencodedName: 'Cars: Audi', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -869,6 +888,7 @@ describe('OptionsListUtils', () => { 'Cars: BMW': { enabled: false, name: 'Cars: BMW', + unencodedName: 'Cars: BMW', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -877,6 +897,7 @@ describe('OptionsListUtils', () => { 'Cars: Mercedes-Benz': { enabled: true, name: 'Cars: Mercedes-Benz', + unencodedName: 'Cars: Mercedes-Benz', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -885,6 +906,7 @@ describe('OptionsListUtils', () => { Medical: { enabled: false, name: 'Medical', + unencodedName: 'Medical', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -893,6 +915,7 @@ describe('OptionsListUtils', () => { 'Travel: Meals': { enabled: true, name: 'Travel: Meals', + unencodedName: 'Travel: Meals', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -901,6 +924,7 @@ describe('OptionsListUtils', () => { 'Travel: Meals: Breakfast': { enabled: true, name: 'Travel: Meals: Breakfast', + unencodedName: 'Travel: Meals: Breakfast', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -909,6 +933,7 @@ describe('OptionsListUtils', () => { 'Travel: Meals: Dinner': { enabled: false, name: 'Travel: Meals: Dinner', + unencodedName: 'Travel: Meals: Dinner', areCommentsRequired: false, 'GL Code': '', externalID: '', @@ -917,13 +942,14 @@ describe('OptionsListUtils', () => { 'Travel: Meals: Lunch': { enabled: true, name: 'Travel: Meals: Lunch', + unencodedName: 'Travel: Meals: Lunch', areCommentsRequired: false, 'GL Code': '', externalID: '', origin: '', }, }; - const largeResultList = [ + const largeResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: false, @@ -1050,7 +1076,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const largeSearchResultList = [ + const largeSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -1083,7 +1109,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const largeWrongSearchResultList = [ + const largeWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -1092,7 +1118,7 @@ describe('OptionsListUtils', () => { }, ]; const emptyCategoriesList = {}; - const emptySelectedResultList = [ + const emptySelectedResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: false, @@ -1201,7 +1227,7 @@ describe('OptionsListUtils', () => { accountID: null, }, }; - const smallResultList = [ + const smallResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: false, @@ -1232,7 +1258,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const smallSearchResultList = [ + const smallSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, @@ -1248,7 +1274,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const smallWrongSearchResultList = [ + const smallWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -1313,7 +1339,7 @@ describe('OptionsListUtils', () => { accountID: null, }, }; - const largeResultList = [ + const largeResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, @@ -1400,7 +1426,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const largeSearchResultList = [ + const largeSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, @@ -1423,7 +1449,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const largeWrongSearchResultList = [ + const largeWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -2177,25 +2203,25 @@ describe('OptionsListUtils', () => { CODE2: { name: 'Tax rate 2', value: '3%', - code: '', - modifiedName: '', + code: 'CODE2', + modifiedName: 'Tax rate 2 (3%)', }, CODE3: { name: 'Tax option 3', value: '5%', - code: '', - modifiedName: '', + code: 'CODE3', + modifiedName: 'Tax option 3 (5%)', }, CODE1: { name: 'Tax exempt 1', value: '0%', - code: '', - modifiedName: '', + code: 'CODE1', + modifiedName: 'Tax exempt 1 (0%) • Default', }, }, }; - const resultList = [ + const resultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: false, @@ -2248,7 +2274,7 @@ describe('OptionsListUtils', () => { }, ]; - const searchResultList = [ + const searchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, @@ -2272,7 +2298,7 @@ describe('OptionsListUtils', () => { }, ]; - const wrongSearchResultList = [ + const wrongSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 41ac11d3a925..9bb2b3b5dcc2 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -4,7 +4,7 @@ import Onyx from 'react-native-onyx'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, Report, ReportAction} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -14,7 +14,7 @@ jest.mock('@libs/Permissions'); const currentUserEmail = 'bjorn@vikings.net'; const currentUserAccountID = 5; -const participantsPersonalDetails = { +const participantsPersonalDetails: PersonalDetailsList = { '1': { accountID: 1, displayName: 'Ragnar Lothbrok', @@ -45,7 +45,7 @@ const participantsPersonalDetails = { login: 'lagertha2@vikings.net', pronouns: 'She/her', }, -} as const; +}; const policy: Policy = { id: '1', From 3a4760d7284f953a12fe99a4c1b506bb9d070881 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Thu, 14 Mar 2024 22:54:14 +0300 Subject: [PATCH 098/218] fix type --- src/components/MoneyRequestConfirmationList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 6d41f8da8af2..e69a15d7eb5c 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -626,7 +626,7 @@ function MoneyRequestConfirmationList({ Date: Thu, 14 Mar 2024 23:10:48 +0300 Subject: [PATCH 099/218] suppress eslint warning --- src/components/MoneyRequestConfirmationList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e69a15d7eb5c..8a5886fe1f4e 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -621,7 +621,7 @@ function MoneyRequestConfirmationList({
)} - + {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {receiptImage || receiptThumbnail ? ( Date: Fri, 15 Mar 2024 11:59:09 +0100 Subject: [PATCH 100/218] Remove unnecessary changes --- src/components/VideoPlayer/BaseVideoPlayer.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js index 17a4ba3548db..bff3f43805e9 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.js +++ b/src/components/VideoPlayer/BaseVideoPlayer.js @@ -257,12 +257,7 @@ function BaseVideoPlayer({ useNativeControls={false} resizeMode={resizeMode} isLooping={isLooping} - onReadyForDisplay={(e) => { - if (isCurrentlyURLSet && !isUploading) { - playVideo(); - } - onVideoLoaded(e); - }} + onReadyForDisplay={onVideoLoaded} onPlaybackStatusUpdate={handlePlaybackStatusUpdate} onFullscreenUpdate={handleFullscreenUpdate} /> From e8d3ec69485aa78de14c7251808a0cdc2fdb7bb4 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Fri, 15 Mar 2024 17:33:44 +0000 Subject: [PATCH 101/218] [TS migration][G3] Lint --- tests/unit/SidebarTest.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/SidebarTest.ts b/tests/unit/SidebarTest.ts index 499af3031553..004667b45fba 100644 --- a/tests/unit/SidebarTest.ts +++ b/tests/unit/SidebarTest.ts @@ -61,11 +61,11 @@ describe('Sidebar', () => { .then(() => { const reportCollection: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, - } + }; const reportAction: ReportActionCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionID]: action}, - } as ReportActionCollectionDataSet + } as ReportActionCollectionDataSet; return Onyx.multiSet({ [ONYXKEYS.BETAS]: betas, @@ -113,11 +113,11 @@ describe('Sidebar', () => { .then(() => { const reportCollection: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, - } + }; const reportAction: ReportActionCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionID]: action}, - } as ReportActionCollectionDataSet + } as ReportActionCollectionDataSet; return Onyx.multiSet({ [ONYXKEYS.BETAS]: betas, From 27cf2b1936e43ef62ae77413fceb6cb98a9dde68 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 18 Mar 2024 11:28:11 +0700 Subject: [PATCH 102/218] Fix prompt permission when component is mounted in mWeb --- .../request/step/IOURequestStepScan/index.js | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 7da97c34cc2b..9bf1b34697fb 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -1,5 +1,5 @@ import lodashGet from 'lodash/get'; -import React, {useCallback, useContext, useReducer, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; import {ActivityIndicator, PanResponder, PixelRatio, View} from 'react-native'; import Hand from '@assets/images/hand.svg'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; @@ -75,6 +75,22 @@ function IOURequestStepScan({ const [isTorchAvailable, setIsTorchAvailable] = useState(false); const cameraRef = useRef(null); + useEffect(() => { + if (!Browser.isMobile()) { + return; + } + navigator.permissions + .query({name: 'camera'}) + .then((permissionState) => { + setCameraPermissionState(permissionState.state); + }) + .catch(() => { + setCameraPermissionState('denied'); + }); + // We only want to get the camera permission status when the component is mounted + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const hideRecieptModal = () => { setIsAttachmentInvalid(false); }; @@ -163,7 +179,17 @@ function IOURequestStepScan({ }; const capturePhoto = useCallback(() => { - if (!cameraRef.current.getScreenshot) { + if (!cameraRef.current || !cameraRef.current.getScreenshot) { + if (cameraPermissionState === 'prompt') { + navigator.mediaDevices + .getUserMedia({video: true}) + .then(() => { + setCameraPermissionState('granted'); + }) + .catch(() => { + setCameraPermissionState('denied'); + }); + } return; } const imageBase64 = cameraRef.current.getScreenshot(); @@ -178,7 +204,7 @@ function IOURequestStepScan({ } navigateToConfirmationStep(); - }, [cameraRef, action, transactionID, updateScanAndNavigate, navigateToConfirmationStep]); + }, [cameraRef, action, transactionID, updateScanAndNavigate, navigateToConfirmationStep, cameraPermissionState]); const panResponder = useRef( PanResponder.create({ @@ -208,18 +234,20 @@ function IOURequestStepScan({ {translate('receipt.cameraAccess')}
)} - setCameraPermissionState('granted')} - onUserMediaError={() => setCameraPermissionState('denied')} - style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} - ref={cameraRef} - screenshotFormat="image/png" - videoConstraints={{facingMode: {exact: 'environment'}}} - torchOn={isFlashLightOn} - onTorchAvailability={setIsTorchAvailable} - forceScreenshotSourceSize - cameraTabIndex={1} - /> + {cameraPermissionState === 'granted' && ( + setCameraPermissionState('granted')} + onUserMediaError={() => setCameraPermissionState('denied')} + style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} + ref={cameraRef} + screenshotFormat="image/png" + videoConstraints={{facingMode: {exact: 'environment'}}} + torchOn={isFlashLightOn} + onTorchAvailability={setIsTorchAvailable} + forceScreenshotSourceSize + cameraTabIndex={1} + /> + )} From 458535dd519d711e16706c93c3849464f4749532 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 18 Mar 2024 15:15:41 +0700 Subject: [PATCH 103/218] update constraints when calling getUserMedia function --- src/pages/iou/request/step/IOURequestStepScan/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 9bf1b34697fb..eeda08d1a06e 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -182,7 +182,7 @@ function IOURequestStepScan({ if (!cameraRef.current || !cameraRef.current.getScreenshot) { if (cameraPermissionState === 'prompt') { navigator.mediaDevices - .getUserMedia({video: true}) + .getUserMedia({video: {facingMode: {exact: 'environment'}}}) .then(() => { setCameraPermissionState('granted'); }) From e90a031945a2a4ab3b9e90af05827b2748edc40e Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 18 Mar 2024 13:14:58 +0100 Subject: [PATCH 104/218] fix: flushSync --- src/components/Composer/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index d8bde63aea8e..8f940d222a83 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -99,6 +99,7 @@ function Composer( const [caretContent, setCaretContent] = useState(''); const [valueBeforeCaret, setValueBeforeCaret] = useState(''); const [textInputWidth, setTextInputWidth] = useState(''); + const [isRendered, setIsRendered] = useState(false); const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); useEffect(() => { @@ -125,7 +126,7 @@ function Composer( const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent) => { const webEvent = event as BaseSyntheticEvent; - if (shouldCalculateCaretPosition && textInput.current) { + if (shouldCalculateCaretPosition && isRendered) { // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state flushSync(() => { setValueBeforeCaret(webEvent.target.value.slice(0, webEvent.nativeEvent.selection.start)); @@ -264,6 +265,7 @@ function Composer( if (typeof ref === 'function') { ref(textInput.current); } + setIsRendered(true); return () => { if (isReportActionCompose) { From a689007e2b3cb925ed18a8bd553dd081121325b4 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 18 Mar 2024 18:05:24 +0100 Subject: [PATCH 105/218] fix: paste in composer for message editing --- src/components/Composer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 8f940d222a83..e46a564fc088 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -166,7 +166,7 @@ function Composer( return true; } - if (textInput.current !== event.target) { + if (textInput.current !== event.target && !(document.activeElement?.nodeName === 'DIV' && document.activeElement?.hasAttribute('contenteditable'))) { const eventTarget = event.target as HTMLInputElement | HTMLTextAreaElement | null; // To make sure the composer does not capture paste events from other inputs, we check where the event originated From f051c2ef1c3032484138c42af04bda18b246a6e3 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 18 Mar 2024 19:01:10 +0000 Subject: [PATCH 106/218] refactor(typescript): apply pull request suggestion --- tests/unit/OptionsListUtilsTest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index bfb9514d7b92..1d5632f0efd9 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import type {Tag} from '@src/libs/OptionsListUtils'; @@ -627,7 +627,7 @@ describe('OptionsListUtils', () => { it('getShareDestinationsOptions()', () => { // Filter current REPORTS as we do in the component, before getting share destination options - const filteredReports = Object.entries(REPORTS).reduce>((reports, [reportKey, report]) => { + const filteredReports = Object.entries(REPORTS).reduce>>((reports, [reportKey, report]) => { if (!ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { return reports; } @@ -655,7 +655,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(1); // Filter current REPORTS_WITH_WORKSPACE_ROOMS as we do in the component, before getting share destination options - const filteredReportsWithWorkspaceRooms = Object.entries(REPORTS_WITH_WORKSPACE_ROOMS).reduce>((reports, [reportKey, report]) => { + const filteredReportsWithWorkspaceRooms = Object.entries(REPORTS_WITH_WORKSPACE_ROOMS).reduce>>((reports, [reportKey, report]) => { if (!ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { return reports; } From b11482b48773dcec3908f3b43d1b3fbee709bdda Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 19 Mar 2024 13:37:59 +0100 Subject: [PATCH 107/218] migrate getGraphiteString and getArtifactInfo to TypeScript --- ...{getArtifactInfo.js => getArtifactInfo.ts} | 22 ++-- .../javascript/getArtifactInfo/index.js | 3 + ...GraphiteString.js => getGraphiteString.ts} | 16 ++- .../javascript/getGraphiteString/index.js | 121 +++++++++--------- .github/scripts/buildActions.sh | 2 +- 5 files changed, 84 insertions(+), 80 deletions(-) rename .github/actions/javascript/getArtifactInfo/{getArtifactInfo.js => getArtifactInfo.ts} (59%) rename .github/actions/javascript/getGraphiteString/{getGraphiteString.js => getGraphiteString.ts} (84%) diff --git a/.github/actions/javascript/getArtifactInfo/getArtifactInfo.js b/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts similarity index 59% rename from .github/actions/javascript/getArtifactInfo/getArtifactInfo.js rename to .github/actions/javascript/getArtifactInfo/getArtifactInfo.ts index c2398a34db4b..60ccd7ef503d 100644 --- a/.github/actions/javascript/getArtifactInfo/getArtifactInfo.js +++ b/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts @@ -1,13 +1,15 @@ -const _ = require('underscore'); -const core = require('@actions/core'); -const GithubUtils = require('../../../libs/GithubUtils'); +import core from '@actions/core'; +import type {components as OctokitComponents} from '@octokit/openapi-types/types'; +import GithubUtils from '../../../libs/GithubUtils'; -const run = function () { +type OctokitArtifact = OctokitComponents['schemas']['artifact']; + +const run = function (): Promise { const artifactName = core.getInput('ARTIFACT_NAME', {required: true}); return GithubUtils.getArtifactByName(artifactName) - .then((artifact) => { - if (_.isUndefined(artifact)) { + .then((artifact: OctokitArtifact) => { + if (artifact === undefined) { console.log(`No artifact found with the name ${artifactName}`); core.setOutput('ARTIFACT_FOUND', false); return; @@ -16,16 +18,16 @@ const run = function () { console.log('Artifact info', artifact); core.setOutput('ARTIFACT_FOUND', true); core.setOutput('ARTIFACT_ID', artifact.id); - core.setOutput('ARTIFACT_WORKFLOW_ID', artifact.workflow_run.id); + core.setOutput('ARTIFACT_WORKFLOW_ID', artifact.workflow_run?.id); }) - .catch((error) => { + .catch((error: Error) => { console.error('A problem occurred while trying to communicate with the GitHub API', error); core.setFailed(error); - }); + }) as Promise; }; if (require.main === module) { run(); } -module.exports = run; +export default run; diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index ea56ff5f4ebd..90928af8bede 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -1,3 +1,6 @@ +/** + * NOTE: This is a compiled file. DO NOT directly edit this file. + */ /** * NOTE: This is a compiled file. DO NOT directly edit this file. */ diff --git a/.github/actions/javascript/getGraphiteString/getGraphiteString.js b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts similarity index 84% rename from .github/actions/javascript/getGraphiteString/getGraphiteString.js rename to .github/actions/javascript/getGraphiteString/getGraphiteString.ts index d4504a79ef6f..882333dd786a 100644 --- a/.github/actions/javascript/getGraphiteString/getGraphiteString.js +++ b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts @@ -1,6 +1,6 @@ -const core = require('@actions/core'); -const github = require('@actions/github'); -const fs = require('fs'); +import core from '@actions/core'; +import github from '@actions/github'; +import fs from 'fs'; const run = () => { // Prefix path to the graphite metric @@ -11,8 +11,10 @@ const run = () => { regressionOutput = JSON.parse(fs.readFileSync('.reassure/output.json', 'utf8')); } catch (err) { // Handle errors that occur during file reading or parsing - console.error('Error while parsing output.json:', err.message); - core.setFailed(err); + if (err instanceof Error) { + console.error('Error while parsing output.json:', err.message); + core.setFailed(err); + } } const creationDate = regressionOutput.metadata.current.creationDate; @@ -22,7 +24,7 @@ const run = () => { const timestamp = Math.floor(timestampInMili / 1000); // Get PR number from the github context - const prNumber = github.context.payload.pull_request.number; + const prNumber = github.context.payload.pull_request?.number; // We need to combine all tests from the 4 buckets const reassureTests = [...regressionOutput.meaningless, ...regressionOutput.significant, ...regressionOutput.countChanged, ...regressionOutput.added]; @@ -51,4 +53,4 @@ if (require.main === module) { run(); } -module.exports = run; +export default run; diff --git a/.github/actions/javascript/getGraphiteString/index.js b/.github/actions/javascript/getGraphiteString/index.js index f4bce400e413..4df71d0da7c5 100644 --- a/.github/actions/javascript/getGraphiteString/index.js +++ b/.github/actions/javascript/getGraphiteString/index.js @@ -4,67 +4,6 @@ /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ -/***/ 1302: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { - -const core = __nccwpck_require__(2186); -const github = __nccwpck_require__(5438); -const fs = __nccwpck_require__(7147); - -const run = () => { - // Prefix path to the graphite metric - const GRAPHITE_PATH = 'reassure'; - - let regressionOutput; - try { - regressionOutput = JSON.parse(fs.readFileSync('.reassure/output.json', 'utf8')); - } catch (err) { - // Handle errors that occur during file reading or parsing - console.error('Error while parsing output.json:', err.message); - core.setFailed(err); - } - - const creationDate = regressionOutput.metadata.current.creationDate; - const timestampInMili = new Date(creationDate).getTime(); - - // Graphite accepts timestamp in seconds - const timestamp = Math.floor(timestampInMili / 1000); - - // Get PR number from the github context - const prNumber = github.context.payload.pull_request.number; - - // We need to combine all tests from the 4 buckets - const reassureTests = [...regressionOutput.meaningless, ...regressionOutput.significant, ...regressionOutput.countChanged, ...regressionOutput.added]; - - // Map through every test and create string for meanDuration and meanCount - // eslint-disable-next-line rulesdir/prefer-underscore-method - const graphiteString = reassureTests - .map((test) => { - const current = test.current; - // Graphite doesn't accept metrics name with space, we replace spaces with "-" - const formattedName = current.name.split(' ').join('-'); - - const renderDurationString = `${GRAPHITE_PATH}.${formattedName}.renderDuration ${current.meanDuration} ${timestamp}`; - const renderCountString = `${GRAPHITE_PATH}.${formattedName}.renderCount ${current.meanCount} ${timestamp}`; - const renderPRNumberString = `${GRAPHITE_PATH}.${formattedName}.prNumber ${prNumber} ${timestamp}`; - - return `${renderDurationString}\n${renderCountString}\n${renderPRNumberString}`; - }) - .join('\n'); - - // Set generated graphite string to the github variable - core.setOutput('GRAPHITE_STRING', graphiteString); -}; - -if (require.main === require.cache[eval('__filename')]) { - run(); -} - -module.exports = run; - - -/***/ }), - /***/ 7351: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { @@ -9565,6 +9504,64 @@ function wrappy (fn, cb) { } +/***/ }), + +/***/ 7717: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const core_1 = __importDefault(__nccwpck_require__(2186)); +const github_1 = __importDefault(__nccwpck_require__(5438)); +const fs_1 = __importDefault(__nccwpck_require__(7147)); +const run = () => { + // Prefix path to the graphite metric + const GRAPHITE_PATH = 'reassure'; + let regressionOutput; + try { + regressionOutput = JSON.parse(fs_1.default.readFileSync('.reassure/output.json', 'utf8')); + } + catch (err) { + // Handle errors that occur during file reading or parsing + if (err instanceof Error) { + console.error('Error while parsing output.json:', err.message); + core_1.default.setFailed(err); + } + } + const creationDate = regressionOutput.metadata.current.creationDate; + const timestampInMili = new Date(creationDate).getTime(); + // Graphite accepts timestamp in seconds + const timestamp = Math.floor(timestampInMili / 1000); + // Get PR number from the github context + const prNumber = github_1.default.context.payload.pull_request?.number; + // We need to combine all tests from the 4 buckets + const reassureTests = [...regressionOutput.meaningless, ...regressionOutput.significant, ...regressionOutput.countChanged, ...regressionOutput.added]; + // Map through every test and create string for meanDuration and meanCount + // eslint-disable-next-line rulesdir/prefer-underscore-method + const graphiteString = reassureTests + .map((test) => { + const current = test.current; + // Graphite doesn't accept metrics name with space, we replace spaces with "-" + const formattedName = current.name.split(' ').join('-'); + const renderDurationString = `${GRAPHITE_PATH}.${formattedName}.renderDuration ${current.meanDuration} ${timestamp}`; + const renderCountString = `${GRAPHITE_PATH}.${formattedName}.renderCount ${current.meanCount} ${timestamp}`; + const renderPRNumberString = `${GRAPHITE_PATH}.${formattedName}.prNumber ${prNumber} ${timestamp}`; + return `${renderDurationString}\n${renderCountString}\n${renderPRNumberString}`; + }) + .join('\n'); + // Set generated graphite string to the github variable + core_1.default.setOutput('GRAPHITE_STRING', graphiteString); +}; +if (require.main === require.cache[eval('__filename')]) { + run(); +} +exports["default"] = run; + + /***/ }), /***/ 2877: @@ -9745,7 +9742,7 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"] /******/ // startup /******/ // Load entry module and return exports /******/ // This entry module is referenced by other modules so it can't be inlined -/******/ var __webpack_exports__ = __nccwpck_require__(1302); +/******/ var __webpack_exports__ = __nccwpck_require__(7717); /******/ module.exports = __webpack_exports__; /******/ /******/ })() diff --git a/.github/scripts/buildActions.sh b/.github/scripts/buildActions.sh index 48acb931aeec..9bcd22e851b3 100755 --- a/.github/scripts/buildActions.sh +++ b/.github/scripts/buildActions.sh @@ -25,7 +25,7 @@ declare -r GITHUB_ACTIONS=( "$ACTIONS_DIR/authorChecklist/authorChecklist.ts" "$ACTIONS_DIR/reviewerChecklist/reviewerChecklist.js" "$ACTIONS_DIR/validateReassureOutput/validateReassureOutput.js" - "$ACTIONS_DIR/getGraphiteString/getGraphiteString.js" + "$ACTIONS_DIR/getGraphiteString/getGraphiteString.ts" "$ACTIONS_DIR/getArtifactInfo/getArtifactInfo.js" ) From a7dba179d8142c7c0ba0bd70c54549512d902fca Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 19 Mar 2024 14:08:47 +0100 Subject: [PATCH 108/218] update file extension in buildActions.sh --- .../javascript/getArtifactInfo/index.js | 81 +++++++++---------- .github/scripts/buildActions.sh | 2 +- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index 90928af8bede..c378a4513f85 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -1,50 +1,9 @@ -/** - * NOTE: This is a compiled file. DO NOT directly edit this file. - */ /** * NOTE: This is a compiled file. DO NOT directly edit this file. */ /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ -/***/ 1307: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { - -const _ = __nccwpck_require__(5067); -const core = __nccwpck_require__(2186); -const GithubUtils = __nccwpck_require__(7999); - -const run = function () { - const artifactName = core.getInput('ARTIFACT_NAME', {required: true}); - - return GithubUtils.getArtifactByName(artifactName) - .then((artifact) => { - if (_.isUndefined(artifact)) { - console.log(`No artifact found with the name ${artifactName}`); - core.setOutput('ARTIFACT_FOUND', false); - return; - } - - console.log('Artifact info', artifact); - core.setOutput('ARTIFACT_FOUND', true); - core.setOutput('ARTIFACT_ID', artifact.id); - core.setOutput('ARTIFACT_WORKFLOW_ID', artifact.workflow_run.id); - }) - .catch((error) => { - console.error('A problem occurred while trying to communicate with the GitHub API', error); - core.setFailed(error); - }); -}; - -if (require.main === require.cache[eval('__filename')]) { - run(); -} - -module.exports = run; - - -/***/ }), - /***/ 4097: /***/ ((module) => { @@ -13640,6 +13599,44 @@ function wrappy (fn, cb) { } +/***/ }), + +/***/ 7750: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const core_1 = __importDefault(__nccwpck_require__(2186)); +const GithubUtils_1 = __importDefault(__nccwpck_require__(7999)); +const run = function () { + const artifactName = core_1.default.getInput('ARTIFACT_NAME', { required: true }); + return GithubUtils_1.default.getArtifactByName(artifactName) + .then((artifact) => { + if (artifact === undefined) { + console.log(`No artifact found with the name ${artifactName}`); + core_1.default.setOutput('ARTIFACT_FOUND', false); + return; + } + console.log('Artifact info', artifact); + core_1.default.setOutput('ARTIFACT_FOUND', true); + core_1.default.setOutput('ARTIFACT_ID', artifact.id); + core_1.default.setOutput('ARTIFACT_WORKFLOW_ID', artifact.workflow_run?.id); + }) + .catch((error) => { + console.error('A problem occurred while trying to communicate with the GitHub API', error); + core_1.default.setFailed(error); + }); +}; +if (require.main === require.cache[eval('__filename')]) { + run(); +} +exports["default"] = run; + + /***/ }), /***/ 2877: @@ -16003,7 +16000,7 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"] /******/ // startup /******/ // Load entry module and return exports /******/ // This entry module is referenced by other modules so it can't be inlined -/******/ var __webpack_exports__ = __nccwpck_require__(1307); +/******/ var __webpack_exports__ = __nccwpck_require__(7750); /******/ module.exports = __webpack_exports__; /******/ /******/ })() diff --git a/.github/scripts/buildActions.sh b/.github/scripts/buildActions.sh index 9bcd22e851b3..a303d224d9a7 100755 --- a/.github/scripts/buildActions.sh +++ b/.github/scripts/buildActions.sh @@ -26,7 +26,7 @@ declare -r GITHUB_ACTIONS=( "$ACTIONS_DIR/reviewerChecklist/reviewerChecklist.js" "$ACTIONS_DIR/validateReassureOutput/validateReassureOutput.js" "$ACTIONS_DIR/getGraphiteString/getGraphiteString.ts" - "$ACTIONS_DIR/getArtifactInfo/getArtifactInfo.js" + "$ACTIONS_DIR/getArtifactInfo/getArtifactInfo.ts" ) # This will be inserted at the top of all compiled files as a warning to devs. From 7fd4d9127dcb0706994189590fd44953aa7c4873 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 19 Mar 2024 15:44:42 +0100 Subject: [PATCH 109/218] apply suggested changes --- .../getArtifactInfo/getArtifactInfo.ts | 2 +- .../javascript/getArtifactInfo/index.js | 37 +++++++++++++++---- .../getGraphiteString/getGraphiteString.ts | 4 +- .../javascript/getGraphiteString/index.js | 33 ++++++++++++++--- 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts b/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts index 60ccd7ef503d..52f1b5458ab7 100644 --- a/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts +++ b/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts @@ -1,4 +1,4 @@ -import core from '@actions/core'; +import * as core from '@actions/core'; import type {components as OctokitComponents} from '@octokit/openapi-types/types'; import GithubUtils from '../../../libs/GithubUtils'; diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index c378a4513f85..112251c8c918 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -13606,29 +13606,52 @@ function wrappy (fn, cb) { "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -const core_1 = __importDefault(__nccwpck_require__(2186)); +const core = __importStar(__nccwpck_require__(2186)); const GithubUtils_1 = __importDefault(__nccwpck_require__(7999)); const run = function () { - const artifactName = core_1.default.getInput('ARTIFACT_NAME', { required: true }); + const artifactName = core.getInput('ARTIFACT_NAME', { required: true }); return GithubUtils_1.default.getArtifactByName(artifactName) .then((artifact) => { if (artifact === undefined) { console.log(`No artifact found with the name ${artifactName}`); - core_1.default.setOutput('ARTIFACT_FOUND', false); + core.setOutput('ARTIFACT_FOUND', false); return; } console.log('Artifact info', artifact); - core_1.default.setOutput('ARTIFACT_FOUND', true); - core_1.default.setOutput('ARTIFACT_ID', artifact.id); - core_1.default.setOutput('ARTIFACT_WORKFLOW_ID', artifact.workflow_run?.id); + core.setOutput('ARTIFACT_FOUND', true); + core.setOutput('ARTIFACT_ID', artifact.id); + core.setOutput('ARTIFACT_WORKFLOW_ID', artifact.workflow_run?.id); }) .catch((error) => { console.error('A problem occurred while trying to communicate with the GitHub API', error); - core_1.default.setFailed(error); + core.setFailed(error); }); }; if (require.main === require.cache[eval('__filename')]) { diff --git a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts index 882333dd786a..54132ab8c322 100644 --- a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts +++ b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts @@ -1,5 +1,5 @@ -import core from '@actions/core'; -import github from '@actions/github'; +import * as core from '@actions/core'; +import * as github from '@actions/github'; import fs from 'fs'; const run = () => { diff --git a/.github/actions/javascript/getGraphiteString/index.js b/.github/actions/javascript/getGraphiteString/index.js index 4df71d0da7c5..43d1de6724e3 100644 --- a/.github/actions/javascript/getGraphiteString/index.js +++ b/.github/actions/javascript/getGraphiteString/index.js @@ -9511,12 +9511,35 @@ function wrappy (fn, cb) { "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -const core_1 = __importDefault(__nccwpck_require__(2186)); -const github_1 = __importDefault(__nccwpck_require__(5438)); +const core = __importStar(__nccwpck_require__(2186)); +const github = __importStar(__nccwpck_require__(5438)); const fs_1 = __importDefault(__nccwpck_require__(7147)); const run = () => { // Prefix path to the graphite metric @@ -9529,7 +9552,7 @@ const run = () => { // Handle errors that occur during file reading or parsing if (err instanceof Error) { console.error('Error while parsing output.json:', err.message); - core_1.default.setFailed(err); + core.setFailed(err); } } const creationDate = regressionOutput.metadata.current.creationDate; @@ -9537,7 +9560,7 @@ const run = () => { // Graphite accepts timestamp in seconds const timestamp = Math.floor(timestampInMili / 1000); // Get PR number from the github context - const prNumber = github_1.default.context.payload.pull_request?.number; + const prNumber = github.context.payload.pull_request?.number; // We need to combine all tests from the 4 buckets const reassureTests = [...regressionOutput.meaningless, ...regressionOutput.significant, ...regressionOutput.countChanged, ...regressionOutput.added]; // Map through every test and create string for meanDuration and meanCount @@ -9554,7 +9577,7 @@ const run = () => { }) .join('\n'); // Set generated graphite string to the github variable - core_1.default.setOutput('GRAPHITE_STRING', graphiteString); + core.setOutput('GRAPHITE_STRING', graphiteString); }; if (require.main === require.cache[eval('__filename')]) { run(); From 394f9df145888319b1adcbb0375a5a1b6c51ed0b Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 19 Mar 2024 15:48:38 +0100 Subject: [PATCH 110/218] add todo comment --- .github/actions/javascript/getArtifactInfo/getArtifactInfo.ts | 1 + .github/actions/javascript/getArtifactInfo/index.js | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts b/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts index 52f1b5458ab7..51a6354b34f8 100644 --- a/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts +++ b/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts @@ -7,6 +7,7 @@ type OctokitArtifact = OctokitComponents['schemas']['artifact']; const run = function (): Promise { const artifactName = core.getInput('ARTIFACT_NAME', {required: true}); + // TODO: remove type casting once GithubUtils (https://github.com/Expensify/App/pull/38280) is migrated to TypeScript return GithubUtils.getArtifactByName(artifactName) .then((artifact: OctokitArtifact) => { if (artifact === undefined) { diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index 112251c8c918..cb6d5524332b 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -13637,6 +13637,7 @@ const core = __importStar(__nccwpck_require__(2186)); const GithubUtils_1 = __importDefault(__nccwpck_require__(7999)); const run = function () { const artifactName = core.getInput('ARTIFACT_NAME', { required: true }); + // TODO: remove type casting once GithubUtils (https://github.com/Expensify/App/pull/38280) is migrated to TypeScript return GithubUtils_1.default.getArtifactByName(artifactName) .then((artifact) => { if (artifact === undefined) { From df50c85d91faa7be2d47c8df0c94cd5cc71a016a Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 19 Mar 2024 22:32:04 +0700 Subject: [PATCH 111/218] fix: console page scrolls to new data position --- src/pages/settings/AboutPage/ConsolePage.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index 50cbce314be4..c4e98ec9c18b 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -1,7 +1,7 @@ import {format} from 'date-fns'; import isEmpty from 'lodash/isEmpty'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {FlatList, View} from 'react-native'; +import {View} from 'react-native'; import type {ListRenderItem, ListRenderItemInfo} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -9,6 +9,7 @@ import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; +import InvertedFlatList from '@components/InvertedFlatList'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; @@ -69,7 +70,18 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const logsList = useMemo(() => (logs ? Object.values(logs).reverse() : []), [logs]); + const logsList = useMemo( + () => + logs + ? Object.entries(logs) + .map(([key, value]) => ({ + key, + ...value, + })) + .reverse() + : [], + [logs], + ); useEffect(() => { if (!shouldStoreLogs) { @@ -136,11 +148,10 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_TROUBLESHOOT)} /> - {translate('initialSettingsPage.debugConsole.noLogsAvailable')}} /> From 08692ed46c996589d2131e3c3ecae542b12d76e0 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 19 Mar 2024 20:16:16 +0100 Subject: [PATCH 112/218] fix: repeated pasting --- src/components/Composer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index e46a564fc088..d3b21bacc892 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -259,7 +259,7 @@ function Composer( updateNumberOfLines(); }, [updateNumberOfLines]); - useHtmlPaste(textInput, handlePaste, true); + useHtmlPaste(textInput, handlePaste, false); useEffect(() => { if (typeof ref === 'function') { From c32c8fb068b3f8d039f069d5e417c7955c5c26a5 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Tue, 19 Mar 2024 22:21:13 +0300 Subject: [PATCH 113/218] updated label font size and line heights --- src/components/EReceiptThumbnail.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index fc4e67441994..e0f88470f460 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -70,15 +70,21 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT let receiptIconWidth: number = variables.eReceiptIconWidth; let receiptIconHeight: number = variables.eReceiptIconHeight; let receiptMCCSize: number = variables.eReceiptMCCHeightWidth; + let labelFontSize: number = variables.fontSizeNormal; + let labelLineHeight: number = variables.lineHeightLarge; if (iconSize === 'small') { receiptIconWidth = variables.eReceiptIconWidthSmall; receiptIconHeight = variables.eReceiptIconHeightSmall; receiptMCCSize = variables.eReceiptMCCHeightWidthSmall; + labelFontSize = variables.fontSizeExtraSmall; + labelLineHeight = variables.lineHeightXSmall; } else if (iconSize === 'medium') { receiptIconWidth = variables.eReceiptIconWidthMedium; receiptIconHeight = variables.eReceiptIconHeightMedium; receiptMCCSize = variables.eReceiptMCCHeightWidthMedium; + labelFontSize = variables.fontSizeLabel; + labelLineHeight = variables.lineHeightNormal; } return ( @@ -107,7 +113,16 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT additionalStyles={[styles.fullScreen]} /> {isReceiptThumbnail && fileExtension && ( - {fileExtension.toUpperCase()} + + {fileExtension.toUpperCase()} + )} {MCCIcon && !isReceiptThumbnail ? ( Date: Wed, 20 Mar 2024 14:21:30 +0800 Subject: [PATCH 114/218] update all parent/ancestor thread data --- src/libs/ReportUtils.ts | 52 ++++++++++++++++++-------------------- src/libs/actions/Report.ts | 8 ++---- src/libs/actions/Task.ts | 8 ++---- 3 files changed, 28 insertions(+), 40 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 826d58ee6ecc..4e4a1f0446f1 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2957,34 +2957,6 @@ function updateOptimisticParentReportAction(parentReportAction: OnyxEntry { + const ancestorReport = getReport(ancestors.reportIDs[index]); + const ancestorReportAction = ReportActionsUtils.getReportAction(ancestorReport.reportID, ancestors.reportActionsIDs[index]); + return { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`, + value: { + [ancestorReportAction?.reportActionID ?? '']: updateOptimisticParentReportAction(ancestorReportAction, lastVisibleActionCreated, type), + }, + }; + }); +} + function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry | EmptyObject): boolean { if (isEmptyObject(policy)) { return false; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 49ecfce36cf0..4251fdfbcb76 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -504,9 +504,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { // Update optimistic data for parent report action if the report is a child report const optimisticParentReportData = ReportUtils.getOptimisticDataForParentReportAction(reportID, currentTime, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - if (!isEmptyObject(optimisticParentReportData)) { - optimisticData.push(optimisticParentReportData); - } + optimisticParentReportData.forEach((parentReportData) => optimisticData.push(parentReportData)); // Update the timezone if it's been 5 minutes from the last time the user added a comment if (DateUtils.canUpdateTimezone() && currentUserAccountID) { @@ -1228,9 +1226,7 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { optimisticReport?.lastVisibleActionCreated ?? '', CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); - if (!isEmptyObject(optimisticParentReportData)) { - optimisticData.push(optimisticParentReportData); - } + optimisticParentReportData.forEach((parentReportData) => optimisticData.push(parentReportData)); } const parameters: DeleteCommentParams = { diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 681ed0ec383f..e3a9eab8ebe8 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -249,9 +249,7 @@ function createTaskAndNavigate( // If needed, update optimistic data for parent report action of the parent report. const optimisticParentReportData = ReportUtils.getOptimisticDataForParentReportAction(parentReportID, currentTime, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - if (!isEmptyObject(optimisticParentReportData)) { - optimisticData.push(optimisticParentReportData); - } + optimisticParentReportData.forEach((parentReportData) => optimisticData.push(parentReportData)); // FOR PARENT REPORT (SHARE DESTINATION) successData.push({ @@ -861,9 +859,7 @@ function deleteTask(report: OnyxEntry) { parentReport?.lastVisibleActionCreated ?? '', CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); - if (!isEmptyObject(optimisticParentReportData)) { - optimisticData.push(optimisticParentReportData); - } + optimisticParentReportData.forEach((parentReportData) => optimisticData.push(parentReportData)); } const successData: OnyxUpdate[] = [ From aa98e80dda2c2dd4723e385bc01e6b86274029cd Mon Sep 17 00:00:00 2001 From: Tienifr <113963320+tienifr@users.noreply.github.com> Date: Wed, 20 Mar 2024 13:29:13 +0700 Subject: [PATCH 115/218] Rewrite ternary Co-authored-by: G T <75031127+getusha@users.noreply.github.com> --- src/pages/settings/AboutPage/ConsolePage.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index c4e98ec9c18b..2de9c1bf218e 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -71,15 +71,10 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { const styles = useThemeStyles(); const logsList = useMemo( - () => - logs - ? Object.entries(logs) - .map(([key, value]) => ({ - key, - ...value, - })) - .reverse() - : [], + () => Object.entries(logs ?? {}) + .map(([key, value]) => ({ key, ...value })) + .reverse(), + [logs], [logs], ); From 2b732142ded4d8fed0842f9dfd47e863b6834e37 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 20 Mar 2024 13:37:18 +0700 Subject: [PATCH 116/218] fix typo --- src/pages/settings/AboutPage/ConsolePage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index 2de9c1bf218e..8b4f7a6ebc36 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -71,10 +71,10 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { const styles = useThemeStyles(); const logsList = useMemo( - () => Object.entries(logs ?? {}) - .map(([key, value]) => ({ key, ...value })) - .reverse(), - [logs], + () => + Object.entries(logs ?? {}) + .map(([key, value]) => ({key, ...value})) + .reverse(), [logs], ); From cbdba9e41b763dbdc906647a077b6d732dcf1e3e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 20 Mar 2024 14:44:46 +0800 Subject: [PATCH 117/218] fix type --- src/libs/ReportUtils.ts | 17 ++++++++++++++++- src/libs/actions/Report.ts | 12 ++++++++++-- src/libs/actions/Task.ts | 12 ++++++++++-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4e4a1f0446f1..db6587b47ca8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5258,14 +5258,29 @@ function getAllAncestorReportActionIDs(report: Report | null | undefined): Ances * @param lastVisibleActionCreated Last visible action created of the child report * @param type The type of action in the child report */ -function getOptimisticDataForParentReportAction(reportID: string, lastVisibleActionCreated: string, type: string): OnyxUpdate[] { +function getOptimisticDataForParentReportAction(reportID: string, lastVisibleActionCreated: string, type: string): (OnyxUpdate | EmptyObject)[] { const report = getReport(reportID); + + if (!report || isEmptyObject(report)) { + return []; + } + const ancestors = getAllAncestorReportActionIDs(report); const totalAncestor = ancestors.reportIDs.length; return Array.from(Array(totalAncestor), (_, index) => { const ancestorReport = getReport(ancestors.reportIDs[index]); + + if (!ancestorReport || isEmptyObject(ancestorReport)) { + return {} as EmptyObject; + } + const ancestorReportAction = ReportActionsUtils.getReportAction(ancestorReport.reportID, ancestors.reportActionsIDs[index]); + + if (!ancestorReportAction || isEmptyObject(ancestorReportAction)) { + return {} as EmptyObject; + } + return { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 4251fdfbcb76..f35c89120a66 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -504,7 +504,11 @@ function addActions(reportID: string, text = '', file?: FileObject) { // Update optimistic data for parent report action if the report is a child report const optimisticParentReportData = ReportUtils.getOptimisticDataForParentReportAction(reportID, currentTime, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - optimisticParentReportData.forEach((parentReportData) => optimisticData.push(parentReportData)); + optimisticParentReportData.forEach((parentReportData) => { + if (!isEmptyObject(parentReportData)) { + optimisticData.push(parentReportData); + } + }); // Update the timezone if it's been 5 minutes from the last time the user added a comment if (DateUtils.canUpdateTimezone() && currentUserAccountID) { @@ -1226,7 +1230,11 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { optimisticReport?.lastVisibleActionCreated ?? '', CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); - optimisticParentReportData.forEach((parentReportData) => optimisticData.push(parentReportData)); + optimisticParentReportData.forEach((parentReportData) => { + if (!isEmptyObject(parentReportData)) { + optimisticData.push(parentReportData); + } + }); } const parameters: DeleteCommentParams = { diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index e3a9eab8ebe8..9de13f5e4138 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -249,7 +249,11 @@ function createTaskAndNavigate( // If needed, update optimistic data for parent report action of the parent report. const optimisticParentReportData = ReportUtils.getOptimisticDataForParentReportAction(parentReportID, currentTime, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - optimisticParentReportData.forEach((parentReportData) => optimisticData.push(parentReportData)); + optimisticParentReportData.forEach((parentReportData) => { + if (!isEmptyObject(parentReportData)) { + optimisticData.push(parentReportData); + } + }); // FOR PARENT REPORT (SHARE DESTINATION) successData.push({ @@ -859,7 +863,11 @@ function deleteTask(report: OnyxEntry) { parentReport?.lastVisibleActionCreated ?? '', CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); - optimisticParentReportData.forEach((parentReportData) => optimisticData.push(parentReportData)); + optimisticParentReportData.forEach((parentReportData) => { + if (!isEmptyObject(parentReportData)) { + optimisticData.push(parentReportData); + } + }); } const successData: OnyxUpdate[] = [ From 4abbbd4b01c81e2db3f23305b9bf23ca71602540 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 20 Mar 2024 14:52:11 +0800 Subject: [PATCH 118/218] fix lint --- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Report.ts | 10 ++++++---- src/libs/actions/Task.ts | 10 ++++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index db6587b47ca8..4463d484e79c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5258,7 +5258,7 @@ function getAllAncestorReportActionIDs(report: Report | null | undefined): Ances * @param lastVisibleActionCreated Last visible action created of the child report * @param type The type of action in the child report */ -function getOptimisticDataForParentReportAction(reportID: string, lastVisibleActionCreated: string, type: string): (OnyxUpdate | EmptyObject)[] { +function getOptimisticDataForParentReportAction(reportID: string, lastVisibleActionCreated: string, type: string): Array { const report = getReport(reportID); if (!report || isEmptyObject(report)) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index f35c89120a66..f39acba8268c 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -505,9 +505,10 @@ function addActions(reportID: string, text = '', file?: FileObject) { // Update optimistic data for parent report action if the report is a child report const optimisticParentReportData = ReportUtils.getOptimisticDataForParentReportAction(reportID, currentTime, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); optimisticParentReportData.forEach((parentReportData) => { - if (!isEmptyObject(parentReportData)) { - optimisticData.push(parentReportData); + if (isEmptyObject(parentReportData)) { + return; } + optimisticData.push(parentReportData); }); // Update the timezone if it's been 5 minutes from the last time the user added a comment @@ -1231,9 +1232,10 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); optimisticParentReportData.forEach((parentReportData) => { - if (!isEmptyObject(parentReportData)) { - optimisticData.push(parentReportData); + if (isEmptyObject(parentReportData)) { + return; } + optimisticData.push(parentReportData); }); } diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 9de13f5e4138..e98acda277b0 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -250,9 +250,10 @@ function createTaskAndNavigate( // If needed, update optimistic data for parent report action of the parent report. const optimisticParentReportData = ReportUtils.getOptimisticDataForParentReportAction(parentReportID, currentTime, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); optimisticParentReportData.forEach((parentReportData) => { - if (!isEmptyObject(parentReportData)) { - optimisticData.push(parentReportData); + if (isEmptyObject(parentReportData)) { + return; } + optimisticData.push(parentReportData); }); // FOR PARENT REPORT (SHARE DESTINATION) @@ -864,9 +865,10 @@ function deleteTask(report: OnyxEntry) { CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); optimisticParentReportData.forEach((parentReportData) => { - if (!isEmptyObject(parentReportData)) { - optimisticData.push(parentReportData); + if (isEmptyObject(parentReportData)) { + return; } + optimisticData.push(parentReportData); }); } From 7e945138e77394834e5e03662864e8a8eae7b351 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 20 Mar 2024 16:02:21 +0800 Subject: [PATCH 119/218] allow to include transaction thread --- src/libs/ReportUtils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4463d484e79c..30ea323f4d0f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5219,7 +5219,7 @@ function getAllAncestorReportActions(report: Report | null | undefined, shouldHi return allAncestors.reverse(); } -function getAllAncestorReportActionIDs(report: Report | null | undefined): AncestorIDs { +function getAllAncestorReportActionIDs(report: Report | null | undefined, includeTransactionThread: boolean = false): AncestorIDs { if (!report) { return { reportIDs: [], @@ -5238,7 +5238,7 @@ function getAllAncestorReportActionIDs(report: Report | null | undefined): Ances const parentReport = getReport(parentReportID); const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '0'); - if (!parentReportAction || ReportActionsUtils.isTransactionThread(parentReportAction) || !parentReport) { + if (!parentReportAction || (!includeTransactionThread && ReportActionsUtils.isTransactionThread(parentReportAction)) || !parentReport) { break; } @@ -5265,9 +5265,11 @@ function getOptimisticDataForParentReportAction(reportID: string, lastVisibleAct return []; } - const ancestors = getAllAncestorReportActionIDs(report); + const ancestors = getAllAncestorReportActionIDs(report, true); const totalAncestor = ancestors.reportIDs.length; + console.log('total ancestor', totalAncestor) + return Array.from(Array(totalAncestor), (_, index) => { const ancestorReport = getReport(ancestors.reportIDs[index]); From f5afda1ee8707a0719593bd1965ccf28239b3f6f Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 20 Mar 2024 16:08:09 +0800 Subject: [PATCH 120/218] fix lint --- src/libs/ReportUtils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 30ea323f4d0f..21b2071e0504 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5219,7 +5219,7 @@ function getAllAncestorReportActions(report: Report | null | undefined, shouldHi return allAncestors.reverse(); } -function getAllAncestorReportActionIDs(report: Report | null | undefined, includeTransactionThread: boolean = false): AncestorIDs { +function getAllAncestorReportActionIDs(report: Report | null | undefined, includeTransactionThread = false): AncestorIDs { if (!report) { return { reportIDs: [], @@ -5268,8 +5268,6 @@ function getOptimisticDataForParentReportAction(reportID: string, lastVisibleAct const ancestors = getAllAncestorReportActionIDs(report, true); const totalAncestor = ancestors.reportIDs.length; - console.log('total ancestor', totalAncestor) - return Array.from(Array(totalAncestor), (_, index) => { const ancestorReport = getReport(ancestors.reportIDs[index]); From e0b672691e083bdf3afef2022645b97a22c39c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 20 Mar 2024 09:52:52 +0100 Subject: [PATCH 121/218] Readd video starting after video is ready to play --- src/components/VideoPlayer/BaseVideoPlayer.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js index bff3f43805e9..17a4ba3548db 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.js +++ b/src/components/VideoPlayer/BaseVideoPlayer.js @@ -257,7 +257,12 @@ function BaseVideoPlayer({ useNativeControls={false} resizeMode={resizeMode} isLooping={isLooping} - onReadyForDisplay={onVideoLoaded} + onReadyForDisplay={(e) => { + if (isCurrentlyURLSet && !isUploading) { + playVideo(); + } + onVideoLoaded(e); + }} onPlaybackStatusUpdate={handlePlaybackStatusUpdate} onFullscreenUpdate={handleFullscreenUpdate} /> From 16e5c0ca3603c1327388c079ef087a06958c0876 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 20 Mar 2024 15:00:29 +0100 Subject: [PATCH 122/218] update type casting in getArtifactInfo --- .github/actions/javascript/getArtifactInfo/getArtifactInfo.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts b/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts index 51a6354b34f8..65c45bbdb3e8 100644 --- a/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts +++ b/.github/actions/javascript/getArtifactInfo/getArtifactInfo.ts @@ -4,7 +4,7 @@ import GithubUtils from '../../../libs/GithubUtils'; type OctokitArtifact = OctokitComponents['schemas']['artifact']; -const run = function (): Promise { +const run = function (): Promise { const artifactName = core.getInput('ARTIFACT_NAME', {required: true}); // TODO: remove type casting once GithubUtils (https://github.com/Expensify/App/pull/38280) is migrated to TypeScript @@ -24,7 +24,7 @@ const run = function (): Promise { .catch((error: Error) => { console.error('A problem occurred while trying to communicate with the GitHub API', error); core.setFailed(error); - }) as Promise; + }) as Promise; }; if (require.main === module) { From 776a81985db29111f276fb4976b763cd5a4307b5 Mon Sep 17 00:00:00 2001 From: Rohan Sasne Date: Wed, 20 Mar 2024 19:31:24 +0530 Subject: [PATCH 123/218] Fix unit tests --- tests/actions/PolicyTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index e59fec068d65..dfb8ae81d104 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -55,7 +55,7 @@ describe('actions/Policy', () => { // check if policy was created with correct values expect(policy?.id).toBe(policyID); expect(policy?.name).toBe(WORKSPACE_NAME); - expect(policy?.type).toBe(CONST.POLICY.TYPE.FREE); + expect(policy?.type).toBe(CONST.POLICY.TYPE.TEAM); expect(policy?.role).toBe(CONST.POLICY.ROLE.ADMIN); expect(policy?.owner).toBe(ESH_EMAIL); expect(policy?.isPolicyExpenseChatEnabled).toBe(true); From a832c3e7acfa902b8879db9cc2dea7666b318027 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 20 Mar 2024 17:41:49 +0100 Subject: [PATCH 124/218] Add checking diff in fullscreen navigator --- src/libs/Navigation/linkTo.ts | 5 +---- src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/libs/Navigation/linkTo.ts b/src/libs/Navigation/linkTo.ts index 2a00895f0492..dfd5cc354797 100644 --- a/src/libs/Navigation/linkTo.ts +++ b/src/libs/Navigation/linkTo.ts @@ -120,7 +120,6 @@ export default function linkTo(navigation: NavigationContainerRef // - default central pane on desktop layout // - found fullscreen - // Full screen navigator can have any central pane and bottom tab under. They will be covered anyway. - metainfo.isCentralPaneAndBottomTabMandatory = false; + metainfo.isFullScreenNavigatorMandatory = false; const routes = []; routes.push( From 3e40a53d735f50c9aa3d80c88a74cccd1c425074 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 20 Mar 2024 17:12:59 +0000 Subject: [PATCH 125/218] refactor(typescript): apply pull request feedback --- src/libs/DateUtils.ts | 2 +- tests/unit/DateUtilsTest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index dee66c3993e3..4d4f8d425681 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -266,7 +266,7 @@ function formatToLongDateWithWeekday(datetime: string | Date): string { * * @returns Sunday */ -function formatToDayOfWeek(datetime: string | Date): string { +function formatToDayOfWeek(datetime: Date): string { return format(new Date(datetime), CONST.DATE.WEEKDAY_TIME_FORMAT); } diff --git a/tests/unit/DateUtilsTest.ts b/tests/unit/DateUtilsTest.ts index 8758050e6f62..a7f43ea84045 100644 --- a/tests/unit/DateUtilsTest.ts +++ b/tests/unit/DateUtilsTest.ts @@ -42,7 +42,7 @@ describe('DateUtils', () => { }); it('formatToDayOfWeek should return a weekday', () => { - const weekDay = DateUtils.formatToDayOfWeek(datetime); + const weekDay = DateUtils.formatToDayOfWeek(new Date(datetime)); expect(weekDay).toBe('Monday'); }); it('formatToLocalTime should return a date in a local format', () => { From 213e2729b17f39570887c8bbf8e5ac44cd9c6227 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 21 Mar 2024 11:13:12 +0700 Subject: [PATCH 126/218] fix logic to show explain UI --- .../request/step/IOURequestStepScan/index.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index b62cb243472c..8e6003c21c9f 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -77,6 +77,7 @@ function IOURequestStepScan({ const [isTorchAvailable, setIsTorchAvailable] = useState(false); const cameraRef = useRef(null); const trackRef = useRef(null); + const isQueriedPermissionStateRef = useRef(null); const getScreenshotTimeoutRef = useRef(null); @@ -97,6 +98,7 @@ function IOURequestStepScan({ navigator.mediaDevices .getUserMedia({video: {facingMode: {exact: 'environment'}, zoom: {ideal: 1}}}) .then((stream) => { + setCameraPermissionState('granted'); _.forEach(stream.getTracks(), (track) => track.stop()); // Only Safari 17+ supports zoom constraint if (Browser.isMobileSafari() && stream.getTracks().length > 0) { @@ -149,7 +151,8 @@ function IOURequestStepScan({ }) .catch(() => { setCameraPermissionState('denied'); - }); + }) + .finally(() => (isQueriedPermissionStateRef.current = true)); // We only want to get the camera permission status when the component is mounted // eslint-disable-next-line react-hooks/exhaustive-deps }, [isTabActive]); @@ -319,14 +322,14 @@ function IOURequestStepScan({ const mobileCameraView = () => ( <> - {(cameraPermissionState === 'prompt' || !cameraPermissionState) && ( + {((cameraPermissionState === 'prompt' && !isQueriedPermissionStateRef.current) || (cameraPermissionState === 'granted' && _.isEmpty(videoConstraints))) && ( )} - {cameraPermissionState === 'denied' && ( + {cameraPermissionState !== 'granted' && isQueriedPermissionStateRef.current && ( {translate('receipt.takePhoto')} {translate('receipt.cameraAccess')} +