From 80fc188d03f0b55da33f46a9eedcd0d2b9c8dccd Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 5 Feb 2024 19:30:42 +0100 Subject: [PATCH 1/2] [TS migration] Migrate 'AvatarWithImagePicker.js' component to TypeScript --- ...agePicker.js => AvatarWithImagePicker.tsx} | 187 +++++++----------- src/components/PopoverMenu.tsx | 2 +- 2 files changed, 77 insertions(+), 112 deletions(-) rename src/components/{AvatarWithImagePicker.js => AvatarWithImagePicker.tsx} (77%) diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.tsx similarity index 77% rename from src/components/AvatarWithImagePicker.js rename to src/components/AvatarWithImagePicker.tsx index f55db3dd0620..b96349bdcd85 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.tsx @@ -1,8 +1,6 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useRef, useState} from 'react'; import {StyleSheet, View} from 'react-native'; -import _ from 'underscore'; +import type {StyleProp, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,9 +8,12 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import getImageResolution from '@libs/fileDownload/getImageResolution'; -import stylePropTypes from '@styles/stylePropTypes'; +import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; 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 AttachmentPicker from './AttachmentPicker'; import Avatar from './Avatar'; @@ -20,162 +21,136 @@ import AvatarCropModal from './AvatarCropModal/AvatarCropModal'; import DotIndicatorMessage from './DotIndicatorMessage'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; -import sourcePropTypes from './Image/sourcePropTypes'; import OfflineWithFeedback from './OfflineWithFeedback'; import PopoverMenu from './PopoverMenu'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Tooltip from './Tooltip'; import withNavigationFocus from './withNavigationFocus'; -const propTypes = { +type ErrorData = { + validationError?: TranslationPaths | null | ''; + phraseParam: Record; +}; + +type OpenPickerParams = { + onPicked: (image: File) => void; +}; +type OpenPicker = (args: OpenPickerParams) => void; + +type MenuItem = { + icon: IconAsset; + text: string; + onSelected: () => void; +}; + +type AvatarWithImagePickerProps = { /** Avatar source to display */ - source: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), + source?: AvatarSource; /** Additional style props */ - style: stylePropTypes, + style?: StyleProp; /** Additional style props for disabled picker */ - disabledStyle: stylePropTypes, + disabledStyle?: StyleProp; /** Executed once an image has been selected */ - onImageSelected: PropTypes.func, + onImageSelected?: () => void; /** Execute when the user taps "remove" */ - onImageRemoved: PropTypes.func, + onImageRemoved?: () => void; /** A default avatar component to display when there is no source */ - DefaultAvatar: PropTypes.func, + DefaultAvatar?: () => React.ReactNode; /** Whether we are using the default avatar */ - isUsingDefaultAvatar: PropTypes.bool, + isUsingDefaultAvatar?: boolean; /** Size of Indicator */ - size: PropTypes.oneOf([CONST.AVATAR_SIZE.XLARGE, CONST.AVATAR_SIZE.LARGE, CONST.AVATAR_SIZE.DEFAULT]), + size?: typeof CONST.AVATAR_SIZE.XLARGE | typeof CONST.AVATAR_SIZE.LARGE | typeof CONST.AVATAR_SIZE.DEFAULT; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon: sourcePropTypes, + fallbackIcon?: AvatarSource; /** Denotes whether it is an avatar or a workspace avatar */ - type: PropTypes.oneOf([CONST.ICON_TYPE_AVATAR, CONST.ICON_TYPE_WORKSPACE]), + type?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; /** Image crop vector mask */ - editorMaskImage: sourcePropTypes, + editorMaskImage?: IconAsset; /** Additional style object for the error row */ - errorRowStyles: stylePropTypes, + errorRowStyles?: StyleProp; /** A function to run when the X button next to the error is clicked */ - onErrorClose: PropTypes.func, + onErrorClose?: () => void; /** The type of action that's pending */ - pendingAction: PropTypes.oneOf(['add', 'update', 'delete']), + pendingAction?: OnyxCommon.PendingAction; /** The errors to display */ - // eslint-disable-next-line react/forbid-prop-types - errors: PropTypes.object, + errors?: OnyxCommon.Errors; /** Title for avatar preview modal */ - headerTitle: PropTypes.string, + headerTitle?: string; /** Avatar source for avatar preview modal */ - previewSource: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), + previewSource?: AvatarSource; /** File name of the avatar */ - originalFileName: PropTypes.string, + originalFileName?: string; /** Whether navigation is focused */ - isFocused: PropTypes.bool.isRequired, + isFocused: boolean; /** Style applied to the avatar */ - avatarStyle: stylePropTypes.isRequired, + avatarStyle: StyleProp; /** Indicates if picker feature should be disabled */ - disabled: PropTypes.bool, + disabled?: boolean; /** Executed once click on view photo option */ - onViewPhotoPress: PropTypes.func, - - /** Where the popover should be positioned relative to the anchor points. */ - anchorAlignment: PropTypes.shape({ - horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), - vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), - }), -}; - -const defaultProps = { - source: '', - onImageSelected: () => {}, - onImageRemoved: () => {}, - style: [], - disabledStyle: [], - DefaultAvatar: () => {}, - isUsingDefaultAvatar: false, - size: CONST.AVATAR_SIZE.DEFAULT, - fallbackIcon: Expensicons.FallbackAvatar, - type: CONST.ICON_TYPE_AVATAR, - editorMaskImage: undefined, - errorRowStyles: [], - onErrorClose: () => {}, - pendingAction: null, - errors: null, - headerTitle: '', - previewSource: '', - originalFileName: '', - disabled: false, - onViewPhotoPress: undefined, - anchorAlignment: { - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, - }, + onViewPhotoPress?: () => void; }; function AvatarWithImagePicker({ isFocused, - DefaultAvatar, + DefaultAvatar = () => null, style, disabledStyle, pendingAction, errors, errorRowStyles, - onErrorClose, - source, - fallbackIcon, - size, - type, - headerTitle, - previewSource, - originalFileName, - isUsingDefaultAvatar, - onImageRemoved, - onImageSelected, + onErrorClose = () => {}, + source = '', + fallbackIcon = Expensicons.FallbackAvatar, + size = CONST.AVATAR_SIZE.DEFAULT, + type = CONST.ICON_TYPE_AVATAR, + headerTitle = '', + previewSource = '', + originalFileName = '', + isUsingDefaultAvatar = false, + onImageSelected = () => {}, + onImageRemoved = () => {}, editorMaskImage, avatarStyle, - disabled, + disabled = false, onViewPhotoPress, -}) { +}: AvatarWithImagePickerProps) { const theme = useTheme(); const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); const [popoverPosition, setPopoverPosition] = useState({horizontal: 0, vertical: 0}); const [isMenuVisible, setIsMenuVisible] = useState(false); - const [errorData, setErrorData] = useState({ - validationError: null, - phraseParam: {}, - }); + const [errorData, setErrorData] = useState({validationError: null, phraseParam: {}}); const [isAvatarCropModalOpen, setIsAvatarCropModalOpen] = useState(false); const [imageData, setImageData] = useState({ uri: '', name: '', type: '', }); - const anchorRef = useRef(); + const anchorRef = useRef(null); const {translate} = useLocalize(); - /** - * @param {String} error - * @param {Object} phraseParam - */ - const setError = (error, phraseParam) => { + const setError = (error: TranslationPaths | null, phraseParam: Record) => { setErrorData({ validationError: error, phraseParam, @@ -193,40 +168,29 @@ function AvatarWithImagePicker({ /** * Check if the attachment extension is allowed. - * - * @param {Object} image - * @returns {Boolean} */ - const isValidExtension = (image) => { - const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(image, 'name', '')); - return _.contains(CONST.AVATAR_ALLOWED_EXTENSIONS, fileExtension.toLowerCase()); + const isValidExtension = (image: File): boolean => { + const {fileExtension} = FileUtils.splitExtensionFromFileName(image?.name ?? ''); + return CONST.AVATAR_ALLOWED_EXTENSIONS.some((extension) => extension === fileExtension.toLowerCase()); }; /** * Check if the attachment size is less than allowed size. - * - * @param {Object} image - * @returns {Boolean} */ - const isValidSize = (image) => image && lodashGet(image, 'size', 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE; + const isValidSize = (image: File): boolean => (image?.size ?? 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE; /** * Check if the attachment resolution matches constraints. - * - * @param {Object} image - * @returns {Promise} */ - const isValidResolution = (image) => + const isValidResolution = (image: File): 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, ); /** * Validates if an image has a valid resolution and opens an avatar crop modal - * - * @param {Object} image */ - const showAvatarCropModal = (image) => { + const showAvatarCropModal = (image: File) => { if (!isValidExtension(image)) { setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS}); return; @@ -264,11 +228,8 @@ function AvatarWithImagePicker({ /** * Create menu items list for avatar menu - * - * @param {Function} openPicker - * @returns {Array} */ - const createMenuItems = (openPicker) => { + const createMenuItems = (openPicker: OpenPicker): MenuItem[] => { const menuItems = [ { icon: Expensicons.Upload, @@ -313,6 +274,7 @@ function AvatarWithImagePicker({ vertical: y + height + variables.spacing2, }); }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMenuVisible, windowWidth]); @@ -372,7 +334,11 @@ 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); @@ -421,7 +387,8 @@ function AvatarWithImagePicker({ {errorData.validationError && ( )} @@ -438,8 +405,6 @@ function AvatarWithImagePicker({ ); } -AvatarWithImagePicker.propTypes = propTypes; -AvatarWithImagePicker.defaultProps = defaultProps; AvatarWithImagePicker.displayName = 'AvatarWithImagePicker'; export default withNavigationFocus(AvatarWithImagePicker); diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 34391933da32..550bee5e397a 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -69,7 +69,7 @@ type PopoverMenuProps = Partial & { anchorPosition: AnchorPosition; /** Ref of the anchor */ - anchorRef: RefObject; + anchorRef: RefObject; /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment?: AnchorAlignment; From cfa58253fe09a8cde52920d96155fb6594cb02e3 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 21 Feb 2024 18:10:28 +0100 Subject: [PATCH 2/2] fix prettier --- src/components/AvatarWithImagePicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 1f49b0f49b40..07c535ccd96c 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -111,7 +111,7 @@ type AvatarWithImagePickerProps = { onViewPhotoPress?: () => void; /** Allows to open an image without Attachment Picker. */ - enablePreview?: boolean, + enablePreview?: boolean; }; function AvatarWithImagePicker({