From d99d92d4a1ba20976af09a51cc504fdd9a21169a Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sat, 20 Jan 2024 01:58:07 +0300 Subject: [PATCH 001/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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/314] 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 88c5fd8e5c29cc7dc1b70d863483abe0fc7db86a Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Tue, 27 Feb 2024 11:53:59 +0100 Subject: [PATCH 046/314] 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 047/314] 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 4e3edb22f06a3c2a43afa5c34744f037ada71479 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 29 Feb 2024 13:41:52 +0100 Subject: [PATCH 048/314] 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 77e3d046b51e8b48a655001432713b1dff794977 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 4 Mar 2024 10:30:46 +0100 Subject: [PATCH 049/314] 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 d64a9971a4c24b46b509015ddcc2401a853dfd7e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 4 Mar 2024 16:41:25 +0700 Subject: [PATCH 050/314] Migrate 'AttachmentPicker' component to TypeScript --- .../{index.native.js => index.native.tsx} | 198 ++++++++---------- .../AttachmentPicker/{index.js => index.tsx} | 34 +-- .../launchCamera.android.ts} | 7 +- .../launchCamera.ios.ts} | 0 .../launchCamera.ts} | 2 +- .../AttachmentPicker/launchCamera/types.ts | 47 +++++ ...{attachmentPickerPropTypes.js => types.ts} | 17 +- 7 files changed, 159 insertions(+), 146 deletions(-) rename src/components/AttachmentPicker/{index.native.js => index.native.tsx} (68%) rename src/components/AttachmentPicker/{index.js => index.tsx} (78%) rename src/components/AttachmentPicker/{launchCamera.android.js => launchCamera/launchCamera.android.ts} (88%) rename src/components/AttachmentPicker/{launchCamera.ios.js => launchCamera/launchCamera.ios.ts} (100%) rename src/components/AttachmentPicker/{launchCamera.js => launchCamera/launchCamera.ts} (66%) create mode 100644 src/components/AttachmentPicker/launchCamera/types.ts rename src/components/AttachmentPicker/{attachmentPickerPropTypes.js => types.ts} (58%) diff --git a/src/components/AttachmentPicker/index.native.js b/src/components/AttachmentPicker/index.native.tsx similarity index 68% rename from src/components/AttachmentPicker/index.native.js rename to src/components/AttachmentPicker/index.native.tsx index 0387ee087127..dcc4296b71ac 100644 --- a/src/components/AttachmentPicker/index.native.js +++ b/src/components/AttachmentPicker/index.native.tsx @@ -1,12 +1,9 @@ -import Str from 'expensify-common/lib/str'; import lodashCompact from 'lodash/compact'; -import PropTypes from 'prop-types'; import React, {useCallback, useMemo, useRef, useState} from 'react'; -import {Alert, Image as RNImage, View} from 'react-native'; +import {Alert, View} from 'react-native'; import RNFetchBlob from 'react-native-blob-util'; import RNDocumentPicker from 'react-native-document-picker'; import {launchImageLibrary} from 'react-native-image-picker'; -import _ from 'underscore'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import Popover from '@components/Popover'; @@ -17,19 +14,32 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import CONST from '@src/CONST'; -import {defaultProps as baseDefaultProps, propTypes as basePropTypes} from './attachmentPickerPropTypes'; -import launchCamera from './launchCamera'; - -const propTypes = { - ...basePropTypes, - +import type {TranslationPaths} from '@src/languages/types'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker' +import type {SupportedPlatforms} from 'react-native-document-picker/lib/typescript/fileTypes'; +import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker'; +import launchCamera from './launchCamera/launchCamera'; +import type BaseAttachmentPickerProps from './types'; + +type AttachmentPickerProps = BaseAttachmentPickerProps & { /** If this value is true, then we exclude Camera option. */ - shouldHideCameraOption: PropTypes.bool, + shouldHideCameraOption?: boolean; }; -const defaultProps = { - ...baseDefaultProps, - shouldHideCameraOption: false, +type Item = { + icon: IconAsset; + textTranslationKey: string; + pickAttachment: () => Promise; +}; + +type FileResult = { + name: string; + type: string; + width: number | undefined; + height: number | undefined; + uri: string; + size: number | null; }; /** @@ -45,10 +55,8 @@ const imagePickerOptions = { /** * Return imagePickerOptions based on the type - * @param {String} type - * @returns {Object} */ -const getImagePickerOptions = (type) => { +const getImagePickerOptions = (type: string): CameraOptions => { // mediaType property is one of the ImagePicker configuration to restrict types' const mediaType = type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE ? 'photo' : 'mixed'; return { @@ -58,40 +66,26 @@ const getImagePickerOptions = (type) => { }; /** - * Return documentPickerOptions based on the type - * @param {String} type - * @returns {Object} + * See https://github.com/rnmods/react-native-document-picker#options for DocumentPicker configuration options */ - -const getDocumentPickerOptions = (type) => { - if (type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE) { - return { - type: [RNDocumentPicker.types.images], - copyTo: 'cachesDirectory', - }; - } - return { - type: [RNDocumentPicker.types.allFiles], - copyTo: 'cachesDirectory', - }; -}; +const documentPickerOptions = { + type: [RNDocumentPicker.types.allFiles], + copyTo: 'cachesDirectory', +} satisfies DocumentPickerOptions; /** * The data returned from `show` is different on web and mobile, so use this function to ensure the data we * send to the xhr will be handled properly. - * - * @param {Object} fileData - * @return {Promise} */ -const getDataForUpload = (fileData) => { - const fileName = fileData.fileName || fileData.name || 'chat_attachment'; - const fileResult = { +const getDataForUpload = (fileData: Asset & DocumentPickerResponse): Promise => { + const fileName = fileData.fileName ?? fileData.name ?? 'chat_attachment'; + const fileResult: FileResult = { name: FileUtils.cleanFileName(fileName), type: fileData.type, width: fileData.width, height: fileData.height, - uri: fileData.fileCopyUri || fileData.uri, - size: fileData.fileSize || fileData.size, + uri: fileData.fileCopyUri ?? fileData.uri, + size: fileData.fileSize ?? fileData.size, }; if (fileResult.size) { @@ -109,16 +103,15 @@ const getDataForUpload = (fileData) => { * returns a "show attachment picker" method that takes * a callback. This is the ios/android implementation * opening a modal with attachment options - * @param {propTypes} props - * @returns {JSX.Element} */ -function AttachmentPicker({type, children, shouldHideCameraOption}) { +function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false}: AttachmentPickerProps) { const styles = useThemeStyles(); const [isVisible, setIsVisible] = useState(false); - const completeAttachmentSelection = useRef(); - const onModalHide = useRef(); - const onCanceled = useRef(); + const completeAttachmentSelection = useRef<(data: FileResult) => void>(() => {}); + const onModalHide = useRef<() => void>(() => {}); + const onCanceled = useRef<() => void>(() => {}); + const popoverRef = useRef(null); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -126,20 +119,19 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { /** * A generic handling when we don't know the exact reason for an error */ - const showGeneralAlert = useCallback(() => { - Alert.alert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingAttachment')); + const showGeneralAlert = useCallback((message = '') => { + Alert.alert(translate('attachmentPicker.attachmentError'), `${message !== '' ? message : translate('attachmentPicker.errorWhileSelectingAttachment')}`); }, [translate]); /** * Common image picker handling * * @param {function} imagePickerFunc - RNImagePicker.launchCamera or RNImagePicker.launchImageLibrary - * @returns {Promise} */ const showImagePicker = useCallback( - (imagePickerFunc) => + (imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise): Promise => new Promise((resolve, reject) => { - imagePickerFunc(getImagePickerOptions(type), (response) => { + imagePickerFunc(getImagePickerOptions(type), (response: ImagePickerResponse) => { if (response.didCancel) { // When the user cancelled resolve with no attachment return resolve(); @@ -166,11 +158,11 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { /** * Launch the DocumentPicker. Results are in the same format as ImagePicker * - * @returns {Promise} + * @returns {Promise} */ const showDocumentPicker = useCallback( - () => - RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error) => { + (): Promise => + RNDocumentPicker.pick(documentPickerOptions).catch((error) => { if (RNDocumentPicker.isCancel(error)) { return; } @@ -178,10 +170,10 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { showGeneralAlert(error.message); throw error; }), - [showGeneralAlert, type], + [showGeneralAlert], ); - const menuItemData = useMemo(() => { + const menuItemData: Item[] = useMemo(() => { const data = lodashCompact([ !shouldHideCameraOption && { icon: Expensicons.Camera, @@ -193,7 +185,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { textTranslationKey: 'attachmentPicker.chooseFromGallery', pickAttachment: () => showImagePicker(launchImageLibrary), }, - { + type !== CONST.ATTACHMENT_PICKER_TYPE.IMAGE && { icon: Expensicons.Paperclip, textTranslationKey: 'attachmentPicker.chooseDocument', pickAttachment: showDocumentPicker, @@ -201,7 +193,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { ]); return data; - }, [showDocumentPicker, showImagePicker, shouldHideCameraOption]); + }, [showDocumentPicker, showImagePicker, type, shouldHideCameraOption]); const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: menuItemData.length - 1, isActive: isVisible}); @@ -215,10 +207,10 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { /** * Opens the attachment modal * - * @param {function} onPickedHandler A callback that will be called with the selected attachment - * @param {function} onCanceledHandler A callback that will be called without a selected attachment + * @param onPickedHandler A callback that will be called with the selected attachment + * @param onCanceledHandler A callback that will be called without a selected attachment */ - const open = (onPickedHandler, onCanceledHandler = () => {}) => { + const open = (onPickedHandler: () => void, onCanceledHandler: () => void = () => {}) => { completeAttachmentSelection.current = onPickedHandler; onCanceled.current = onCanceledHandler; setIsVisible(true); @@ -232,15 +224,23 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { }; /** - * @param {Object} fileData - * @returns {Promise} + * Handles the image/document picker result and + * sends the selected attachment to the caller (parent component) */ - const validateAndCompleteAttachmentSelection = useCallback( - (fileData) => { + const pickAttachment = useCallback( + (attachments: Array = []): Promise => { + if (attachments.length === 0) { + onCanceled.current(); + return Promise.resolve(); + } + + const fileData = attachments[0]; + if (fileData.width === -1 || fileData.height === -1) { showImageCorruptionAlert(); return Promise.resolve(); } + return getDataForUpload(fileData) .then((result) => { completeAttachmentSelection.current(result); @@ -253,33 +253,6 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { [showGeneralAlert, showImageCorruptionAlert], ); - /** - * Handles the image/document picker result and - * sends the selected attachment to the caller (parent component) - * - * @param {Array} attachments - * @returns {Promise} - */ - const pickAttachment = useCallback( - (attachments = []) => { - if (attachments.length === 0) { - onCanceled.current(); - return Promise.resolve(); - } - const fileData = _.first(attachments); - if (Str.isImage(fileData.fileName || fileData.name)) { - RNImage.getSize(fileData.fileCopyUri || fileData.uri, (width, height) => { - fileData.width = width; - fileData.height = height; - return validateAndCompleteAttachmentSelection(fileData); - }); - } else { - return validateAndCompleteAttachmentSelection(fileData); - } - }, - [validateAndCompleteAttachmentSelection], - ); - /** * Setup native attachment selection to start after this popover closes * @@ -287,24 +260,24 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { * @param {Function} item.pickAttachment */ const selectItem = useCallback( - (item) => { + (item: Item) => { /* setTimeout delays execution to the frame after the modal closes * without this on iOS closing the modal closes the gallery/camera as well */ - onModalHide.current = () => - setTimeout( - () => - item - .pickAttachment() - .then(pickAttachment) - .catch(console.error) - .finally(() => delete onModalHide.current), - 200, - ); - + onModalHide.current = () => { + setTimeout(() => { + item + .pickAttachment() + .then(pickAttachment) + .catch(console.error) + .finally(() => delete onModalHide.current !== undefined); + }, 200); + }; + close(); }, [pickAttachment], ); + useKeyboardShortcut( CONST.KEYBOARD_SHORTCUTS.ENTER, @@ -322,10 +295,8 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { /** * Call the `children` renderProp with the interface defined in propTypes - * - * @returns {React.ReactNode} */ - const renderChildren = () => + const renderChildren = (): React.ReactNode => children({ openPicker: ({onPicked, onCanceled: newOnCanceled}) => open(onPicked, newOnCanceled), }); @@ -338,15 +309,16 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { onCanceled.current(); }} isVisible={isVisible} - anchorPosition={styles.createMenuPosition} + anchorRef={popoverRef} + // anchorPosition={styles.createMenuPosition} onModalHide={onModalHide.current} > - {_.map(menuItemData, (item, menuIndex) => ( + {menuItemData.map((item, menuIndex) => ( selectItem(item)} focused={focusedIndex === menuIndex} /> @@ -358,8 +330,6 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { ); } -AttachmentPicker.propTypes = propTypes; -AttachmentPicker.defaultProps = defaultProps; AttachmentPicker.displayName = 'AttachmentPicker'; -export default AttachmentPicker; +export default AttachmentPicker; \ No newline at end of file diff --git a/src/components/AttachmentPicker/index.js b/src/components/AttachmentPicker/index.tsx similarity index 78% rename from src/components/AttachmentPicker/index.js rename to src/components/AttachmentPicker/index.tsx index 24024eae6515..9a372a9d4a48 100644 --- a/src/components/AttachmentPicker/index.js +++ b/src/components/AttachmentPicker/index.tsx @@ -1,14 +1,12 @@ import React, {useRef} from 'react'; import Visibility from '@libs/Visibility'; import CONST from '@src/CONST'; -import {defaultProps, propTypes} from './attachmentPickerPropTypes'; +import type AttachmentPickerProps from './types'; /** * Returns acceptable FileTypes based on ATTACHMENT_PICKER_TYPE - * @param {String} type - * @returns {String|undefined} Picker will accept all file types when its undefined */ -function getAcceptableFileTypes(type) { +function getAcceptableFileTypes(type: string): string | undefined { if (type !== CONST.ATTACHMENT_PICKER_TYPE.IMAGE) { return; } @@ -22,13 +20,11 @@ function getAcceptableFileTypes(type) { * a callback. This is the web/mWeb/desktop version since * on a Browser we must append a hidden input to the DOM * and listen to onChange event. - * @param {propTypes} props - * @returns {JSX.Element} */ -function AttachmentPicker(props) { - const fileInput = useRef(); - const onPicked = useRef(); - const onCanceled = useRef(() => {}); +function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE}: AttachmentPickerProps): React.JSX.Element { + const fileInput = useRef(null); + const onPicked = useRef<(file: File) => void>(() => {}); + const onCanceled = useRef<() => void>(() => {}); return ( <> @@ -37,6 +33,10 @@ function AttachmentPicker(props) { type="file" ref={fileInput} onChange={(e) => { + if (!e.target.files) { + return; + } + const file = e.target.files[0]; if (file) { @@ -45,7 +45,9 @@ function AttachmentPicker(props) { } // Cleanup after selecting a file to start from a fresh state - fileInput.current.value = null; + if (fileInput.current) { + fileInput.current.value = ''; + } }} // We are stopping the event propagation because triggering the `click()` on the hidden input // causes the event to unexpectedly bubble up to anything wrapping this component e.g. Pressable @@ -72,12 +74,12 @@ function AttachmentPicker(props) { {once: true}, ); }} - accept={getAcceptableFileTypes(props.type)} + accept={getAcceptableFileTypes(type)} /> - {props.children({ + {children({ openPicker: ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => { onPicked.current = newOnPicked; - fileInput.current.click(); + fileInput.current?.click(); onCanceled.current = newOnCanceled; }, })} @@ -85,6 +87,4 @@ function AttachmentPicker(props) { ); } -AttachmentPicker.propTypes = propTypes; -AttachmentPicker.defaultProps = defaultProps; -export default AttachmentPicker; +export default AttachmentPicker; \ No newline at end of file diff --git a/src/components/AttachmentPicker/launchCamera.android.js b/src/components/AttachmentPicker/launchCamera/launchCamera.android.ts similarity index 88% rename from src/components/AttachmentPicker/launchCamera.android.js rename to src/components/AttachmentPicker/launchCamera/launchCamera.android.ts index b431c55e756d..cac42a874495 100644 --- a/src/components/AttachmentPicker/launchCamera.android.js +++ b/src/components/AttachmentPicker/launchCamera/launchCamera.android.ts @@ -1,15 +1,14 @@ import {PermissionsAndroid} from 'react-native'; import {launchCamera} from 'react-native-image-picker'; +import type {Callback, CameraOptions} from './types'; /** * Launching the camera for Android involves checking for permissions * And only then starting the camera * If the user deny permission the callback will be called with an error response * in the same format as the error returned by react-native-image-picker - * @param {CameraOptions} options - * @param {function} callback - callback called with the result */ -export default function launchCameraAndroid(options, callback) { +export default function launchCameraAndroid(options: CameraOptions, callback: Callback) { // Checks current camera permissions and prompts the user in case they aren't granted PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA) .then((permission) => { @@ -29,4 +28,4 @@ export default function launchCameraAndroid(options, callback) { errorCode: error.errorCode || 'others', }); }); -} +} \ No newline at end of file diff --git a/src/components/AttachmentPicker/launchCamera.ios.js b/src/components/AttachmentPicker/launchCamera/launchCamera.ios.ts similarity index 100% rename from src/components/AttachmentPicker/launchCamera.ios.js rename to src/components/AttachmentPicker/launchCamera/launchCamera.ios.ts diff --git a/src/components/AttachmentPicker/launchCamera.js b/src/components/AttachmentPicker/launchCamera/launchCamera.ts similarity index 66% rename from src/components/AttachmentPicker/launchCamera.js rename to src/components/AttachmentPicker/launchCamera/launchCamera.ts index dc1f921086de..d272e629f98f 100644 --- a/src/components/AttachmentPicker/launchCamera.js +++ b/src/components/AttachmentPicker/launchCamera/launchCamera.ts @@ -1,3 +1,3 @@ import {launchCamera} from 'react-native-image-picker'; -export default launchCamera; +export default launchCamera; \ No newline at end of file diff --git a/src/components/AttachmentPicker/launchCamera/types.ts b/src/components/AttachmentPicker/launchCamera/types.ts new file mode 100644 index 000000000000..c7f0481c7773 --- /dev/null +++ b/src/components/AttachmentPicker/launchCamera/types.ts @@ -0,0 +1,47 @@ +type Callback = (response: ImagePickerResponse) => void; +type OptionsCommon = { + mediaType: MediaType; + maxWidth?: number; + maxHeight?: number; + quality?: PhotoQuality; + videoQuality?: AndroidVideoOptions | IOSVideoOptions; + includeBase64?: boolean; + includeExtra?: boolean; + presentationStyle?: 'currentContext' | 'fullScreen' | 'pageSheet' | 'formSheet' | 'popover' | 'overFullScreen' | 'overCurrentContext'; +}; +type ImageLibraryOptions = OptionsCommon & { + selectionLimit?: number; +}; +type CameraOptions = OptionsCommon & { + durationLimit?: number; + saveToPhotos?: boolean; + cameraType?: CameraType; +}; +type Asset = { + base64?: string; + uri?: string; + width?: number; + height?: number; + fileSize?: number; + type?: string; + fileName?: string; + duration?: number; + bitrate?: number; + timestamp?: string; + id?: string; +}; +type ImagePickerResponse = { + didCancel?: boolean; + errorCode?: ErrorCode; + errorMessage?: string; + assets?: Asset[]; +}; +type PhotoQuality = 0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1; +type CameraType = 'back' | 'front'; +type MediaType = 'photo' | 'video' | 'mixed'; +type AndroidVideoOptions = 'low' | 'high'; +type IOSVideoOptions = 'low' | 'medium' | 'high'; +type ErrorCode = 'camera_unavailable' | 'permission' | 'others'; +type ErrorLaunchCamera = Error & ErrorCode + +export type {CameraOptions, Callback, ErrorCode, ImagePickerResponse, Asset, ImageLibraryOptions}; \ No newline at end of file diff --git a/src/components/AttachmentPicker/attachmentPickerPropTypes.js b/src/components/AttachmentPicker/types.ts similarity index 58% rename from src/components/AttachmentPicker/attachmentPickerPropTypes.js rename to src/components/AttachmentPicker/types.ts index a3a346f5ea27..19b98d85f691 100644 --- a/src/components/AttachmentPicker/attachmentPickerPropTypes.js +++ b/src/components/AttachmentPicker/types.ts @@ -1,7 +1,8 @@ -import PropTypes from 'prop-types'; -import CONST from '@src/CONST'; +import type {ReactNode} from 'react'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; -const propTypes = { +type AttachmentPickerProps = { /** * A renderProp with the following interface * @@ -20,14 +21,10 @@ const propTypes = { * )} * * */ - children: PropTypes.func.isRequired, + children: (openPicker: ({onPicked, onCanceled}: {onPicked: (file: File) => void; onCanceled?: () => void}) => void) => ReactNode; /** The types of files that can be selected with this picker. */ - type: PropTypes.oneOf([CONST.ATTACHMENT_PICKER_TYPE.FILE, CONST.ATTACHMENT_PICKER_TYPE.IMAGE]), + type?: ValueOf; }; -const defaultProps = { - type: CONST.ATTACHMENT_PICKER_TYPE.FILE, -}; - -export {propTypes, defaultProps}; +export default AttachmentPickerProps; \ No newline at end of file From 049f95069ee50330c678076d1b4deadcc9aa389c Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 4 Mar 2024 17:08:58 +0700 Subject: [PATCH 051/314] fix: type in launch camera --- .../AttachmentPicker/index.native.tsx | 23 ++++++++++--------- src/components/AttachmentPicker/index.tsx | 2 +- .../launchCamera/launchCamera.android.ts | 7 +++--- .../launchCamera/launchCamera.ios.ts | 11 ++++----- .../launchCamera/launchCamera.ts | 2 +- .../AttachmentPicker/launchCamera/types.ts | 11 +++++++-- src/components/AttachmentPicker/types.ts | 2 +- 7 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index dcc4296b71ac..3aaaac1083a0 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -3,7 +3,10 @@ import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Alert, View} from 'react-native'; import RNFetchBlob from 'react-native-blob-util'; import RNDocumentPicker from 'react-native-document-picker'; +import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker'; +import type {SupportedPlatforms} from 'react-native-document-picker/lib/typescript/fileTypes'; import {launchImageLibrary} from 'react-native-image-picker'; +import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import Popover from '@components/Popover'; @@ -16,9 +19,6 @@ import * as FileUtils from '@libs/fileDownload/FileUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker' -import type {SupportedPlatforms} from 'react-native-document-picker/lib/typescript/fileTypes'; -import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker'; import launchCamera from './launchCamera/launchCamera'; import type BaseAttachmentPickerProps from './types'; @@ -119,9 +119,12 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s /** * A generic handling when we don't know the exact reason for an error */ - const showGeneralAlert = useCallback((message = '') => { - Alert.alert(translate('attachmentPicker.attachmentError'), `${message !== '' ? message : translate('attachmentPicker.errorWhileSelectingAttachment')}`); - }, [translate]); + const showGeneralAlert = useCallback( + (message = '') => { + Alert.alert(translate('attachmentPicker.attachmentError'), `${message !== '' ? message : translate('attachmentPicker.errorWhileSelectingAttachment')}`); + }, + [translate], + ); /** * Common image picker handling @@ -265,19 +268,17 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s * without this on iOS closing the modal closes the gallery/camera as well */ onModalHide.current = () => { setTimeout(() => { - item - .pickAttachment() + item.pickAttachment() .then(pickAttachment) .catch(console.error) .finally(() => delete onModalHide.current !== undefined); }, 200); }; - + close(); }, [pickAttachment], ); - useKeyboardShortcut( CONST.KEYBOARD_SHORTCUTS.ENTER, @@ -332,4 +333,4 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s AttachmentPicker.displayName = 'AttachmentPicker'; -export default AttachmentPicker; \ No newline at end of file +export default AttachmentPicker; diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx index 9a372a9d4a48..e8a23e29f114 100644 --- a/src/components/AttachmentPicker/index.tsx +++ b/src/components/AttachmentPicker/index.tsx @@ -87,4 +87,4 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE}: ); } -export default AttachmentPicker; \ No newline at end of file +export default AttachmentPicker; diff --git a/src/components/AttachmentPicker/launchCamera/launchCamera.android.ts b/src/components/AttachmentPicker/launchCamera/launchCamera.android.ts index cac42a874495..135b5dfd80e6 100644 --- a/src/components/AttachmentPicker/launchCamera/launchCamera.android.ts +++ b/src/components/AttachmentPicker/launchCamera/launchCamera.android.ts @@ -1,6 +1,7 @@ import {PermissionsAndroid} from 'react-native'; import {launchCamera} from 'react-native-image-picker'; import type {Callback, CameraOptions} from './types'; +import {ErrorLaunchCamera} from './types'; /** * Launching the camera for Android involves checking for permissions @@ -13,9 +14,7 @@ export default function launchCameraAndroid(options: CameraOptions, callback: Ca PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA) .then((permission) => { if (permission !== PermissionsAndroid.RESULTS.GRANTED) { - const error = new Error('User did not grant permissions'); - error.errorCode = 'permission'; - throw error; + throw new ErrorLaunchCamera('User did not grant permissions', 'permission'); } launchCamera(options, callback); @@ -28,4 +27,4 @@ export default function launchCameraAndroid(options: CameraOptions, callback: Ca errorCode: error.errorCode || 'others', }); }); -} \ No newline at end of file +} diff --git a/src/components/AttachmentPicker/launchCamera/launchCamera.ios.ts b/src/components/AttachmentPicker/launchCamera/launchCamera.ios.ts index d6e3518d7188..cffb00f39e4a 100644 --- a/src/components/AttachmentPicker/launchCamera/launchCamera.ios.ts +++ b/src/components/AttachmentPicker/launchCamera/launchCamera.ios.ts @@ -1,24 +1,21 @@ import {launchCamera} from 'react-native-image-picker'; import {PERMISSIONS, request, RESULTS} from 'react-native-permissions'; +import type {Callback, CameraOptions} from './types'; +import {ErrorLaunchCamera} from './types'; /** * Launching the camera for iOS involves checking for permissions * And only then starting the camera * If the user deny permission the callback will be called with an error response * in the same format as the error returned by react-native-image-picker - * @param {CameraOptions} options - * @param {function} callback - callback called with the result */ -export default function launchCameraIOS(options, callback) { +export default function launchCameraIOS(options: CameraOptions, callback: Callback) { // Checks current camera permissions and prompts the user in case they aren't granted request(PERMISSIONS.IOS.CAMERA) .then((permission) => { if (permission !== RESULTS.GRANTED) { - const error = new Error('User did not grant permissions'); - error.errorCode = 'permission'; - throw error; + throw new ErrorLaunchCamera('User did not grant permissions', 'permission'); } - launchCamera(options, callback); }) .catch((error) => { diff --git a/src/components/AttachmentPicker/launchCamera/launchCamera.ts b/src/components/AttachmentPicker/launchCamera/launchCamera.ts index d272e629f98f..dc1f921086de 100644 --- a/src/components/AttachmentPicker/launchCamera/launchCamera.ts +++ b/src/components/AttachmentPicker/launchCamera/launchCamera.ts @@ -1,3 +1,3 @@ import {launchCamera} from 'react-native-image-picker'; -export default launchCamera; \ No newline at end of file +export default launchCamera; diff --git a/src/components/AttachmentPicker/launchCamera/types.ts b/src/components/AttachmentPicker/launchCamera/types.ts index c7f0481c7773..1a3aae3f0ad7 100644 --- a/src/components/AttachmentPicker/launchCamera/types.ts +++ b/src/components/AttachmentPicker/launchCamera/types.ts @@ -42,6 +42,13 @@ type MediaType = 'photo' | 'video' | 'mixed'; type AndroidVideoOptions = 'low' | 'high'; type IOSVideoOptions = 'low' | 'medium' | 'high'; type ErrorCode = 'camera_unavailable' | 'permission' | 'others'; -type ErrorLaunchCamera = Error & ErrorCode +class ErrorLaunchCamera extends Error { + errorCode: ErrorCode; -export type {CameraOptions, Callback, ErrorCode, ImagePickerResponse, Asset, ImageLibraryOptions}; \ No newline at end of file + constructor(message: string, errorCode: ErrorCode) { + super(message); + this.errorCode = errorCode; + } +} +export {ErrorLaunchCamera}; +export type {CameraOptions, Callback, ErrorCode, ImagePickerResponse, Asset, ImageLibraryOptions}; diff --git a/src/components/AttachmentPicker/types.ts b/src/components/AttachmentPicker/types.ts index 19b98d85f691..b9974f4082b2 100644 --- a/src/components/AttachmentPicker/types.ts +++ b/src/components/AttachmentPicker/types.ts @@ -27,4 +27,4 @@ type AttachmentPickerProps = { type?: ValueOf; }; -export default AttachmentPickerProps; \ No newline at end of file +export default AttachmentPickerProps; 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 052/314] 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 053/314] 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 d34c62bd1aa9ab68b95f58bf37d8268111d0b72a Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 5 Mar 2024 10:21:51 +0700 Subject: [PATCH 054/314] fix type select item function --- src/components/AttachmentPicker/index.native.tsx | 9 ++++----- src/components/AttachmentPicker/index.tsx | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 3aaaac1083a0..e144a210fa7c 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -109,7 +109,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s const [isVisible, setIsVisible] = useState(false); const completeAttachmentSelection = useRef<(data: FileResult) => void>(() => {}); - const onModalHide = useRef<() => void>(() => {}); + const onModalHide = useRef<() => void>(); const onCanceled = useRef<() => void>(() => {}); const popoverRef = useRef(null); @@ -266,15 +266,14 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s (item: Item) => { /* setTimeout delays execution to the frame after the modal closes * without this on iOS closing the modal closes the gallery/camera as well */ - onModalHide.current = () => { + onModalHide.current = () => { setTimeout(() => { item.pickAttachment() - .then(pickAttachment) + .then((result) => pickAttachment(result as Array)) .catch(console.error) - .finally(() => delete onModalHide.current !== undefined); + .finally(() => delete onModalHide.current); }, 200); }; - close(); }, [pickAttachment], diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx index e8a23e29f114..9a372a9d4a48 100644 --- a/src/components/AttachmentPicker/index.tsx +++ b/src/components/AttachmentPicker/index.tsx @@ -87,4 +87,4 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE}: ); } -export default AttachmentPicker; +export default AttachmentPicker; \ No newline at end of file From e3c79db7b98918f70b23796604aa25afcb02af1d Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Tue, 5 Mar 2024 16:23:56 +0000 Subject: [PATCH 055/314] [TS migration] Migrate Storybook files --- .storybook/{main.js => main.ts} | 21 ++++++- .storybook/{manager.js => manager.ts} | 0 .storybook/{preview.js => preview.tsx} | 28 +++++---- .storybook/{theme.js => theme.ts} | 5 +- .storybook/webpack.config.js | 54 ------------------ .storybook/webpack.config.ts | 79 ++++++++++++++++++++++++++ 6 files changed, 120 insertions(+), 67 deletions(-) rename .storybook/{main.js => main.ts} (59%) rename .storybook/{manager.js => manager.ts} (100%) rename .storybook/{preview.js => preview.tsx} (51%) rename .storybook/{theme.js => theme.ts} (86%) delete mode 100644 .storybook/webpack.config.js create mode 100644 .storybook/webpack.config.ts diff --git a/.storybook/main.js b/.storybook/main.ts similarity index 59% rename from .storybook/main.js rename to .storybook/main.ts index 7d063fd6ffe1..0234f18ff488 100644 --- a/.storybook/main.js +++ b/.storybook/main.ts @@ -1,12 +1,29 @@ -module.exports = { +type Dir = { + from: string; + to: string; +}; + +type Main = { + stories: string[]; + addons: string[]; + staticDirs: Array; + core: { + builder: string; + }; + managerHead: (head: string) => string; +}; + +const main: Main = { stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: ['@storybook/addon-essentials', '@storybook/addon-a11y', '@storybook/addon-react-native-web'], staticDirs: ['./public', {from: '../assets/css', to: 'css'}, {from: '../assets/fonts/web', to: 'fonts'}], core: { builder: 'webpack5', }, - managerHead: (head) => ` + managerHead: (head: string) => ` ${head} ${process.env.ENV === 'staging' ? '' : ''} `, }; + +export default main; diff --git a/.storybook/manager.js b/.storybook/manager.ts similarity index 100% rename from .storybook/manager.js rename to .storybook/manager.ts diff --git a/.storybook/preview.js b/.storybook/preview.tsx similarity index 51% rename from .storybook/preview.js rename to .storybook/preview.tsx index a89c720976c9..5ddb9d04d8ae 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.tsx @@ -2,16 +2,24 @@ import {PortalProvider} from '@gorhom/portal'; import React from 'react'; import Onyx from 'react-native-onyx'; import {SafeAreaProvider} from 'react-native-safe-area-context'; -import ComposeProviders from '../src/components/ComposeProviders'; -import HTMLEngineProvider from '../src/components/HTMLEngineProvider'; -import {LocaleContextProvider} from '../src/components/LocaleContextProvider'; -import OnyxProvider from '../src/components/OnyxProvider'; -import {EnvironmentProvider} from '../src/components/withEnvironment'; -import {KeyboardStateProvider} from '../src/components/withKeyboardState'; -import {WindowDimensionsProvider} from '../src/components/withWindowDimensions'; -import ONYXKEYS from '../src/ONYXKEYS'; +import ComposeProviders from '@src/components/ComposeProviders'; +import HTMLEngineProvider from '@src/components/HTMLEngineProvider'; +import {LocaleContextProvider} from '@src/components/LocaleContextProvider'; +import OnyxProvider from '@src/components/OnyxProvider'; +import {EnvironmentProvider} from '@src/components/withEnvironment'; +import {KeyboardStateProvider} from '@src/components/withKeyboardState'; +import {WindowDimensionsProvider} from '@src/components/withWindowDimensions'; +import ONYXKEYS from '@src/ONYXKEYS'; import './fonts.css'; +type Parameter = { + controls: { + matchers: { + color: RegExp; + }; + }; +}; + Onyx.init({ keys: ONYXKEYS, initialKeyStates: { @@ -20,7 +28,7 @@ Onyx.init({ }); const decorators = [ - (Story) => ( + (Story: React.ElementType) => ( @@ -29,7 +37,7 @@ const decorators = [ ), ]; -const parameters = { +const parameters: Parameter = { controls: { matchers: { color: /(background|color)$/i, diff --git a/.storybook/theme.js b/.storybook/theme.ts similarity index 86% rename from .storybook/theme.js rename to .storybook/theme.ts index 08d8b584d580..f1ace5dd37c0 100644 --- a/.storybook/theme.js +++ b/.storybook/theme.ts @@ -1,7 +1,8 @@ +import type {ThemeVars} from '@storybook/theming'; import {create} from '@storybook/theming'; import colors from '../src/styles/theme/colors'; -export default create({ +const theme: ThemeVars = create({ brandTitle: 'New Expensify UI Docs', brandImage: 'logomark.svg', fontBase: 'ExpensifyNeue-Regular', @@ -21,3 +22,5 @@ export default create({ appBorderRadius: 8, inputBorderRadius: 8, }); + +export default theme; diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js deleted file mode 100644 index 204f70344b18..000000000000 --- a/.storybook/webpack.config.js +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -/* eslint-disable no-param-reassign */ -const path = require('path'); -const dotenv = require('dotenv'); -const _ = require('underscore'); - -let envFile; -switch (process.env.ENV) { - case 'production': - envFile = '.env.production'; - break; - case 'staging': - envFile = '.env.staging'; - break; - default: - envFile = '.env'; -} - -const env = dotenv.config({path: path.resolve(__dirname, `../${envFile}`)}); -const custom = require('../config/webpack/webpack.common')({ - envFile, -}); - -module.exports = ({config}) => { - config.resolve.alias = { - 'react-native-config': 'react-web-config', - 'react-native$': 'react-native-web', - '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'), - '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'), - - // Module alias support for storybook files, coping from `webpack.common.js` - ...custom.resolve.alias, - }; - - // Necessary to overwrite the values in the existing DefinePlugin hardcoded to the Config staging values - const definePluginIndex = _.findIndex(config.plugins, (plugin) => plugin.constructor.name === 'DefinePlugin'); - config.plugins[definePluginIndex].definitions.__REACT_WEB_CONFIG__ = JSON.stringify(env); - config.resolve.extensions = custom.resolve.extensions; - - const babelRulesIndex = _.findIndex(custom.module.rules, (rule) => rule.loader === 'babel-loader'); - const babelRule = custom.module.rules[babelRulesIndex]; - config.module.rules.push(babelRule); - - // Allows loading SVG - more context here https://github.com/storybookjs/storybook/issues/6188 - const fileLoaderRule = _.find(config.module.rules, (rule) => rule.test && rule.test.test('.svg')); - fileLoaderRule.exclude = /\.svg$/; - config.module.rules.push({ - test: /\.svg$/, - enforce: 'pre', - loader: require.resolve('@svgr/webpack'), - }); - - return config; -}; diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts new file mode 100644 index 000000000000..0c66e184c18f --- /dev/null +++ b/.storybook/webpack.config.ts @@ -0,0 +1,79 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +/* eslint-disable no-underscore-dangle */ + +/* eslint-disable no-param-reassign */ +import dotenv from 'dotenv'; +import path from 'path'; +import {DefinePlugin} from 'webpack'; +import type {Configuration, RuleSetRule} from 'webpack'; + +type CustomWebpackConfig = { + resolve: { + alias: Record; + extensions: string[]; + }; + module: { + rules: RuleSetRule[]; + }; +}; + +let envFile; +switch (process.env.ENV) { + case 'production': + envFile = '.env.production'; + break; + case 'staging': + envFile = '.env.staging'; + break; + default: + envFile = '.env'; +} + +const env = dotenv.config({path: path.resolve(__dirname, `../${envFile}`)}); +const custom: CustomWebpackConfig = require('../config/webpack/webpack.common')({ + envFile, +}); + +module.exports = ({config}: {config: Configuration}) => { + if (config.resolve && config.plugins && config.module) { + config.resolve.alias = { + 'react-native-config': 'react-web-config', + 'react-native$': 'react-native-web', + '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'), + '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'), + ...custom.resolve.alias, + }; + + // Necessary to overwrite the values in the existing DefinePlugin hardcoded to the Config staging values + const definePluginIndex = config.plugins.findIndex((plugin) => plugin instanceof DefinePlugin); + if (definePluginIndex !== -1 && config.plugins[definePluginIndex] instanceof DefinePlugin) { + const definePlugin = config.plugins[definePluginIndex] as DefinePlugin; + if (definePlugin.definitions) { + definePlugin.definitions.__REACT_WEB_CONFIG__ = JSON.stringify(env); + } + } + config.resolve.extensions = custom.resolve.extensions; + + const babelRulesIndex = custom.module.rules.findIndex((rule) => rule.loader === 'babel-loader'); + const babelRule = custom.module.rules[babelRulesIndex]; + if (babelRule) { + config.module.rules?.push(babelRule); + } + + const fileLoaderRule = config.module.rules?.find( + (rule): rule is RuleSetRule => + typeof rule !== 'boolean' && typeof rule !== 'string' && typeof rule !== 'number' && !!rule?.test && rule.test instanceof RegExp && rule.test.test('.svg'), + ); + if (fileLoaderRule?.exclude) { + fileLoaderRule.exclude = /\.svg$/; + } + config.module.rules?.push({ + test: /\.svg$/, + enforce: 'pre', + loader: require.resolve('@svgr/webpack'), + }); + } + + return config; +}; From 6aa6a28e1e7c446442aea364ca06caf24bd217d4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Mar 2024 17:30:44 +0100 Subject: [PATCH 056/314] 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 cd2681e34826070baa28bafde8a7fd0eda10007e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 6 Mar 2024 10:13:31 +0700 Subject: [PATCH 057/314] fix type --- src/components/AttachmentPicker/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx index 9a372a9d4a48..dc5ce45153a3 100644 --- a/src/components/AttachmentPicker/index.tsx +++ b/src/components/AttachmentPicker/index.tsx @@ -76,13 +76,13 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE}: }} accept={getAcceptableFileTypes(type)} /> - {children({ - openPicker: ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => { + {children( + ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => { onPicked.current = newOnPicked; fileInput.current?.click(); onCanceled.current = newOnCanceled; }, - })} + )} ); } From 2dda80fcfc839d3823f07882bde897b88cf8c3c2 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 6 Mar 2024 14:32:22 +0700 Subject: [PATCH 058/314] fix: type attachment --- src/components/AttachmentModal.tsx | 10 +++++----- .../AttachmentPicker/index.native.tsx | 20 ++++++------------- src/components/AttachmentPicker/index.tsx | 8 ++++---- src/components/AttachmentPicker/types.ts | 4 ++-- src/components/AvatarWithImagePicker.tsx | 18 +++++++---------- .../AttachmentPickerWithMenuItems.tsx | 1 - 6 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index eed40d75387e..2a6ebd12f143 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -71,12 +71,12 @@ type Attachment = { }; type ImagePickerResponse = { - height: number; + height?: number; name: string; - size: number; + size?: number | null; type: string; uri: string; - width: number; + width?: number; }; type FileObject = File | ImagePickerResponse; @@ -292,14 +292,14 @@ function AttachmentModal({ }, [transaction, report]); const isValidFile = useCallback((fileObject: FileObject) => { - if (fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { + if (fileObject.size && fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { setIsAttachmentInvalid(true); setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooLarge'); setAttachmentInvalidReason('attachmentPicker.sizeExceeded'); return false; } - if (fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { + if (fileObject.size && fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { setIsAttachmentInvalid(true); setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooSmall'); setAttachmentInvalidReason('attachmentPicker.sizeNotMet'); diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index e144a210fa7c..7412c7382513 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -7,6 +7,7 @@ import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-d import type {SupportedPlatforms} from 'react-native-document-picker/lib/typescript/fileTypes'; import {launchImageLibrary} from 'react-native-image-picker'; import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker'; +import type {FileObject} from '@components/AttachmentModal'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import Popover from '@components/Popover'; @@ -33,15 +34,6 @@ type Item = { pickAttachment: () => Promise; }; -type FileResult = { - name: string; - type: string; - width: number | undefined; - height: number | undefined; - uri: string; - size: number | null; -}; - /** * See https://github.com/react-native-image-picker/react-native-image-picker/#options * for ImagePicker configuration options @@ -77,9 +69,9 @@ const documentPickerOptions = { * The data returned from `show` is different on web and mobile, so use this function to ensure the data we * send to the xhr will be handled properly. */ -const getDataForUpload = (fileData: Asset & DocumentPickerResponse): Promise => { +const getDataForUpload = (fileData: Asset & DocumentPickerResponse): Promise => { const fileName = fileData.fileName ?? fileData.name ?? 'chat_attachment'; - const fileResult: FileResult = { + const fileResult: FileObject = { name: FileUtils.cleanFileName(fileName), type: fileData.type, width: fileData.width, @@ -108,7 +100,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s const styles = useThemeStyles(); const [isVisible, setIsVisible] = useState(false); - const completeAttachmentSelection = useRef<(data: FileResult) => void>(() => {}); + const completeAttachmentSelection = useRef<(data: FileObject) => void>(() => {}); const onModalHide = useRef<() => void>(); const onCanceled = useRef<() => void>(() => {}); const popoverRef = useRef(null); @@ -213,7 +205,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s * @param onPickedHandler A callback that will be called with the selected attachment * @param onCanceledHandler A callback that will be called without a selected attachment */ - const open = (onPickedHandler: () => void, onCanceledHandler: () => void = () => {}) => { + const open = (onPickedHandler: (file: FileObject) => void, onCanceledHandler: () => void = () => {}) => { completeAttachmentSelection.current = onPickedHandler; onCanceled.current = onCanceledHandler; setIsVisible(true); @@ -266,7 +258,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s (item: Item) => { /* setTimeout delays execution to the frame after the modal closes * without this on iOS closing the modal closes the gallery/camera as well */ - onModalHide.current = () => { + onModalHide.current = () => { setTimeout(() => { item.pickAttachment() .then((result) => pickAttachment(result as Array)) diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx index dc5ce45153a3..e8a23e29f114 100644 --- a/src/components/AttachmentPicker/index.tsx +++ b/src/components/AttachmentPicker/index.tsx @@ -76,15 +76,15 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE}: }} accept={getAcceptableFileTypes(type)} /> - {children( - ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => { + {children({ + openPicker: ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => { onPicked.current = newOnPicked; fileInput.current?.click(); onCanceled.current = newOnCanceled; }, - )} + })} ); } -export default AttachmentPicker; \ No newline at end of file +export default AttachmentPicker; diff --git a/src/components/AttachmentPicker/types.ts b/src/components/AttachmentPicker/types.ts index b9974f4082b2..66ee91b33c24 100644 --- a/src/components/AttachmentPicker/types.ts +++ b/src/components/AttachmentPicker/types.ts @@ -1,5 +1,6 @@ import type {ReactNode} from 'react'; import type {ValueOf} from 'type-fest'; +import type {FileObject} from '@components/AttachmentModal'; import type CONST from '@src/CONST'; type AttachmentPickerProps = { @@ -21,8 +22,7 @@ type AttachmentPickerProps = { * )} * * */ - children: (openPicker: ({onPicked, onCanceled}: {onPicked: (file: File) => void; onCanceled?: () => void}) => void) => ReactNode; - + children: (props: {openPicker: (options: {onPicked: (file: FileObject) => void; onCanceled?: () => void}) => void}) => ReactNode; /** The types of files that can be selected with this picker. */ type?: ValueOf; }; diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 5755c69641c8..6c8b4e65ae93 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -15,7 +15,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type IconAsset from '@src/types/utils/IconAsset'; -import AttachmentModal from './AttachmentModal'; +import AttachmentModal, {FileObject} from './AttachmentModal'; import AttachmentPicker from './AttachmentPicker'; import Avatar from './Avatar'; import AvatarCropModal from './AvatarCropModal/AvatarCropModal'; @@ -34,7 +34,7 @@ type ErrorData = { }; type OpenPickerParams = { - onPicked: (image: File) => void; + onPicked: (image: FileObject) => void; }; type OpenPicker = (args: OpenPickerParams) => void; @@ -174,7 +174,7 @@ function AvatarWithImagePicker({ /** * Check if the attachment extension is allowed. */ - const isValidExtension = (image: File): boolean => { + const isValidExtension = (image: FileObject): boolean => { const {fileExtension} = FileUtils.splitExtensionFromFileName(image?.name ?? ''); return CONST.AVATAR_ALLOWED_EXTENSIONS.some((extension) => extension === fileExtension.toLowerCase()); }; @@ -182,12 +182,12 @@ function AvatarWithImagePicker({ /** * Check if the attachment size is less than allowed size. */ - const isValidSize = (image: File): boolean => (image?.size ?? 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE; + const isValidSize = (image: FileObject): boolean => (image?.size ?? 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE; /** * Check if the attachment resolution matches constraints. */ - const isValidResolution = (image: File): Promise => + const isValidResolution = (image: FileObject): Promise => getImageResolution(image).then( ({height, width}) => height >= CONST.AVATAR_MIN_HEIGHT_PX && width >= CONST.AVATAR_MIN_WIDTH_PX && height <= CONST.AVATAR_MAX_HEIGHT_PX && width <= CONST.AVATAR_MAX_WIDTH_PX, ); @@ -195,7 +195,7 @@ function AvatarWithImagePicker({ /** * Validates if an image has a valid resolution and opens an avatar crop modal */ - const showAvatarCropModal = (image: File) => { + const showAvatarCropModal = (image: FileObject) => { if (!isValidExtension(image)) { setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS}); return; @@ -343,11 +343,7 @@ function AvatarWithImagePicker({ maybeIcon={isUsingDefaultAvatar} > {({show}) => ( - - {/* @ts-expect-error TODO: Remove this once AttachmentPicker (https://github.com/Expensify/App/issues/25134) is migrated to TypeScript. */} + {({openPicker}) => { const menuItems = createMenuItems(openPicker); diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 68c7f0883683..b8b4693ac66e 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -185,7 +185,6 @@ function AttachmentPickerWithMenuItems({ return ( - {/* @ts-expect-error TODO: Remove this once AttachmentPicker (https://github.com/Expensify/App/issues/25134) is migrated to TypeScript. */} {({openPicker}) => { const triggerAttachmentPicker = () => { onTriggerAttachmentPicker(); From 9ac6e89085815d5c3f3064262fc3c37dde8206de Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 6 Mar 2024 14:38:28 +0700 Subject: [PATCH 059/314] fix lint --- src/components/AvatarWithImagePicker.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 6c8b4e65ae93..0e1a8d0e17a6 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -15,7 +15,8 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type IconAsset from '@src/types/utils/IconAsset'; -import AttachmentModal, {FileObject} from './AttachmentModal'; +import AttachmentModal from './AttachmentModal'; +import type {FileObject} from './AttachmentModal'; import AttachmentPicker from './AttachmentPicker'; import Avatar from './Avatar'; import AvatarCropModal from './AvatarCropModal/AvatarCropModal'; From d906791422db9dfcc0a3ca4c086dd78095f648b0 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Wed, 6 Mar 2024 14:55:47 +0000 Subject: [PATCH 060/314] [TS migration][Storybook] Feedback --- .storybook/main.ts | 13 ++----------- .storybook/preview.tsx | 11 ++--------- .storybook/webpack.config.ts | 8 +++++--- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 0234f18ff488..33f4befb0f40 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,17 +1,8 @@ -type Dir = { - from: string; - to: string; -}; +import type {StorybookConfig} from '@storybook/core-common'; type Main = { - stories: string[]; - addons: string[]; - staticDirs: Array; - core: { - builder: string; - }; managerHead: (head: string) => string; -}; +} & StorybookConfig; const main: Main = { stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 5ddb9d04d8ae..4767c7d81343 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,4 +1,5 @@ import {PortalProvider} from '@gorhom/portal'; +import type {Parameters} from '@storybook/addons'; import React from 'react'; import Onyx from 'react-native-onyx'; import {SafeAreaProvider} from 'react-native-safe-area-context'; @@ -12,14 +13,6 @@ import {WindowDimensionsProvider} from '@src/components/withWindowDimensions'; import ONYXKEYS from '@src/ONYXKEYS'; import './fonts.css'; -type Parameter = { - controls: { - matchers: { - color: RegExp; - }; - }; -}; - Onyx.init({ keys: ONYXKEYS, initialKeyStates: { @@ -37,7 +30,7 @@ const decorators = [ ), ]; -const parameters: Parameter = { +const parameters: Parameters = { controls: { matchers: { color: /(background|color)$/i, diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts index 0c66e184c18f..6f7ee023643c 100644 --- a/.storybook/webpack.config.ts +++ b/.storybook/webpack.config.ts @@ -1,8 +1,8 @@ -/* eslint-disable @typescript-eslint/naming-convention */ - /* eslint-disable no-underscore-dangle */ /* eslint-disable no-param-reassign */ + +/* eslint-disable @typescript-eslint/naming-convention */ import dotenv from 'dotenv'; import path from 'path'; import {DefinePlugin} from 'webpack'; @@ -35,7 +35,7 @@ const custom: CustomWebpackConfig = require('../config/webpack/webpack.common')( envFile, }); -module.exports = ({config}: {config: Configuration}) => { +const webpackConfig = ({config}: {config: Configuration}) => { if (config.resolve && config.plugins && config.module) { config.resolve.alias = { 'react-native-config': 'react-web-config', @@ -77,3 +77,5 @@ module.exports = ({config}: {config: Configuration}) => { return config; }; + +export default webpackConfig; From 2f5bf904096e487492bd5e4c6f2b66edd70d1d2e Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Wed, 6 Mar 2024 15:09:03 +0000 Subject: [PATCH 061/314] [TS migration][Storybook] Lint --- .storybook/theme.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/.storybook/theme.ts b/.storybook/theme.ts index f1ace5dd37c0..a28a0f031b0c 100644 --- a/.storybook/theme.ts +++ b/.storybook/theme.ts @@ -1,5 +1,6 @@ import type {ThemeVars} from '@storybook/theming'; import {create} from '@storybook/theming'; +// eslint-disable-next-line @dword-design/import-alias/prefer-alias import colors from '../src/styles/theme/colors'; const theme: ThemeVars = create({ From c90ce7e8d360788fa31be13fe94f31a123689a4f Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 6 Mar 2024 18:23:50 +0300 Subject: [PATCH 062/314] 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 9f38e25e51dcfa089429348fa683bd78f7a4ea88 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 6 Mar 2024 19:07:12 +0300 Subject: [PATCH 063/314] 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 064/314] 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: Wed, 6 Mar 2024 13:12:48 +0100 Subject: [PATCH 065/314] is selectable item property introduced --- src/components/SelectionList/BaseListItem.tsx | 2 +- src/components/SelectionList/BaseSelectionList.tsx | 3 ++- src/components/SelectionList/types.ts | 3 +++ src/pages/workspace/WorkspaceMembersPage.tsx | 7 +++---- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index c032fe2d081b..3e8573888404 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -80,7 +80,7 @@ function BaseListItem({ diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index bcf45fb2e2f4..5646b993d135 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -111,7 +111,8 @@ function BaseSelectionList( }); // If disabled, add to the disabled indexes array - if (!!section.isDisabled || item.isDisabled) { + // eslint-disable-next-line + if (!!section.isDisabled || item.isDisabled || !item.isSelectable) { disabledOptionsIndexes.push(disabledIndex); } disabledIndex += 1; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 005a8ab21cc1..a8addd8c68e6 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -58,6 +58,9 @@ type ListItem = { /** Whether this option is selected */ isSelected?: boolean; + /** Whether this option is selectable */ + isSelectable?: boolean; + /** Whether this option is disabled for selection */ isDisabled?: boolean; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 3970533870c1..6b886520336f 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -200,7 +200,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se * Add or remove all users passed from the selectedEmployees list */ const toggleAllUsers = (memberList: MemberOption[]) => { - const enabledAccounts = memberList.filter((member) => !member.isDisabled); + const enabledAccounts = memberList.filter((member) => !member.isDisabled && member.isSelectable); const everyoneSelected = enabledAccounts.every((member) => selectedEmployees.includes(member.accountID)); if (everyoneSelected) { @@ -337,11 +337,10 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se keyForList: accountIDKey, accountID, isSelected, + isSelectable: isPolicyAdmin && accountID !== session?.accountID && accountID !== policy?.ownerAccountID, isDisabled: isPolicyAdmin && - (accountID === session?.accountID || - accountID === policy?.ownerAccountID || - policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || + (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyMember.errors)), text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), alternateText: formatPhoneNumber(details?.login ?? ''), From 2d361a8798936049716964c176d53541321b6616 Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 7 Mar 2024 11:43:47 +0100 Subject: [PATCH 066/314] showing disable icon when item is not selectable --- src/components/SelectionList/BaseListItem.tsx | 6 ++++-- src/components/SelectionList/BaseSelectionList.tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 3e8573888404..38998e745196 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -34,6 +34,8 @@ function BaseListItem({ const StyleUtils = useStyleUtils(); const {hovered, bind} = useHover(); + const isItemSelectable = item.isSelectable === undefined || item.isSelectable; + const rightHandSideComponentRender = () => { if (canSelectMultiple || !rightHandSideComponent) { return null; @@ -80,9 +82,9 @@ function BaseListItem({ {item.isSelected && ( diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 5646b993d135..3fc586b5ed13 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -112,7 +112,7 @@ function BaseSelectionList( // If disabled, add to the disabled indexes array // eslint-disable-next-line - if (!!section.isDisabled || item.isDisabled || !item.isSelectable) { + if (!!section.isDisabled || item.isDisabled || (item.isSelectable !== undefined && !item.isSelectable)) { disabledOptionsIndexes.push(disabledIndex); } disabledIndex += 1; From 8d92c5e61f3849cf94901bdc2fcb258248b06a16 Mon Sep 17 00:00:00 2001 From: burczu Date: Fri, 8 Mar 2024 13:06:57 +0100 Subject: [PATCH 067/314] showing transfer owner button for owners opened by other admins --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + .../members/WorkspaceMemberDetailsPage.tsx | 37 +++++++++++++++---- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 3575854ee7e2..956c7d5637de 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1813,6 +1813,7 @@ export default { removeMemberButtonTitle: 'Remove from workspace', removeMemberPrompt: ({memberName}) => `Are you sure you want to remove ${memberName}`, removeMemberTitle: 'Remove member', + transferOwner: 'Transfer owner', makeMember: 'Make member', makeAdmin: 'Make admin', selectAll: 'Select all', diff --git a/src/languages/es.ts b/src/languages/es.ts index 51a83e55fee2..832538e91951 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1837,6 +1837,7 @@ export default { removeMemberButtonTitle: 'Quitar del espacio de trabajo', removeMemberPrompt: ({memberName}) => `¿Estás seguro de que deseas eliminar a ${memberName}`, removeMemberTitle: 'Eliminar miembro', + transferOwner: 'Transferir la propiedad', makeMember: 'Hacer miembro', makeAdmin: 'Hacer administrador', selectAll: 'Seleccionar todo', diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index d44ff8baa08b..e6befe277d3c 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -30,6 +30,7 @@ import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx'; +import useCurrentUserPersonalDetails from "@hooks/useCurrentUserPersonalDetails"; type WorkspacePolicyOnyxProps = { /** Personal details of all users */ @@ -42,6 +43,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, rou const styles = useThemeStyles(); const {translate} = useLocalize(); const StyleUtils = useStyleUtils(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const [isRemoveMemberConfirmModalVisible, setIsRemoveMemberConfirmModalVisible] = React.useState(false); @@ -54,6 +56,9 @@ function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, rou const avatar = details.avatar ?? UserUtils.getDefaultAvatar(); const fallbackIcon = details.fallbackIcon ?? ''; const displayName = details.displayName ?? ''; + const isOwner = policy?.owner === details.login; + const isCurrentUserAdmin = policyMembers?.[currentUserPersonalDetails?.accountID]?.role === CONST.POLICY.ROLE.ADMIN; + const isCurrentUserOwner = policy?.owner === currentUserPersonalDetails?.login; const askForConfirmationToRemove = () => { setIsRemoveMemberConfirmModalVisible(true); @@ -73,6 +78,10 @@ function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, rou Navigation.navigate(ROUTES.WORKSPACE_MEMBER_ROLE_SELECTION.getRoute(route.params.policyID, accountID, Navigation.getActiveRoute())); }, [accountID, route.params.policyID]); + const startChangeOwnershipFlow = useCallback(() => { + + }, []); + return ( @@ -101,14 +110,26 @@ function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, rou {displayName} )} - + ) : ( + + )} + From ec0d55355e71ea71f81f29a013eb5185a34cf119 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 11 Mar 2024 17:32:37 +0100 Subject: [PATCH 090/314] migrate bumpVersion.js to TypeScript --- .../{bumpVersion.js => bumpVersion.ts} | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) rename .github/actions/javascript/bumpVersion/{bumpVersion.js => bumpVersion.ts} (73%) diff --git a/.github/actions/javascript/bumpVersion/bumpVersion.js b/.github/actions/javascript/bumpVersion/bumpVersion.ts similarity index 73% rename from .github/actions/javascript/bumpVersion/bumpVersion.js rename to .github/actions/javascript/bumpVersion/bumpVersion.ts index 647c295fdc52..d08293c856be 100644 --- a/.github/actions/javascript/bumpVersion/bumpVersion.js +++ b/.github/actions/javascript/bumpVersion/bumpVersion.ts @@ -1,17 +1,16 @@ -const {promisify} = require('util'); -const fs = require('fs'); -const exec = promisify(require('child_process').exec); -const _ = require('underscore'); -const core = require('@actions/core'); -const versionUpdater = require('../../../libs/versionUpdater'); -const {updateAndroidVersion, updateiOSVersion, generateAndroidVersionCode} = require('../../../libs/nativeVersionUpdater'); +import * as core from '@actions/core'; +import {exec as originalExec} from 'child_process'; +import fs from 'fs'; +import {promisify} from 'util'; +import {generateAndroidVersionCode, updateAndroidVersion, updateiOSVersion} from '../../../libs/nativeVersionUpdater'; +import * as versionUpdater from '../../../libs/versionUpdater'; + +const exec = promisify(originalExec); /** * Update the native app versions. - * - * @param {String} version */ -function updateNativeVersions(version) { +function updateNativeVersions(version: string) { console.log(`Updating native versions to ${version}`); // Update Android @@ -28,7 +27,7 @@ function updateNativeVersions(version) { // Update iOS try { const cfBundleVersion = updateiOSVersion(version); - if (_.isString(cfBundleVersion) && cfBundleVersion.split('.').length === 4) { + if (typeof cfBundleVersion === 'string' && cfBundleVersion.split('.').length === 4) { core.setOutput('NEW_IOS_VERSION', cfBundleVersion); console.log('Successfully updated iOS!'); } else { @@ -36,17 +35,17 @@ function updateNativeVersions(version) { } } catch (err) { console.error('Error updating iOS'); - core.setFailed(err); + core.setFailed(err as string); } } -let semanticVersionLevel = core.getInput('SEMVER_LEVEL', {require: true}); -if (!semanticVersionLevel || !_.contains(versionUpdater.SEMANTIC_VERSION_LEVELS, semanticVersionLevel)) { +let semanticVersionLevel = core.getInput('SEMVER_LEVEL', {required: true}); +if (!semanticVersionLevel || !Object.keys(versionUpdater.SEMANTIC_VERSION_LEVELS).includes(semanticVersionLevel)) { semanticVersionLevel = versionUpdater.SEMANTIC_VERSION_LEVELS.BUILD; console.log(`Invalid input for 'SEMVER_LEVEL': ${semanticVersionLevel}`, `Defaulting to: ${semanticVersionLevel}`); } -const {version: previousVersion} = JSON.parse(fs.readFileSync('./package.json')); +const {version: previousVersion} = JSON.parse(fs.readFileSync('./package.json').toString()); const newVersion = versionUpdater.incrementVersion(previousVersion, semanticVersionLevel); console.log(`Previous version: ${previousVersion}`, `New version: ${newVersion}`); From 7068222b62669118304ac6a424deda12388b695d Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 11 Mar 2024 17:33:05 +0100 Subject: [PATCH 091/314] start migrating markPullRequestsAsDeployed to TypeScript --- ...loyed.js => markPullRequestsAsDeployed.ts} | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) rename .github/actions/javascript/markPullRequestsAsDeployed/{markPullRequestsAsDeployed.js => markPullRequestsAsDeployed.ts} (84%) diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts similarity index 84% rename from .github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js rename to .github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts index d03a947cdec8..cf132794ce46 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts @@ -1,17 +1,15 @@ -const _ = require('underscore'); -const core = require('@actions/core'); -const {context} = require('@actions/github'); -const CONST = require('../../../libs/CONST'); -const ActionUtils = require('../../../libs/ActionUtils'); -const GithubUtils = require('../../../libs/GithubUtils'); +import core from '@actions/core'; +import {context} from '@actions/github'; +import * as ActionUtils from '../../../libs/ActionUtils'; +import CONST from '../../../libs/CONST'; +import * as GithubUtils from '../../../libs/GithubUtils'; + +type PlatformResult = 'success' | 'cancelled' | 'skipped' | 'failure'; /** * Return a nicely formatted message for the table based on the result of the GitHub action job - * - * @param {String} platformResult - * @returns {String} */ -function getDeployTableMessage(platformResult) { +function getDeployTableMessage(platformResult: PlatformResult) { switch (platformResult) { case 'success': return `${platformResult} ✅`; @@ -27,10 +25,6 @@ function getDeployTableMessage(platformResult) { /** * Comment Single PR - * - * @param {Number} PR - * @param {String} message - * @returns {Promise} */ async function commentPR(PR, message) { try { @@ -45,7 +39,7 @@ async function commentPR(PR, message) { const workflowURL = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; async function run() { - const prList = _.map(ActionUtils.getJSONInput('PR_LIST', {required: true}), (num) => Number.parseInt(num, 10)); + const prList = ActionUtils.getJSONInput('PR_LIST', {required: true}).map((num: string) => Number.parseInt(num, 10)); const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: true}); const version = core.getInput('DEPLOY_VERSION', {required: true}); @@ -55,10 +49,10 @@ async function run() { const webResult = getDeployTableMessage(core.getInput('WEB', {required: true})); /** - * @param {String} deployer - * @param {String} deployVerb - * @param {String} prTitle - * @returns {String} + * @param deployer + * @param deployVerb + * @param prTitle + * @returns */ function getDeployMessage(deployer, deployVerb, prTitle) { let message = `🚀 [${deployVerb}](${workflowURL}) to ${isProd ? 'production' : 'staging'}`; @@ -83,7 +77,7 @@ async function run() { labels: CONST.LABELS.STAGING_DEPLOY, state: 'closed', }); - const previousChecklistID = _.first(deployChecklists).number; + const previousChecklistID = deployChecklists[0].number; // who closed the last deploy checklist? const deployer = await GithubUtils.getActorWhoClosedIssue(previousChecklistID); @@ -102,7 +96,7 @@ async function run() { repo: CONST.APP_REPO, per_page: 100, }); - const currentTag = _.find(recentTags, (tag) => tag.name === version); + const currentTag = recentTags.find((tag) => tag.name === version); if (!currentTag) { const err = `Could not find tag matching ${version}`; console.error(err); @@ -139,4 +133,4 @@ if (require.main === module) { run(); } -module.exports = run; +export default run; From 2260d2a4a9fcecb00e2dd2c64bcd3379b40a5c70 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 11 Mar 2024 17:33:21 +0100 Subject: [PATCH 092/314] start migrating GithubUtils to TypeScript --- .../libs/{GithubUtils.js => GithubUtils.ts} | 235 +++++++++--------- 1 file changed, 120 insertions(+), 115 deletions(-) rename .github/libs/{GithubUtils.js => GithubUtils.ts} (71%) diff --git a/.github/libs/GithubUtils.js b/.github/libs/GithubUtils.ts similarity index 71% rename from .github/libs/GithubUtils.js rename to .github/libs/GithubUtils.ts index e988167850ec..046701a94a9c 100644 --- a/.github/libs/GithubUtils.js +++ b/.github/libs/GithubUtils.ts @@ -1,10 +1,12 @@ -const _ = require('underscore'); -const lodashGet = require('lodash/get'); -const core = require('@actions/core'); -const {GitHub, getOctokitOptions} = require('@actions/github/lib/utils'); -const {throttling} = require('@octokit/plugin-throttling'); -const {paginateRest} = require('@octokit/plugin-paginate-rest'); -const CONST = require('./CONST'); +import * as core from '@actions/core'; +import {getOctokitOptions, GitHub} from '@actions/github/lib/utils'; +import type {Octokit as OctokitCore} from '@octokit/core'; +import type {PaginateInterface} from '@octokit/plugin-paginate-rest'; +import {paginateRest} from '@octokit/plugin-paginate-rest'; +import {throttling} from '@octokit/plugin-throttling'; +import _ from 'underscore'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import CONST from './CONST'; const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const PULL_REQUEST_REGEX = new RegExp(`${GITHUB_BASE_URL_REGEX.source}/.*/.*/pull/([0-9]+).*`); @@ -14,11 +16,14 @@ const ISSUE_OR_PULL_REQUEST_REGEX = new RegExp(`${GITHUB_BASE_URL_REGEX.source}/ /** * The standard rate in ms at which we'll poll the GitHub API to check for status changes. * It's 10 seconds :) - * @type {number} */ const POLL_RATE = 10000; +type OctokitOptions = {method: string; url: string; request: {retryCount: number}}; + class GithubUtils { + static internalOctokit: OctokitCore & {paginate: PaginateInterface}; + /** * Initialize internal octokit * @@ -33,7 +38,7 @@ class GithubUtils { getOctokitOptions(token, { throttle: { retryAfterBaseValue: 2000, - onRateLimit: (retryAfter, options) => { + onRateLimit: (retryAfter: number, options: OctokitOptions) => { console.warn(`Request quota exhausted for request ${options.method} ${options.url}`); // Retry five times when hitting a rate limit error, then give up @@ -42,7 +47,7 @@ class GithubUtils { return true; } }, - onAbuseLimit: (retryAfter, options) => { + onAbuseLimit: (retryAfter: number, options: OctokitOptions) => { // does not retry, only logs a warning console.warn(`Abuse detected for request ${options.method} ${options.url}`); }, @@ -98,7 +103,7 @@ class GithubUtils { /** * Finds one open `StagingDeployCash` issue via GitHub octokit library. * - * @returns {Promise} + * @returns */ static getStagingDeployCash() { return this.octokit.issues @@ -128,8 +133,8 @@ class GithubUtils { /** * Takes in a GitHub issue object and returns the data we want. * - * @param {Object} issue - * @returns {Object} + * @param issue + * @returns */ static getStagingDeployCashData(issue) { try { @@ -158,8 +163,8 @@ class GithubUtils { * * @private * - * @param {Object} issue - * @returns {Array} - [{url: String, number: Number, isVerified: Boolean}] + * @param issue + * @returns - [{url: String, number: Number, isVerified: Boolean}] */ static getStagingDeployCashPRList(issue) { let PRListSection = issue.body.match(/pull requests:\*\*\r?\n((?:-.*\r?\n)+)\r?\n\r?\n?/) || []; @@ -169,7 +174,7 @@ class GithubUtils { return []; } PRListSection = PRListSection[1]; - const PRList = _.map([...PRListSection.matchAll(new RegExp(`- \\[([ x])] (${PULL_REQUEST_REGEX.source})`, 'g'))], (match) => ({ + const PRList = [...PRListSection.matchAll(new RegExp(`- \\[([ x])] (${PULL_REQUEST_REGEX.source})`, 'g'))].map((match) => ({ url: match[2], number: Number.parseInt(match[3], 10), isVerified: match[1] === 'x', @@ -182,8 +187,8 @@ class GithubUtils { * * @private * - * @param {Object} issue - * @returns {Array} - [{URL: String, number: Number, isResolved: Boolean}] + * @param issue + * @returns - [{URL: String, number: Number, isResolved: Boolean}] */ static getStagingDeployCashDeployBlockers(issue) { let deployBlockerSection = issue.body.match(/Deploy Blockers:\*\*\r?\n((?:-.*\r?\n)+)/) || []; @@ -191,7 +196,7 @@ class GithubUtils { return []; } deployBlockerSection = deployBlockerSection[1]; - const deployBlockers = _.map([...deployBlockerSection.matchAll(new RegExp(`- \\[([ x])]\\s(${ISSUE_OR_PULL_REQUEST_REGEX.source})`, 'g'))], (match) => ({ + const deployBlockers = [...deployBlockerSection.matchAll(new RegExp(`- \\[([ x])]\\s(${ISSUE_OR_PULL_REQUEST_REGEX.source})`, 'g'))].map((match) => ({ url: match[2], number: Number.parseInt(match[3], 10), isResolved: match[1] === 'x', @@ -204,8 +209,8 @@ class GithubUtils { * * @private * - * @param {Object} issue - * @returns {Array} - [{URL: String, number: Number, isResolved: Boolean}] + * @param issue + * @returns - [{URL: String, number: Number, isResolved: Boolean}] */ static getStagingDeployCashInternalQA(issue) { let internalQASection = issue.body.match(/Internal QA:\*\*\r?\n((?:- \[[ x]].*\r?\n)+)/) || []; @@ -213,7 +218,7 @@ class GithubUtils { return []; } internalQASection = internalQASection[1]; - const internalQAPRs = _.map([...internalQASection.matchAll(new RegExp(`- \\[([ x])]\\s(${PULL_REQUEST_REGEX.source})`, 'g'))], (match) => ({ + const internalQAPRs = [...internalQASection.matchAll(new RegExp(`- \\[([ x])]\\s(${PULL_REQUEST_REGEX.source})`, 'g'))].map((match) => ({ url: match[2].split('-')[0].trim(), number: Number.parseInt(match[3], 10), isResolved: match[1] === 'x', @@ -224,54 +229,52 @@ class GithubUtils { /** * Generate the issue body for a StagingDeployCash. * - * @param {String} tag - * @param {Array} PRList - The list of PR URLs which are included in this StagingDeployCash - * @param {Array} [verifiedPRList] - The list of PR URLs which have passed QA. - * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. - * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. - * @param {Array} [resolvedInternalQAPRs] - The list of Internal QA PR URLs which have been resolved. - * @param {Boolean} [isTimingDashboardChecked] - * @param {Boolean} [isFirebaseChecked] - * @param {Boolean} [isGHStatusChecked] - * @returns {Promise} + * @param tag + * @param PRList - The list of PR URLs which are included in this StagingDeployCash + * @param [verifiedPRList] - The list of PR URLs which have passed QA. + * @param [deployBlockers] - The list of DeployBlocker URLs. + * @param [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param [resolvedInternalQAPRs] - The list of Internal QA PR URLs which have been resolved. + * @param [isTimingDashboardChecked] + * @param [isFirebaseChecked] + * @param [isGHStatusChecked] + * @returns */ static generateStagingDeployCashBody( - tag, - PRList, - verifiedPRList = [], - deployBlockers = [], - resolvedDeployBlockers = [], - resolvedInternalQAPRs = [], + tag: string, + PRList: string[], + verifiedPRList: string[] = [], + deployBlockers: string[] = [], + resolvedDeployBlockers: string[] = [], + resolvedInternalQAPRs: string[] = [], isTimingDashboardChecked = false, isFirebaseChecked = false, isGHStatusChecked = false, ) { - return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) + return this.fetchAllPullRequests(PRList.map(this.getPullRequestNumberFromURL)) .then((data) => { // The format of this map is following: // { // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } - const internalQAPRMap = _.reduce( - _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), - (map, pr) => { + const internalQAPRMap = data + .filter((pr) => !isEmptyObject(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))) + .reduce((map, pr) => { // eslint-disable-next-line no-param-reassign map[pr.html_url] = pr.merged_by.login; return map; - }, - {}, - ); + }, {}); console.log('Found the following Internal QA PRs:', internalQAPRMap); const noQAPRs = _.pluck( - _.filter(data, (PR) => /\[No\s?QA]/i.test(PR.title)), + data.filter((PR) => /\[No\s?QA]/i.test(PR.title)), 'html_url', ); console.log('Found the following NO QA PRs:', noQAPRs); const verifiedOrNoQAPRs = _.union(verifiedPRList, noQAPRs); - const sortedPRList = _.chain(PRList).difference(_.keys(internalQAPRMap)).unique().sortBy(GithubUtils.getPullRequestNumberFromURL).value(); + const sortedPRList = _.chain(PRList).difference(Object.keys(internalQAPRMap)).unique().sortBy(GithubUtils.getPullRequestNumberFromURL).value(); const sortedDeployBlockers = _.sortBy(_.unique(deployBlockers), GithubUtils.getIssueOrPullRequestNumberFromURL); // Tag version and comparison URL @@ -279,22 +282,22 @@ class GithubUtils { let issueBody = `**Release Version:** \`${tag}\`\r\n**Compare Changes:** https://github.com/Expensify/App/compare/production...staging\r\n`; // PR list - if (!_.isEmpty(sortedPRList)) { + if (!isEmptyObject(sortedPRList)) { issueBody += '\r\n**This release contains changes from the following pull requests:**\r\n'; - _.each(sortedPRList, (URL) => { - issueBody += _.contains(verifiedOrNoQAPRs, URL) ? '- [x]' : '- [ ]'; + sortedPRList.forEach((URL) => { + issueBody += verifiedOrNoQAPRs.includes(URL) ? '- [x]' : '- [ ]'; issueBody += ` ${URL}\r\n`; }); issueBody += '\r\n\r\n'; } // Internal QA PR list - if (!_.isEmpty(internalQAPRMap)) { + if (!isEmptyObject(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { + internalQAPRMap.each((merger, URL) => { const mergerMention = `@${merger}`; - issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; + issueBody += `${resolvedInternalQAPRs.includes(URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; @@ -303,10 +306,10 @@ class GithubUtils { } // Deploy blockers - if (!_.isEmpty(deployBlockers)) { + if (!isEmptyObject(deployBlockers)) { issueBody += '**Deploy Blockers:**\r\n'; - _.each(sortedDeployBlockers, (URL) => { - issueBody += _.contains(resolvedDeployBlockers, URL) ? '- [x] ' : '- [ ] '; + sortedDeployBlockers.forEach((URL) => { + issueBody += resolvedDeployBlockers.includes(URL) ? '- [x] ' : '- [ ] '; issueBody += URL; issueBody += '\r\n'; }); @@ -326,7 +329,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); + const issueAssignees = Object.values(internalQAPRMap); const issue = {issueBody, issueAssignees}; return issue; }) @@ -335,12 +338,9 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. - * - * @param {Array} pullRequestNumbers - * @returns {Promise} */ - static fetchAllPullRequests(pullRequestNumbers) { - const oldestPR = _.first(_.sortBy(pullRequestNumbers)); + static fetchAllPullRequests(pullRequestNumbers: number[]) { + const oldestPR = _.sortBy(pullRequestNumbers)[0]; return this.paginate( this.octokit.pulls.list, { @@ -352,19 +352,19 @@ class GithubUtils { per_page: 100, }, ({data}, done) => { - if (_.find(data, (pr) => pr.number === oldestPR)) { + if (data.find((pr) => pr.number === oldestPR)) { done(); } return data; }, ) - .then((prList) => _.filter(prList, (pr) => _.contains(pullRequestNumbers, pr.number))) + .then((prList) => prList.filter((pr) => pullRequestNumbers.includes(pr.number))) .catch((err) => console.error('Failed to get PR list', err)); } /** - * @param {Number} pullRequestNumber - * @returns {Promise} + * @param pullRequestNumber + * @returns */ static getPullRequestBody(pullRequestNumber) { return this.octokit.pulls @@ -377,8 +377,8 @@ class GithubUtils { } /** - * @param {Number} pullRequestNumber - * @returns {Promise} + * @param pullRequestNumber + * @returns */ static getAllReviewComments(pullRequestNumber) { return this.paginate( @@ -389,13 +389,13 @@ class GithubUtils { pull_number: pullRequestNumber, per_page: 100, }, - (response) => _.map(response.data, (review) => review.body), + (response) => response.data.map((review) => review.body), ); } /** - * @param {Number} issueNumber - * @returns {Promise} + * @param issueNumber + * @returns */ static getAllComments(issueNumber) { return this.paginate( @@ -406,17 +406,17 @@ class GithubUtils { issue_number: issueNumber, per_page: 100, }, - (response) => _.map(response.data, (comment) => comment.body), + (response) => response.data.map((comment) => comment.body), ); } /** * Create comment on pull request * - * @param {String} repo - The repo to search for a matching pull request or issue number - * @param {Number} number - The pull request or issue number - * @param {String} messageBody - The comment message - * @returns {Promise} + * @param repo - The repo to search for a matching pull request or issue number + * @param number - The pull request or issue number + * @param messageBody - The comment message + * @returns */ static createComment(repo, number, messageBody) { console.log(`Writing comment on #${number}`); @@ -431,50 +431,53 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. * - * @param {String} workflow - * @returns {Promise} + * @param workflow + * @returns */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); - return this.octokit.actions - .listWorkflowRuns({ - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - workflow_id: workflow, - }) - .then((response) => lodashGet(response, 'data.workflow_runs[0].id')); + return ( + this.octokit.actions + .listWorkflowRuns({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + workflow_id: workflow, + }) + // .then((response) => lodashGet(response, 'data.workflow_runs[0].id')); + .then((response) => response.data.workflow_runs[0].id) + ); } /** * Generate the well-formatted body of a production release. * - * @param {Array} pullRequests - * @returns {String} + * @param pullRequests + * @returns */ static getReleaseBody(pullRequests) { - return _.map(pullRequests, (number) => `- ${this.getPullRequestURLFromNumber(number)}`).join('\r\n'); + return pullRequests.map((number) => `- ${this.getPullRequestURLFromNumber(number)}`).join('\r\n'); } /** * Generate the URL of an New Expensify pull request given the PR number. * - * @param {Number} number - * @returns {String} + * @param number + * @returns */ - static getPullRequestURLFromNumber(number) { - return `${CONST.APP_REPO_URL}/pull/${number}`; + static getPullRequestURLFromNumber(value: number): string { + return `${CONST.APP_REPO_URL}/pull/${value}`; } /** * Parse the pull request number from a URL. * - * @param {String} URL - * @returns {Number} + * @param URL + * @returns * @throws {Error} If the URL is not a valid Github Pull Request. */ - static getPullRequestNumberFromURL(URL) { + static getPullRequestNumberFromURL(URL: string): number { const matches = URL.match(PULL_REQUEST_REGEX); - if (!_.isArray(matches) || matches.length !== 2) { + if (!Array.isArray(matches) || matches.length !== 2) { throw new Error(`Provided URL ${URL} is not a Github Pull Request!`); } return Number.parseInt(matches[1], 10); @@ -483,13 +486,13 @@ class GithubUtils { /** * Parse the issue number from a URL. * - * @param {String} URL - * @returns {Number} + * @param URL + * @returns * @throws {Error} If the URL is not a valid Github Issue. */ static getIssueNumberFromURL(URL) { const matches = URL.match(ISSUE_REGEX); - if (!_.isArray(matches) || matches.length !== 2) { + if (!Array.isArray(matches) || matches.length !== 2) { throw new Error(`Provided URL ${URL} is not a Github Issue!`); } return Number.parseInt(matches[1], 10); @@ -498,13 +501,13 @@ class GithubUtils { /** * Parse the issue or pull request number from a URL. * - * @param {String} URL - * @returns {Number} + * @param URL + * @returns * @throws {Error} If the URL is not a valid Github Issue or Pull Request. */ static getIssueOrPullRequestNumberFromURL(URL) { const matches = URL.match(ISSUE_OR_PULL_REQUEST_REGEX); - if (!_.isArray(matches) || matches.length !== 2) { + if (!Array.isArray(matches) || matches.length !== 2) { throw new Error(`Provided URL ${URL} is not a valid Github Issue or Pull Request!`); } return Number.parseInt(matches[1], 10); @@ -513,18 +516,21 @@ class GithubUtils { /** * Return the login of the actor who closed an issue or PR. If the issue is not closed, return an empty string. * - * @param {Number} issueNumber - * @returns {Promise} + * @param issueNumber + * @returns */ static getActorWhoClosedIssue(issueNumber) { - return this.paginate(this.octokit.issues.listEvents, { - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - issue_number: issueNumber, - per_page: 100, - }) - .then((events) => _.filter(events, (event) => event.event === 'closed')) - .then((closedEvents) => lodashGet(_.last(closedEvents), 'actor.login', '')); + return ( + this.paginate(this.octokit.issues.listEvents, { + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + issue_number: issueNumber, + per_page: 100, + }) + .then((events) => events.filter((event) => event.event === 'closed')) + // .then((closedEvents) => lodashGet(_.last(closedEvents), 'actor.login', '')); + .then((closedEvents) => _.last(closedEvents).actor.login ?? '') + ); } static getArtifactByName(artefactName) { @@ -536,6 +542,5 @@ class GithubUtils { } } -module.exports = GithubUtils; -module.exports.ISSUE_OR_PULL_REQUEST_REGEX = ISSUE_OR_PULL_REQUEST_REGEX; -module.exports.POLL_RATE = POLL_RATE; +export default GithubUtils; +export {ISSUE_OR_PULL_REQUEST_REGEX, POLL_RATE}; From 7c1cf629aa902962e3f738c9f84669135c03afac Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Mon, 11 Mar 2024 20:55:23 +0300 Subject: [PATCH 093/314] 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 05:23:14 +0530 Subject: [PATCH 094/314] setup theme for helpdot --- docs/_sass/_colors.scss | 58 +++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/docs/_sass/_colors.scss b/docs/_sass/_colors.scss index f0c89d31c580..c9473925d791 100644 --- a/docs/_sass/_colors.scss +++ b/docs/_sass/_colors.scss @@ -1,22 +1,46 @@ -// Product Color Spectrum -$color-product-dark-100: #061B09; -$color-product-dark-200: #072419; -$color-product-dark-300: #0A2E25; -$color-product-dark-400: #1A3D32; -$color-product-dark-500: #224F41; -$color-product-dark-600: #2A604F; -$color-product-dark-700: #8B9C8F; -$color-product-dark-800: #AFBBB0; -$color-product-dark-900: #E7ECE9; +@media (prefers-color-scheme: dark) { + // Product Color Spectrum + $color-product-dark-100: #061B09; + $color-product-dark-200: #072419; + $color-product-dark-300: #0A2E25; + $color-product-dark-400: #1A3D32; + $color-product-dark-500: #224F41; + $color-product-dark-600: #2A604F; + $color-product-dark-700: #8B9C8F; + $color-product-dark-800: #AFBBB0; + $color-product-dark-900: #E7ECE9; -// Colors for Links and Success -$color-blue200: #B0D9FF; -$color-blue300: #5AB0FF; -$color-green400: #03D47C; -$color-green500: #00a862; + // Colors for Links and Success + $color-blue200: #B0D9FF; + $color-blue300: #5AB0FF; + $color-green400: #03D47C; + $color-green500: #00a862; -// Overlay BG color -$color-overlay-background: rgba(26, 61, 50, 0.72); + // Overlay BG color + $color-overlay-background: rgba(26, 61, 50, 0.72); +} + +@media (prefers-color-scheme: light) { + // Product Color Spectrum + $color-product-dark-100: #061B09; + $color-product-dark-200: #072419; + $color-product-dark-300: #0A2E25; + $color-product-dark-400: #1A3D32; + $color-product-dark-500: #224F41; + $color-product-dark-600: #2A604F; + $color-product-dark-700: #8B9C8F; + $color-product-dark-800: #AFBBB0; + $color-product-dark-900: #E7ECE9; + + // Colors for Links and Success + $color-blue200: #B0D9FF; + $color-blue300: #5AB0FF; + $color-green400: #03D47C; + $color-green500: #00a862; + + // Overlay BG color + $color-overlay-background: rgba(26, 61, 50, 0.72); +} // UI Colors $color-text: $color-product-dark-900; From 283c4de6983d0fe856e280d068a2e2b320b329e1 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 12 Mar 2024 05:23:21 +0530 Subject: [PATCH 095/314] setup theme for helpdot --- docs/_sass/_colors.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_sass/_colors.scss b/docs/_sass/_colors.scss index c9473925d791..22af28974281 100644 --- a/docs/_sass/_colors.scss +++ b/docs/_sass/_colors.scss @@ -58,3 +58,4 @@ $color-button-background: $color-product-dark-400; $color-button-background-hover: $color-product-dark-500; $color-button-success-background: $color-green400; $color-button-success-background-hover: $color-green500; +$color-overlay: $color-overlay-background; From efaa96a5f02d592032bb7ed48d2850dbd65a5be0 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 12 Mar 2024 05:23:25 +0530 Subject: [PATCH 096/314] setup theme for helpdot --- docs/_sass/_search-bar.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_sass/_search-bar.scss b/docs/_sass/_search-bar.scss index f414d25fc266..5c58d70b5851 100644 --- a/docs/_sass/_search-bar.scss +++ b/docs/_sass/_search-bar.scss @@ -67,7 +67,7 @@ left: 0; right: 0; bottom: 0; - background-color: $color-overlay-background; + background-color: $color-overlay; z-index: 1; } From 2e4c65f989ce4f835fe19c876e646ad4bf94d2db Mon Sep 17 00:00:00 2001 From: burczu Date: Tue, 12 Mar 2024 10:17:53 +0100 Subject: [PATCH 097/314] transfer balance confirmation copy handled --- src/languages/en.ts | 6 +++ src/languages/es.ts | 6 +++ .../members/WorkspaceMemberDetailsPage.tsx | 2 +- .../members/WorkspaceOwnerChangeCheckPage.tsx | 48 ++++++++++++++++--- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index e096aa091c95..51c3623aaaed 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1967,6 +1967,12 @@ export default { updateCurrencyPrompt: 'It looks like your Workspace is currently set to a different currency than USD. Please click the button below to update your currency to USD now.', updateToUSD: 'Update to USD', }, + changeOwner: { + changeOwnerPageTitle: 'Change owner', + outstandingBalance: 'Outstanding balance', + transferBalance: 'Transfer balance', + transferBalanceFirstParagraph: ({email, amount}) => `The account owing this workspace (${email}) has an outstanding balance.\n\nDo you want to transfer this amount ${amount} in order to take over billing for this workspace? Your payment card will be charged immediately.`, + } }, getAssistancePage: { title: 'Get assistance', diff --git a/src/languages/es.ts b/src/languages/es.ts index 99bb64e0af37..6c5b5c73e589 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1993,6 +1993,12 @@ export default { 'Parece que tu espacio de trabajo está configurado actualmente en una moneda diferente a USD. Por favor, haz clic en el botón de abajo para actualizar tu moneda a USD ahora.', updateToUSD: 'Actualizar a USD', }, + changeOwner: { + changeOwnerPageTitle: 'Cambio de propietario', + outstandingBalance: 'Saldo pendiente', + transferBalance: 'Transfer balance', + transferBalanceFirstParagraph: ({email, amount}) => `La cuenta que debe este espacio de trabajo (${email}) tiene un saldo pendiente.\n\n¿Desea transferir este monto ${amount} para hacerse cargo de la facturación de este espacio de trabajo? El cargo en su tarjeta de pago se realizará inmediatamente.`, + } }, getAssistancePage: { title: 'Obtener ayuda', diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index d12d0235a699..294138c7daa9 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -94,7 +94,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, rou }, [policyID]); const temporaryOpenCheckPage = () => { - Navigation.navigate(ROUTES.WORKSPACE_OWNER_CHANGE_CHECK.getRoute(policyID, CONST.POLICY.OWNERSHIP_ERRORS.NO_BILLING_CARD)); + Navigation.navigate(ROUTES.WORKSPACE_OWNER_CHANGE_CHECK.getRoute(policyID, CONST.POLICY.OWNERSHIP_ERRORS.AMOUNT_OWED)); }; return ( diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeCheckPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeCheckPage.tsx index 8ae4dc5a232a..f1a01366ad9a 100644 --- a/src/pages/workspace/members/WorkspaceOwnerChangeCheckPage.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerChangeCheckPage.tsx @@ -1,5 +1,5 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Text from '@components/Text'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -9,9 +9,10 @@ import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAcce import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import type SCREENS from '@src/SCREENS'; import CONST from "@src/CONST"; -import Button from "@components/Button"; +import Button from '@components/Button'; import {View} from "react-native"; import useThemeStyles from "@hooks/useThemeStyles"; +import useLocalize from "@hooks/useLocalize"; type WorkspaceMemberDetailsPageProps = StackScreenProps; @@ -24,6 +25,7 @@ const CONFIRMABLE_ERRORS: string[] = [ function WorkspaceOwnerChangeCheckPage({route}: WorkspaceMemberDetailsPageProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); const policyID = route.params.policyID; const error = route.params.error; @@ -35,29 +37,61 @@ function WorkspaceOwnerChangeCheckPage({route}: WorkspaceMemberDetailsPageProps) }, []); const cancel = useCallback(() => { - + }, []); + const confirmationTitle = useMemo(() => { + switch (error) { + case CONST.POLICY.OWNERSHIP_ERRORS.AMOUNT_OWED: + return translate('workspace.changeOwner.outstandingBalance'); + default: + return null; + } + }, [error, translate]); + + const confirmationButtonText = useMemo(() => { + switch (error) { + case CONST.POLICY.OWNERSHIP_ERRORS.AMOUNT_OWED: + return translate('workspace.changeOwner.transferBalance'); + default: + return ''; + } + }, [error, translate]); + + const confirmationText = useMemo(() => { + switch (error) { + case CONST.POLICY.OWNERSHIP_ERRORS.AMOUNT_OWED: + return translate('workspace.changeOwner.transferBalanceFirstParagraph', {email: 'test@test.com', amount: '$50.00'}); + default: + return null; + } + }, [error, translate]); + return ( Navigation.goBack()} /> - Current error: {error} + {confirmationTitle} + {confirmationText} {shouldAskForConfirmation ? ( + text={confirmationButtonText} + /> ) : ( + text={translate('common.buttonConfirm')} + /> )} From 1acd6eaca8c525191a4a21f5b75a90132b66326d Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Tue, 12 Mar 2024 10:02:12 +0000 Subject: [PATCH 098/314] [TS migration][storybook] Fixed styling and feedback --- .storybook/webpack.config.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts index 6f7ee023643c..bff8c3fa6747 100644 --- a/.storybook/webpack.config.ts +++ b/.storybook/webpack.config.ts @@ -1,7 +1,5 @@ /* eslint-disable no-underscore-dangle */ - /* eslint-disable no-param-reassign */ - /* eslint-disable @typescript-eslint/naming-convention */ import dotenv from 'dotenv'; import path from 'path'; @@ -18,7 +16,7 @@ type CustomWebpackConfig = { }; }; -let envFile; +let envFile: string | null; switch (process.env.ENV) { case 'production': envFile = '.env.production'; From 075f940a96e195fbb64ea73f825dbf68cc683b1c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 12 Mar 2024 11:09:11 +0100 Subject: [PATCH 099/314] 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 1fb509f9469de830ca27b66c644cb97df364c324 Mon Sep 17 00:00:00 2001 From: burczu Date: Tue, 12 Mar 2024 11:28:12 +0100 Subject: [PATCH 100/314] temporary copy added --- src/languages/en.ts | 18 ++++++++-- src/languages/es.ts | 19 +++++++++-- .../members/WorkspaceMemberDetailsPage.tsx | 7 +--- .../members/WorkspaceOwnerChangeCheckPage.tsx | 33 ++++++++++++++++--- 4 files changed, 61 insertions(+), 16 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 51c3623aaaed..37a2cfbd03e6 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1969,9 +1969,21 @@ export default { }, changeOwner: { changeOwnerPageTitle: 'Change owner', - outstandingBalance: 'Outstanding balance', - transferBalance: 'Transfer balance', - transferBalanceFirstParagraph: ({email, amount}) => `The account owing this workspace (${email}) has an outstanding balance.\n\nDo you want to transfer this amount ${amount} in order to take over billing for this workspace? Your payment card will be charged immediately.`, + amountOwedTitle: 'Amount owed title', + amountOwedButtonText: 'Amount owed button text', + amountOwedText: 'Amount owed paragraph text.', + ownerOwesAmountTitle: 'Owner owes amount title', + ownerOwesAmountButtonText: 'Owner owes amount button text', + ownerOwesAmountText: 'Owner owes amount paragraph text.', + subscriptionTitle: 'Subscription title', + subscriptionButtonText: 'Subscription button text', + subscriptionText: 'Subscription paragraph text.', + duplicateSubscriptionTitle: 'Duplicate subscription title', + duplicateSubscriptionButtonText: 'Duplicate subscription button text', + duplicateSubscriptionText: 'Duplicate subscription paragraph text.', + hasFailedSettlementsTitle: 'Has failed settlements title', + hasFailedSettlementsButtonText: 'Has failed settlements button text', + hasFailedSettlementsText: 'Has failed settlements paragraph text.', } }, getAssistancePage: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 6c5b5c73e589..51701d38c501 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1995,9 +1995,22 @@ export default { }, changeOwner: { changeOwnerPageTitle: 'Cambio de propietario', - outstandingBalance: 'Saldo pendiente', - transferBalance: 'Transfer balance', - transferBalanceFirstParagraph: ({email, amount}) => `La cuenta que debe este espacio de trabajo (${email}) tiene un saldo pendiente.\n\n¿Desea transferir este monto ${amount} para hacerse cargo de la facturación de este espacio de trabajo? El cargo en su tarjeta de pago se realizará inmediatamente.`, + // TODO: add spanish translations below + amountOwedTitle: 'Amount owed title', + amountOwedButtonText: 'Amount owed button text', + amountOwedText: 'Amount owed paragraph text.', + ownerOwesAmountTitle: 'Owner owes amount title', + ownerOwesAmountButtonText: 'Owner owes amount button text', + ownerOwesAmountText: 'Owner owes amount paragraph text.', + subscriptionTitle: 'Subscription title', + subscriptionButtonText: 'Subscription button text', + subscriptionText: 'Subscription paragraph text.', + duplicateSubscriptionTitle: 'Duplicate subscription title', + duplicateSubscriptionButtonText: 'Duplicate subscription button text', + duplicateSubscriptionText: 'Duplicate subscription paragraph text.', + hasFailedSettlementTitle: 'Has failed settlement title', + hasFailedSettlementButtonText: 'Has failed settlement button text', + hasFailedSettlementText: 'Has failed settlement paragraph text.', } }, getAssistancePage: { diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 294138c7daa9..f914d4697a31 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -93,10 +93,6 @@ function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, rou Policy.requestWorkspaceOwnerChange(policyID); }, [policyID]); - const temporaryOpenCheckPage = () => { - Navigation.navigate(ROUTES.WORKSPACE_OWNER_CHANGE_CHECK.getRoute(policyID, CONST.POLICY.OWNERSHIP_ERRORS.AMOUNT_OWED)); - }; - return ( @@ -128,8 +124,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, rou {isSelectedMemberOwner && isCurrentUserAdmin && !isCurrentUserOwner ? (