From 8a00e7eda76ac7ff91979b8eb38e8240bae0f662 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Wed, 11 Oct 2023 10:49:18 +0200 Subject: [PATCH 001/231] Migrated useKeyboardState.js hook to TypeScript. --- ...KeyboardState.js => withKeyboardState.tsx} | 41 +++++++++---------- src/hooks/useKeyboardState.js | 11 ----- src/hooks/useKeyboardState.ts | 10 +++++ 3 files changed, 30 insertions(+), 32 deletions(-) rename src/components/{withKeyboardState.js => withKeyboardState.tsx} (51%) delete mode 100644 src/hooks/useKeyboardState.js create mode 100644 src/hooks/useKeyboardState.ts diff --git a/src/components/withKeyboardState.js b/src/components/withKeyboardState.tsx similarity index 51% rename from src/components/withKeyboardState.js rename to src/components/withKeyboardState.tsx index 8ddf667b4e30..997f5db59619 100755 --- a/src/components/withKeyboardState.js +++ b/src/components/withKeyboardState.tsx @@ -1,20 +1,25 @@ -import React, {forwardRef, createContext, useEffect, useState} from 'react'; +import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactNode, useEffect, useState} from 'react'; import {Keyboard} from 'react-native'; import PropTypes from 'prop-types'; -import getComponentDisplayName from '../libs/getComponentDisplayName'; +import getComponentDisplayName from "../libs/getComponentDisplayName"; -const KeyboardStateContext = createContext(null); -const keyboardStatePropTypes = { - /** Whether or not the keyboard is open */ - isKeyboardShown: PropTypes.bool.isRequired, +type KeyboardStateContextValue = { + /** Whether the keyboard is open */ + isKeyboardShown : boolean }; -const keyboardStateProviderPropTypes = { +type KeyboardStateProviderProps = { /* Actual content wrapped by this component */ - children: PropTypes.node.isRequired, + children: ReactNode }; -function KeyboardStateProvider(props) { +// TODO: Remove - left for backwards compatibility with existing components. +const keyboardStatePropTypes = { + /** Whether the keyboard is open */ + isKeyboardShown: PropTypes.bool.isRequired, +}; +const KeyboardStateContext = createContext(null); +function KeyboardStateProvider(props: KeyboardStateProviderProps) { const {children} = props; const [isKeyboardShown, setIsKeyboardShown] = useState(false); useEffect(() => { @@ -34,14 +39,10 @@ function KeyboardStateProvider(props) { return {children}; } -KeyboardStateProvider.propTypes = keyboardStateProviderPropTypes; -/** - * @param {React.Component} WrappedComponent - * @returns {React.Component} - */ -export default function withKeyboardState(WrappedComponent) { - const WithKeyboardState = forwardRef((props, ref) => ( +// eslint-disable-next-line @typescript-eslint/naming-convention +export default function withKeyboardState(WrappedComponent: ComponentType<{ref: ForwardedRef}>) { + const WithKeyboardState = forwardRef((props: Record, ref: React.ForwardedRef) => ( {(keyboardStateProps) => ( )} - - )); - - WithKeyboardState.displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent)})`; + )); + (WithKeyboardState as unknown as {displayName: string}).displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent as ComponentType)})`; return WithKeyboardState; } -export {KeyboardStateProvider, keyboardStatePropTypes, KeyboardStateContext}; +export {KeyboardStateProvider, keyboardStatePropTypes, type KeyboardStateContextValue, KeyboardStateContext}; diff --git a/src/hooks/useKeyboardState.js b/src/hooks/useKeyboardState.js deleted file mode 100644 index 8b57fb60f2b6..000000000000 --- a/src/hooks/useKeyboardState.js +++ /dev/null @@ -1,11 +0,0 @@ -import {useContext} from 'react'; -import {KeyboardStateContext} from '../components/withKeyboardState'; - -/** - * Hook for getting current state of keyboard - * whether or not the keyboard is open - * @returns {Object} - */ -export default function useKeyboardState() { - return useContext(KeyboardStateContext); -} diff --git a/src/hooks/useKeyboardState.ts b/src/hooks/useKeyboardState.ts new file mode 100644 index 000000000000..d7cc8d271503 --- /dev/null +++ b/src/hooks/useKeyboardState.ts @@ -0,0 +1,10 @@ +import {useContext} from 'react'; +import {KeyboardStateContext, KeyboardStateContextValue} from '../components/withKeyboardState'; + +/** + * Hook for getting current state of keyboard + * whether the keyboard is open + */ +export default function useKeyboardState(): KeyboardStateContextValue | null { + return useContext(KeyboardStateContext); +} From d3d050dd3fba985b9b85d25d8bbfa44746c500c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Fa=C5=82at?= Date: Wed, 11 Oct 2023 11:15:46 +0200 Subject: [PATCH 002/231] Added spacing. Co-authored-by: Michael (Mykhailo) Kravchenko --- src/components/withKeyboardState.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index 997f5db59619..e6853686c9b3 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -18,7 +18,9 @@ const keyboardStatePropTypes = { /** Whether the keyboard is open */ isKeyboardShown: PropTypes.bool.isRequired, }; + const KeyboardStateContext = createContext(null); + function KeyboardStateProvider(props: KeyboardStateProviderProps) { const {children} = props; const [isKeyboardShown, setIsKeyboardShown] = useState(false); From deb4a0842b366ed84a7619001c4fedcdfbc0f677 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Fri, 13 Oct 2023 08:29:39 +0200 Subject: [PATCH 003/231] Prettier applied. --- src/components/withKeyboardState.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index 997f5db59619..44e5960aaff2 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -1,16 +1,16 @@ import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactNode, useEffect, useState} from 'react'; import {Keyboard} from 'react-native'; import PropTypes from 'prop-types'; -import getComponentDisplayName from "../libs/getComponentDisplayName"; +import getComponentDisplayName from '../libs/getComponentDisplayName'; type KeyboardStateContextValue = { /** Whether the keyboard is open */ - isKeyboardShown : boolean + isKeyboardShown: boolean; }; type KeyboardStateProviderProps = { /* Actual content wrapped by this component */ - children: ReactNode + children: ReactNode; }; // TODO: Remove - left for backwards compatibility with existing components. @@ -39,7 +39,6 @@ function KeyboardStateProvider(props: KeyboardStateProviderProps) { return {children}; } - // eslint-disable-next-line @typescript-eslint/naming-convention export default function withKeyboardState(WrappedComponent: ComponentType<{ref: ForwardedRef}>) { const WithKeyboardState = forwardRef((props: Record, ref: React.ForwardedRef) => ( @@ -53,7 +52,8 @@ export default function withKeyboardState(WrappedComponent: ComponentType<{ref: ref={ref} /> )} - )); + + )); (WithKeyboardState as unknown as {displayName: string}).displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent as ComponentType)})`; return WithKeyboardState; } From 326e443c3f2868cca74f1cec9816f2c8b53c8e13 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 15 Nov 2023 12:42:02 +0100 Subject: [PATCH 004/231] ref: migrating TextInput to TS --- src/components/Icon/index.tsx | 2 + .../{index.native.js => index.native.tsx} | 0 .../BaseTextInput/{index.js => index.tsx} | 95 ++++++++------- .../TextInput/BaseTextInput/types.ts | 109 ++++++++++++++++++ .../TextInputLabel/TextInputLabelPropTypes.js | 25 ---- .../{index.native.js => index.native.tsx} | 16 ++- .../TextInputLabel/{index.js => index.tsx} | 12 +- .../TextInput/TextInputLabel/types.ts | 20 ++++ 8 files changed, 188 insertions(+), 91 deletions(-) rename src/components/TextInput/BaseTextInput/{index.native.js => index.native.tsx} (100%) rename src/components/TextInput/BaseTextInput/{index.js => index.tsx} (85%) create mode 100644 src/components/TextInput/BaseTextInput/types.ts delete mode 100644 src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js rename src/components/TextInput/TextInputLabel/{index.native.js => index.native.tsx} (76%) rename src/components/TextInput/TextInputLabel/{index.js => index.tsx} (67%) create mode 100644 src/components/TextInput/TextInputLabel/types.ts diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 022c740907ea..86e4a592045c 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -100,3 +100,5 @@ class Icon extends PureComponent { } export default Icon; + +export type {SrcProps}; diff --git a/src/components/TextInput/BaseTextInput/index.native.js b/src/components/TextInput/BaseTextInput/index.native.tsx similarity index 100% rename from src/components/TextInput/BaseTextInput/index.native.js rename to src/components/TextInput/BaseTextInput/index.native.tsx diff --git a/src/components/TextInput/BaseTextInput/index.js b/src/components/TextInput/BaseTextInput/index.tsx similarity index 85% rename from src/components/TextInput/BaseTextInput/index.js rename to src/components/TextInput/BaseTextInput/index.tsx index e643c6ff6b4f..8f5b8e4f5320 100644 --- a/src/components/TextInput/BaseTextInput/index.js +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,7 +1,6 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; -import _ from 'underscore'; +import {ActivityIndicator, Animated, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleSheet, TextInput, TextInputFocusEventData, View} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -22,21 +21,22 @@ import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; +import BaseTextInputProps from './types'; -function BaseTextInput(props) { - const initialValue = props.value || props.defaultValue || ''; - const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput(props: BaseTextInputProps) { + const initialValue = props.value ?? props.defaultValue ?? ''; + const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); - const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [height, setHeight] = useState(variables.componentSizeLarge); + const [width, setWidth] = useState(); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -47,7 +47,7 @@ function BaseTextInput(props) { } if (props.shouldDelayFocus) { - const focusTimeout = setTimeout(() => input.current.focus(), CONST.ANIMATED_TRANSITION); + const focusTimeout = setTimeout(() => input?.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } input.current.focus(); @@ -56,16 +56,16 @@ function BaseTextInput(props) { }, []); const animateLabel = useCallback( - (translateY, scale) => { + (translateY: number, scale: number) => { Animated.parallel([ Animated.spring(labelTranslateY, { toValue: translateY, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), Animated.spring(labelScale, { toValue: scale, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), ]).start(); @@ -74,7 +74,7 @@ function BaseTextInput(props) { ); const activateLabel = useCallback(() => { - const value = props.value || ''; + const value = props.value ?? ''; if (value.length < 0 || isLabelActive.current) { return; @@ -85,9 +85,9 @@ function BaseTextInput(props) { }, [animateLabel, props.value]); const deactivateLabel = useCallback(() => { - const value = props.value || ''; + const value = props.value ?? ''; - if (props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { return; } @@ -95,22 +95,22 @@ function BaseTextInput(props) { isLabelActive.current = false; }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value]); - const onFocus = (event) => { + const onFocus = (event: NativeSyntheticEvent) => { if (props.onFocus) { props.onFocus(event); } setIsFocused(true); }; - const onBlur = (event) => { + const onBlur = (event: NativeSyntheticEvent) => { if (props.onBlur) { props.onBlur(event); } setIsFocused(false); }; - const onPress = (event) => { - if (props.disabled) { + const onPress = (event?: GestureResponderEvent | KeyboardEvent) => { + if (!!props.disabled || !event) { return; } @@ -118,28 +118,28 @@ function BaseTextInput(props) { props.onPress(event); } - if (!event.isDefaultPrevented()) { - input.current.focus(); + if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { + input.current?.focus(); } }; const onLayout = useCallback( - (event) => { + (event: LayoutChangeEvent) => { if (!props.autoGrowHeight && props.multiline) { return; } const layout = event.nativeEvent.layout; - setWidth((prevWidth) => (props.autoGrowHeight ? layout.width : prevWidth)); - setHeight((prevHeight) => (!props.multiline ? layout.height : prevHeight)); + setWidth((prevWidth: number | undefined) => (props.autoGrowHeight ? layout.width : prevWidth)); + setHeight((prevHeight: number) => (!props.multiline ? layout.height : prevHeight)); }, [props.autoGrowHeight, props.multiline], ); // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value || ''; + const inputValue = props.value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -161,7 +161,7 @@ function BaseTextInput(props) { // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || !_.isEmpty(props.value)) { + if (props.value === undefined || !props.value) { return; } hasValueRef.current = false; @@ -169,17 +169,14 @@ function BaseTextInput(props) { /** * Set Value & activateLabel - * - * @param {String} value - * @memberof BaseTextInput */ - const setValue = (value) => { + const setValue = (value: string) => { if (props.onInputChange) { props.onInputChange(value); } - - Str.result(props.onChangeText, value); - + if (props.onChangeText) { + Str.result(props.onChangeText, value); + } if (value && value.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: @@ -192,7 +189,7 @@ function BaseTextInput(props) { }; const togglePasswordVisibility = useCallback(() => { - setPasswordHidden((prevPasswordHidden) => !prevPasswordHidden); + setPasswordHidden((prevPasswordHidden: boolean | undefined) => !prevPasswordHidden); }, []); // When adding a new prefix character, adjust this method to add expected character width. @@ -201,7 +198,7 @@ function BaseTextInput(props) { // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix) => { + const getCharacterPadding = (prefix: string) => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -212,20 +209,20 @@ function BaseTextInput(props) { // eslint-disable-next-line react/forbid-foreign-prop-types const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label.length); - const isReadOnly = _.isUndefined(props.readOnly) ? props.disabled : props.readOnly; + const hasLabel = Boolean(props.label?.length); + const isReadOnly = props.readOnly ?? props.disabled; const inputHelpText = props.errorText || props.hint; - const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; + const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; const textInputContainerStyles = StyleSheet.flatten([ styles.textInputContainer, - ...props.textInputContainerStyles, + props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), !props.hideFocusedState && isFocused && styles.borderColorFocus, - (props.hasError || props.errorText) && styles.borderColorDanger, + (!!props.hasError || !!props.errorText) && styles.borderColorDanger, props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, ]); - const isMultiline = props.multiline || props.autoGrowHeight; + const isMultiline = props.multiline ?? props.autoGrowHeight; /* To prevent text jumping caused by virtual DOM calculations on Safari and mobile Chrome, make sure to include the `lineHeight`. @@ -236,8 +233,8 @@ function BaseTextInput(props) { See https://github.com/Expensify/App/issues/13802 */ const lineHeight = useMemo(() => { - if ((Browser.isSafari() || Browser.isMobileChrome()) && _.isArray(props.inputStyle)) { - const lineHeightValue = _.find(props.inputStyle, (f) => f.lineHeight !== undefined); + if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(props.inputStyle)) { + const lineHeightValue = props.inputStyle.find((f) => f?.lineHeight !== undefined); if (lineHeightValue) { return lineHeightValue.lineHeight; } @@ -260,7 +257,7 @@ function BaseTextInput(props) { style={[ props.autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), !isMultiline && styles.componentHeightLarge, - ...props.containerStyles, + props.containerStyles, ]} > } e.preventDefault()} - accessibilityLabel={props.translate('common.visible')} + accessibilityLabel={props.translate?.('common.visible') ?? ''} > { let additionalWidth = 0; if (Browser.isMobileSafari() || Browser.isSafari()) { @@ -423,7 +420,5 @@ function BaseTextInput(props) { } BaseTextInput.displayName = 'BaseTextInput'; -BaseTextInput.propTypes = baseTextInputPropTypes.propTypes; -BaseTextInput.defaultProps = baseTextInputPropTypes.defaultProps; export default withLocalize(BaseTextInput); diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts new file mode 100644 index 000000000000..091a5d18379f --- /dev/null +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -0,0 +1,109 @@ +import React from 'react'; +import {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, TextInputProps, ViewStyle} from 'react-native'; +import {SrcProps} from '@components/Icon'; + +type CustomTextInputProps = { + /** Input label */ + label?: string; + + /** Name attribute for the input */ + name?: string; + + /** Input value */ + value?: string; + + /** Default value - used for non controlled inputs */ + defaultValue?: string; + + /** Input value placeholder */ + placeholder?: string; + + /** Error text to display */ + errorText: string | string[] | Record; + + /** Icon to display in right side of text input */ + icon: (props: SrcProps) => React.ReactNode; + + /** Customize the TextInput container */ + textInputContainerStyles: StyleProp; + + /** Customize the main container */ + containerStyles: StyleProp; + + /** input style */ + inputStyle: StyleProp; + + /** If present, this prop forces the label to remain in a position where it will not collide with input text */ + forceActiveLabel?: boolean; + + /** Should the input auto focus? */ + autoFocus?: boolean; + + /** Disable the virtual keyboard */ + disableKeyboard?: boolean; + + /** + * Autogrow input container length based on the entered text. + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrow?: boolean; + + /** + * Autogrow input container height based on the entered text + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrowHeight?: boolean; + + /** Hide the focus styles on TextInput */ + hideFocusedState?: boolean; + + /** Forward the inner ref */ + innerRef?: React.RefObject; + + /** Maximum characters allowed */ + maxLength?: number; + + /** Hint text to display below the TextInput */ + hint?: string; + + /** Prefix character */ + prefixCharacter?: string; + + /** Whether autoCorrect functionality should enable */ + autoCorrect?: boolean; + + /** Form props */ + /** The ID used to uniquely identify the input in a Form */ + inputID?: string; + + /** Saves a draft of the input value when used in a form */ + shouldSaveDraft?: boolean; + + /** Callback to update the value on Form when input is used in the Form component. */ + onInputChange?: (value: string) => void; + + /** Whether we should wait before focusing the TextInput, useful when using transitions */ + shouldDelayFocus?: boolean; + + /** Indicate whether pressing Enter on multiline input is allowed to submit the form. */ + submitOnEnter?: boolean; + + /** Indicate whether input is multiline */ + multiline?: boolean; + + /** Set the default value to the input if there is a valid saved value */ + shouldUseDefaultValue?: boolean; + + /** Indicate whether or not the input should prevent swipe actions in tabs */ + shouldInterceptSwipe?: boolean; + + hasError?: boolean; + onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; + isLoading?: boolean; + translate?: (key: string) => string; +}; + +type BaseTextInputProps = CustomTextInputProps & TextInputProps; +export default BaseTextInputProps; diff --git a/src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js b/src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js deleted file mode 100644 index 82b98f17d808..000000000000 --- a/src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js +++ /dev/null @@ -1,25 +0,0 @@ -import PropTypes from 'prop-types'; -import {Animated} from 'react-native'; - -const propTypes = { - /** Label */ - label: PropTypes.string.isRequired, - - /** Label vertical translate */ - labelTranslateY: PropTypes.instanceOf(Animated.Value).isRequired, - - /** Label scale */ - labelScale: PropTypes.instanceOf(Animated.Value).isRequired, - - /** Whether the label is currently active or not */ - isLabelActive: PropTypes.bool.isRequired, - - /** For attribute for label */ - for: PropTypes.string, -}; - -const defaultProps = { - for: '', -}; - -export {propTypes, defaultProps}; diff --git a/src/components/TextInput/TextInputLabel/index.native.js b/src/components/TextInput/TextInputLabel/index.native.tsx similarity index 76% rename from src/components/TextInput/TextInputLabel/index.native.js rename to src/components/TextInput/TextInputLabel/index.native.tsx index 51b231287b1f..7260e7bc9976 100644 --- a/src/components/TextInput/TextInputLabel/index.native.js +++ b/src/components/TextInput/TextInputLabel/index.native.tsx @@ -2,9 +2,9 @@ import React, {useState} from 'react'; import {Animated} from 'react-native'; import * as styleConst from '@components/TextInput/styleConst'; import styles from '@styles/styles'; -import * as TextInputLabelPropTypes from './TextInputLabelPropTypes'; +import TextInputLabelProps from './types'; -function TextInputLabel(props) { +function TextInputLabel({isLabelActive, label, labelScale, labelTranslateY}: TextInputLabelProps) { const [width, setWidth] = useState(0); return ( @@ -16,29 +16,27 @@ function TextInputLabel(props) { style={[ styles.textInputLabel, styles.textInputLabelTransformation( - props.labelTranslateY, - props.labelScale.interpolate({ + labelTranslateY, + labelScale.interpolate({ inputRange: [styleConst.ACTIVE_LABEL_SCALE, styleConst.INACTIVE_LABEL_SCALE], outputRange: [-(width - width * styleConst.ACTIVE_LABEL_SCALE) / 2, 0], }), - props.labelScale, + labelScale, ), // If the label is active but the width is not ready yet, the above translateX value will be 0, // making the label sits at the top center instead of the top left of the input. To solve it // move the label by a percentage value with left style as translateX doesn't support percentage value. width === 0 && - props.isLabelActive && { + isLabelActive && { left: `${-((1 - styleConst.ACTIVE_LABEL_SCALE) * 100) / 2}%`, }, ]} > - {props.label} + {label} ); } -TextInputLabel.propTypes = TextInputLabelPropTypes.propTypes; -TextInputLabel.defaultProps = TextInputLabelPropTypes.defaultProps; TextInputLabel.displayName = 'TextInputLabel'; export default TextInputLabel; diff --git a/src/components/TextInput/TextInputLabel/index.js b/src/components/TextInput/TextInputLabel/index.tsx similarity index 67% rename from src/components/TextInput/TextInputLabel/index.js rename to src/components/TextInput/TextInputLabel/index.tsx index b49635b91d96..1db173c24d0c 100644 --- a/src/components/TextInput/TextInputLabel/index.js +++ b/src/components/TextInput/TextInputLabel/index.tsx @@ -1,17 +1,17 @@ import React, {useEffect, useRef} from 'react'; -import {Animated} from 'react-native'; +import {Animated, Text} from 'react-native'; import styles from '@styles/styles'; import CONST from '@src/CONST'; -import {defaultProps, propTypes} from './TextInputLabelPropTypes'; +import TextInputLabelProps from './types'; -function TextInputLabel({for: inputId, label, labelTranslateY, labelScale}) { - const labelRef = useRef(null); +function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: TextInputLabelProps) { + const labelRef = useRef(null); useEffect(() => { if (!inputId || !labelRef.current) { return; } - labelRef.current.setAttribute('for', inputId); + (labelRef.current as unknown as HTMLFormElement).setAttribute('for', inputId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -27,7 +27,5 @@ function TextInputLabel({for: inputId, label, labelTranslateY, labelScale}) { } TextInputLabel.displayName = 'TextInputLabel'; -TextInputLabel.propTypes = propTypes; -TextInputLabel.defaultProps = defaultProps; export default React.memo(TextInputLabel); diff --git a/src/components/TextInput/TextInputLabel/types.ts b/src/components/TextInput/TextInputLabel/types.ts new file mode 100644 index 000000000000..6f85eef18f42 --- /dev/null +++ b/src/components/TextInput/TextInputLabel/types.ts @@ -0,0 +1,20 @@ +import {Animated} from 'react-native'; + +type TextInputLabelProps = { + /** Label */ + label: string; + + /** Label vertical translate */ + labelTranslateY: Animated.Value; + + /** Label scale */ + labelScale: Animated.Value; + + /** Whether the label is currently active or not */ + isLabelActive: boolean; + + /** For attribute for label */ + for?: string; +}; + +export default TextInputLabelProps; From 255058f4adf1540438b10807ce5a8eaa2f857318 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 15 Nov 2023 16:39:04 +0100 Subject: [PATCH 005/231] fix: fixing types problems in BaseTextInput for Web --- .../TextInput/BaseTextInput/index.native.tsx | 62 ++++++++++--------- .../TextInput/BaseTextInput/index.tsx | 45 ++++++++++---- .../TextInput/BaseTextInput/types.ts | 18 +++--- 3 files changed, 75 insertions(+), 50 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 5c3e19a2d94c..89c8f67a1026 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,6 +1,6 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; +import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; import _ from 'underscore'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -22,10 +22,11 @@ import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; +import BaseTextInputProps from './types'; -function BaseTextInput(props) { - const initialValue = props.value || props.defaultValue || ''; - const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput(props: BaseTextInputProps) { + const initialValue = props.value ?? props.defaultValue ?? ''; + const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); @@ -36,7 +37,7 @@ function BaseTextInput(props) { const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -47,7 +48,7 @@ function BaseTextInput(props) { } if (props.shouldDelayFocus) { - const focusTimeout = setTimeout(() => input.current.focus(), CONST.ANIMATED_TRANSITION); + const focusTimeout = setTimeout(() => input.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } input.current.focus(); @@ -56,16 +57,16 @@ function BaseTextInput(props) { }, []); const animateLabel = useCallback( - (translateY, scale) => { + (translateY: number, scale: number) => { Animated.parallel([ Animated.spring(labelTranslateY, { toValue: translateY, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), Animated.spring(labelScale, { toValue: scale, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), ]).start(); @@ -85,9 +86,9 @@ function BaseTextInput(props) { }, [animateLabel, props.value]); const deactivateLabel = useCallback(() => { - const value = props.value || ''; + const value = props.value ?? ''; - if (props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { return; } @@ -119,7 +120,7 @@ function BaseTextInput(props) { } if (!event.isDefaultPrevented()) { - input.current.focus(); + input.current?.focus(); } }; @@ -139,7 +140,7 @@ function BaseTextInput(props) { // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value || ''; + const inputValue = props.value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -169,9 +170,6 @@ function BaseTextInput(props) { /** * Set Value & activateLabel - * - * @param {String} value - * @memberof BaseTextInput */ const setValue = (value) => { if (props.onInputChange) { @@ -201,7 +199,7 @@ function BaseTextInput(props) { // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix) => { + const getCharacterPadding = (prefix: string) => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -212,20 +210,21 @@ function BaseTextInput(props) { // eslint-disable-next-line react/forbid-foreign-prop-types const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label.length); + const hasLabel = Boolean(props.label?.length); const isReadOnly = _.isUndefined(props.readOnly) ? props.disabled : props.readOnly; const inputHelpText = props.errorText || props.hint; - const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; + const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; + console.log('maxHeight', maxHeight); const textInputContainerStyles = StyleSheet.flatten([ styles.textInputContainer, - ...props.textInputContainerStyles, + props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), !props.hideFocusedState && isFocused && styles.borderColorFocus, - (props.hasError || props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, + (!!props.hasError || props.errorText) && styles.borderColorDanger, + props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); - const isMultiline = props.multiline || props.autoGrowHeight; + const isMultiline = !!props.multiline || props.autoGrowHeight; return ( <> @@ -241,7 +240,7 @@ function BaseTextInput(props) { style={[ props.autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), !isMultiline && styles.componentHeightLarge, - ...props.containerStyles, + props.containerStyles, ]} > } - {!_.isEmpty(inputHelpText) && ( + {!inputHelpText && ( )} @@ -375,12 +374,17 @@ function BaseTextInput(props) { This text view is used to calculate width or height of the input value given textStyle in this component. This Text component is intentionally positioned out of the screen. */} - {(props.autoGrow || props.autoGrowHeight) && ( + {(!!props.autoGrow || props.autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 { setTextInputWidth(e.nativeEvent.layout.width); setTextInputHeight(e.nativeEvent.layout.height); diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 8f5b8e4f5320..77b2458c6b41 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,6 +1,19 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleSheet, TextInput, TextInputFocusEventData, View} from 'react-native'; +import { + ActivityIndicator, + Animated, + FlexStyle, + GestureResponderEvent, + LayoutChangeEvent, + NativeSyntheticEvent, + StyleProp, + StyleSheet, + TextInput, + TextInputFocusEventData, + View, + ViewStyle, +} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -207,20 +220,19 @@ function BaseTextInput(props: BaseTextInputProps) { } }; - // eslint-disable-next-line react/forbid-foreign-prop-types const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); const hasLabel = Boolean(props.label?.length); const isReadOnly = props.readOnly ?? props.disabled; const inputHelpText = props.errorText || props.hint; const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles = StyleSheet.flatten([ + const textInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), !props.hideFocusedState && isFocused && styles.borderColorFocus, (!!props.hasError || !!props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, + props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); const isMultiline = props.multiline ?? props.autoGrowHeight; @@ -234,8 +246,8 @@ function BaseTextInput(props: BaseTextInputProps) { const lineHeight = useMemo(() => { if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(props.inputStyle)) { - const lineHeightValue = props.inputStyle.find((f) => f?.lineHeight !== undefined); - if (lineHeightValue) { + const lineHeightValue = props.inputStyle.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); + if (lineHeightValue && 'lineHeight' in lineHeightValue) { return lineHeightValue.lineHeight; } } @@ -253,7 +265,8 @@ function BaseTextInput(props: BaseTextInputProps) { { if (typeof props.innerRef === 'function') { props.innerRef(ref); - } else if (props.innerRef && _.has(props.innerRef, 'current')) { + } else if (props.innerRef && 'current' in props.innerRef) { // eslint-disable-next-line no-param-reassign props.innerRef.current = ref; } + // @ts-expect-error We need to reassign this ref to the input ref input.current = ref; }} // eslint-disable-next-line @@ -318,7 +332,7 @@ function BaseTextInput(props: BaseTextInputProps) { styles.w100, props.inputStyle, (!hasLabel || isMultiline) && styles.pv0, - props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), + !!props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), props.secureTextEntry && styles.secureInput, // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear @@ -330,7 +344,7 @@ function BaseTextInput(props: BaseTextInputProps) { !isMultiline && Browser.isMobileChrome() && {boxSizing: 'content-box', height: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), + props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. props.disabled && styles.textInputDisabled, styles.pointerEventsAuto, @@ -383,9 +397,9 @@ function BaseTextInput(props: BaseTextInputProps) { - {!_.isEmpty(inputHelpText) && ( + {!inputHelpText && ( )} @@ -401,7 +415,12 @@ function BaseTextInput(props: BaseTextInputProps) { // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 { let additionalWidth = 0; if (Browser.isMobileSafari() || Browser.isSafari()) { diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 091a5d18379f..de453802b2b6 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,8 +1,9 @@ -import React from 'react'; -import {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, TextInputProps, ViewStyle} from 'react-native'; +import React, {Component} from 'react'; +import {FlexStyle, GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import {SrcProps} from '@components/Icon'; -type CustomTextInputProps = { +type CustomBaseTextInputProps = { /** Input label */ label?: string; @@ -25,13 +26,13 @@ type CustomTextInputProps = { icon: (props: SrcProps) => React.ReactNode; /** Customize the TextInput container */ - textInputContainerStyles: StyleProp; + textInputContainerStyles: StyleProp; /** Customize the main container */ - containerStyles: StyleProp; + containerStyles: StyleProp; /** input style */ - inputStyle: StyleProp; + inputStyle: StyleProp; /** If present, this prop forces the label to remain in a position where it will not collide with input text */ forceActiveLabel?: boolean; @@ -60,7 +61,7 @@ type CustomTextInputProps = { hideFocusedState?: boolean; /** Forward the inner ref */ - innerRef?: React.RefObject; + innerRef?: React.ForwardedRef, unknown, unknown> | null>; /** Maximum characters allowed */ maxLength?: number; @@ -105,5 +106,6 @@ type CustomTextInputProps = { translate?: (key: string) => string; }; -type BaseTextInputProps = CustomTextInputProps & TextInputProps; +type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; export default BaseTextInputProps; +export type {CustomBaseTextInputProps}; From b573de9b56fe0cb4516c7d3fd744699324bc4685 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 16 Nov 2023 11:40:08 +0100 Subject: [PATCH 006/231] fix: move inputProps const into native ones --- .../TextInput/BaseTextInput/index.native.tsx | 239 +++++++++++------- .../TextInput/BaseTextInput/index.tsx | 5 +- .../{index.native.js => index.native.tsx} | 0 .../TextInput/{index.js => index.tsx} | 0 .../{styleConst.js => styleConst.ts} | 0 src/components/TextInput/types.ts | 0 src/libs/getSecureEntryKeyboardType/types.ts | 4 +- 7 files changed, 149 insertions(+), 99 deletions(-) rename src/components/TextInput/{index.native.js => index.native.tsx} (100%) rename src/components/TextInput/{index.js => index.tsx} (100%) rename src/components/TextInput/{styleConst.js => styleConst.ts} (100%) create mode 100644 src/components/TextInput/types.ts diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 89c8f67a1026..d696a97232c6 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,7 +1,19 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; -import _ from 'underscore'; +import { + ActivityIndicator, + Animated, + FlexStyle, + GestureResponderEvent, + LayoutChangeEvent, + NativeSyntheticEvent, + StyleProp, + StyleSheet, + TextInput, + TextInputFocusEventData, + View, + ViewStyle, +} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -24,16 +36,50 @@ import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; -function BaseTextInput(props: BaseTextInputProps) { - const initialValue = props.value ?? props.defaultValue ?? ''; - const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput({ + label = '', + name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + innerRef = () => {}, + maxLength = null, + hint = '', + shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + translate, + ...inputProps +}: BaseTextInputProps) { + const {hasError = false} = inputProps; + console.log({inputProps}); + const initialValue = value ?? defaultValue ?? ''; + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); const [isFocused, setIsFocused] = useState(false); - const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); + const [passwordHidden, setPasswordHidden] = useState(inputProps.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); - const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [height, setHeight] = useState(variables.componentSizeLarge); + const [width, setWidth] = useState(); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; @@ -43,11 +89,11 @@ function BaseTextInput(props: BaseTextInputProps) { // AutoFocus which only works on mount: useEffect(() => { // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 - if (!props.autoFocus || !input.current) { + if (!autoFocus || !input.current) { return; } - if (props.shouldDelayFocus) { + if (shouldDelayFocus) { const focusTimeout = setTimeout(() => input.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } @@ -75,72 +121,72 @@ function BaseTextInput(props: BaseTextInputProps) { ); const activateLabel = useCallback(() => { - const value = props.value || ''; + const inputValue = value ?? ''; - if (value.length < 0 || isLabelActive.current) { + if (inputValue.length < 0 || isLabelActive.current) { return; } animateLabel(styleConst.ACTIVE_LABEL_TRANSLATE_Y, styleConst.ACTIVE_LABEL_SCALE); isLabelActive.current = true; - }, [animateLabel, props.value]); + }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - const value = props.value ?? ''; + const inputValue = value ?? ''; - if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!forceActiveLabel || inputValue.length !== 0 || prefixCharacter) { return; } animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); isLabelActive.current = false; - }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value]); + }, [animateLabel, forceActiveLabel, prefixCharacter, value]); - const onFocus = (event) => { - if (props.onFocus) { - props.onFocus(event); + const onFocus = (event: NativeSyntheticEvent) => { + if (inputProps.onFocus) { + inputProps.onFocus(event); } setIsFocused(true); }; - const onBlur = (event) => { - if (props.onBlur) { - props.onBlur(event); + const onBlur = (event: NativeSyntheticEvent) => { + if (inputProps.onBlur) { + inputProps.onBlur(event); } setIsFocused(false); }; - const onPress = (event) => { - if (props.disabled) { + const onPress = (event?: GestureResponderEvent | KeyboardEvent) => { + if (!!inputProps.disabled || !event) { return; } - if (props.onPress) { - props.onPress(event); + if (inputProps.onPress) { + inputProps.onPress(event); } - if (!event.isDefaultPrevented()) { + if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { input.current?.focus(); } }; const onLayout = useCallback( - (event) => { - if (!props.autoGrowHeight && props.multiline) { + (event: LayoutChangeEvent) => { + if (!autoGrowHeight && multiline) { return; } const layout = event.nativeEvent.layout; - setWidth((prevWidth) => (props.autoGrowHeight ? layout.width : prevWidth)); - setHeight((prevHeight) => (!props.multiline ? layout.height : prevHeight)); + setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, - [props.autoGrowHeight, props.multiline], + [autoGrowHeight, multiline], ); // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value ?? ''; + const inputValue = value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -162,26 +208,28 @@ function BaseTextInput(props: BaseTextInputProps) { // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || !_.isEmpty(props.value)) { + if (value === undefined || value) { return; } hasValueRef.current = false; - }, [props.value]); + }, [value]); /** * Set Value & activateLabel */ - const setValue = (value) => { - if (props.onInputChange) { - props.onInputChange(value); + const setValue = (newValue: string) => { + if (onInputChange) { + onInputChange(newValue); } - Str.result(props.onChangeText, value); + if (inputProps.onChangeText) { + Str.result(inputProps.onChangeText, newValue); + } - if (value && value.length > 0) { + if (newValue && newValue.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: - if (props.value === undefined) { + if (value === undefined) { activateLabel(); } } else { @@ -209,38 +257,38 @@ function BaseTextInput(props: BaseTextInputProps) { }; // eslint-disable-next-line react/forbid-foreign-prop-types - const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label?.length); - const isReadOnly = _.isUndefined(props.readOnly) ? props.disabled : props.readOnly; - const inputHelpText = props.errorText || props.hint; - const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; - const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - console.log('maxHeight', maxHeight); - const textInputContainerStyles = StyleSheet.flatten([ + // const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); + const hasLabel = Boolean(label?.length); + const isReadOnly = inputProps.readOnly ?? inputProps.disabled; + const inputHelpText = errorText || hint; + const placeholderValue = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; + const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, - props.textInputContainerStyles, - props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), - !props.hideFocusedState && isFocused && styles.borderColorFocus, - (!!props.hasError || props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, + textInputContainerStyles, + autoGrow && StyleUtils.getWidthStyle(textInputWidth), + !hideFocusedState && isFocused && styles.borderColorFocus, + (!!hasError || errorText) && styles.borderColorDanger, + autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); - const isMultiline = !!props.multiline || props.autoGrowHeight; + const isMultiline = !!multiline || autoGrowHeight; return ( <> {hasLabel ? ( @@ -261,88 +309,89 @@ function BaseTextInput(props: BaseTextInputProps) { {isMultiline && } ) : null} - {Boolean(props.prefixCharacter) && ( + {Boolean(prefixCharacter) && ( - {props.prefixCharacter} + {prefixCharacter} )} { - if (typeof props.innerRef === 'function') { - props.innerRef(ref); - } else if (props.innerRef && _.has(props.innerRef, 'current')) { + if (typeof innerRef === 'function') { + innerRef(ref); + } else if (innerRef && 'current' in innerRef) { // eslint-disable-next-line no-param-reassign - props.innerRef.current = ref; + innerRef.current = ref; } + // @ts-expect-error We need to reassign this ref to the input ref input.current = ref; }} // eslint-disable-next-line {...inputProps} - autoCorrect={props.secureTextEntry ? false : props.autoCorrect} - placeholder={placeholder} + autoCorrect={inputProps.secureTextEntry ? false : autoCorrect} + placeholder={placeholderValue} placeholderTextColor={themeColors.placeholderText} underlineColorAndroid="transparent" style={[ styles.flex1, styles.w100, - props.inputStyle, + inputStyle, (!hasLabel || isMultiline) && styles.pv0, - props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), - props.secureTextEntry && styles.secureInput, + !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(prefixCharacter) + styles.pl1.paddingLeft), + inputProps.secureTextEntry && styles.secureInput, !isMultiline && {height, lineHeight: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), // Add disabled color theme when field is not editable. - props.disabled && styles.textInputDisabled, + inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, ]} multiline={isMultiline} - maxLength={props.maxLength} + maxLength={maxLength} onFocus={onFocus} onBlur={onBlur} onChangeText={setValue} secureTextEntry={passwordHidden} - onPressOut={props.onPress} - showSoftInputOnFocus={!props.disableKeyboard} - keyboardType={getSecureEntryKeyboardType(props.keyboardType, props.secureTextEntry, passwordHidden)} - inputMode={!props.disableKeyboard ? props.inputMode : CONST.INPUT_MODE.NONE} - value={props.value} - selection={props.selection} + onPressOut={inputProps.onPress} + showSoftInputOnFocus={!disableKeyboard} + keyboardType={getSecureEntryKeyboardType(inputProps.keyboardType, inputProps.secureTextEntry ?? false, passwordHidden ?? false)} + inputMode={!disableKeyboard ? inputProps.inputMode : CONST.INPUT_MODE.NONE} + value={value} + selection={inputProps.selection} readOnly={isReadOnly} - defaultValue={props.defaultValue} + defaultValue={defaultValue} // FormSubmit Enter key handler does not have access to direct props. // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: isMultiline && props.submitOnEnter}} + dataSet={{submitOnEnter: isMultiline && submitOnEnter}} /> - {props.isLoading && ( + {inputProps.isLoading && ( )} - {Boolean(props.secureTextEntry) && ( + {Boolean(inputProps.secureTextEntry) && ( e.preventDefault()} - accessibilityLabel={props.translate('common.visible')} + accessibilityLabel={translate?.('common.visible') ?? ''} > )} - {!props.secureTextEntry && Boolean(props.icon) && ( + {!inputProps.secureTextEntry && Boolean(icon) && ( @@ -363,7 +412,7 @@ function BaseTextInput(props: BaseTextInputProps) { {!inputHelpText && ( )} @@ -374,14 +423,14 @@ function BaseTextInput(props: BaseTextInputProps) { This text view is used to calculate width or height of the input value given textStyle in this component. This Text component is intentionally positioned out of the screen. */} - {(!!props.autoGrow || props.autoGrowHeight) && ( + {(!!autoGrow || autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 {/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */} - {props.value ? `${props.value}${props.value.endsWith('\n') ? '\u200B' : ''}` : props.placeholder} + {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} )} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 77b2458c6b41..547e11b6cbb6 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -3,7 +3,6 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import { ActivityIndicator, Animated, - FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, @@ -174,7 +173,7 @@ function BaseTextInput(props: BaseTextInputProps) { // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || !props.value) { + if (props.value === undefined || props.value) { return; } hasValueRef.current = false; @@ -226,7 +225,7 @@ function BaseTextInput(props: BaseTextInputProps) { const inputHelpText = props.errorText || props.hint; const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles: StyleProp = StyleSheet.flatten([ + const textInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), diff --git a/src/components/TextInput/index.native.js b/src/components/TextInput/index.native.tsx similarity index 100% rename from src/components/TextInput/index.native.js rename to src/components/TextInput/index.native.tsx diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.tsx similarity index 100% rename from src/components/TextInput/index.js rename to src/components/TextInput/index.tsx diff --git a/src/components/TextInput/styleConst.js b/src/components/TextInput/styleConst.ts similarity index 100% rename from src/components/TextInput/styleConst.js rename to src/components/TextInput/styleConst.ts diff --git a/src/components/TextInput/types.ts b/src/components/TextInput/types.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/libs/getSecureEntryKeyboardType/types.ts b/src/libs/getSecureEntryKeyboardType/types.ts index fe79440e3109..750c84133ea2 100644 --- a/src/libs/getSecureEntryKeyboardType/types.ts +++ b/src/libs/getSecureEntryKeyboardType/types.ts @@ -1,3 +1,5 @@ -type GetSecureEntryKeyboardType = (keyboardType: string, secureTextEntry: boolean, passwordHidden: boolean) => string; +import {KeyboardTypeOptions} from 'react-native'; + +type GetSecureEntryKeyboardType = (keyboardType: KeyboardTypeOptions | undefined, secureTextEntry: boolean, passwordHidden: boolean) => KeyboardTypeOptions | undefined; export default GetSecureEntryKeyboardType; From b568d32025186f0cd4ce39fcb1d211249662dbc1 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 16 Nov 2023 14:48:31 +0100 Subject: [PATCH 007/231] fix: destructure props --- .../TextInput/BaseTextInput/index.native.tsx | 2 - .../TextInput/BaseTextInput/index.tsx | 44 ++++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index d696a97232c6..bde3f880cfbf 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -66,11 +66,9 @@ function BaseTextInput({ autoCorrect = true, prefixCharacter, inputID, - translate, ...inputProps }: BaseTextInputProps) { const {hasError = false} = inputProps; - console.log({inputProps}); const initialValue = value ?? defaultValue ?? ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 547e11b6cbb6..4f83e87084ed 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -35,12 +35,44 @@ import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; -function BaseTextInput(props: BaseTextInputProps) { - const initialValue = props.value ?? props.defaultValue ?? ''; - const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput({ + label = '', + name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + innerRef = () => {}, + maxLength = null, + hint = '', + shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + ...inputProps +}: BaseTextInputProps) { + const {hasError = false} = inputProps; + const initialValue = value ?? defaultValue ?? ''; + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); const [isFocused, setIsFocused] = useState(false); - const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); + const [passwordHidden, setPasswordHidden] = useState(inputProps.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); @@ -54,11 +86,11 @@ function BaseTextInput(props: BaseTextInputProps) { // AutoFocus which only works on mount: useEffect(() => { // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 - if (!props.autoFocus || !input.current) { + if (!autoFocus || !input.current) { return; } - if (props.shouldDelayFocus) { + if (shouldDelayFocus) { const focusTimeout = setTimeout(() => input?.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } From e90c503a36c75447619c9d8ef26a76108ecbecd0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 17 Nov 2023 10:54:41 +0100 Subject: [PATCH 008/231] fix: fixing types --- .../TextInput/BaseTextInput/index.tsx | 163 +++++++++--------- src/components/TextInput/index.tsx | 52 +++--- 2 files changed, 104 insertions(+), 111 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 4f83e87084ed..f86e33d1eab9 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import { ActivityIndicator, Animated, + FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, @@ -32,7 +33,6 @@ import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; function BaseTextInput({ @@ -118,48 +118,48 @@ function BaseTextInput({ ); const activateLabel = useCallback(() => { - const value = props.value ?? ''; + const newValue = value ?? ''; - if (value.length < 0 || isLabelActive.current) { + if (newValue.length < 0 || isLabelActive.current) { return; } animateLabel(styleConst.ACTIVE_LABEL_TRANSLATE_Y, styleConst.ACTIVE_LABEL_SCALE); isLabelActive.current = true; - }, [animateLabel, props.value]); + }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - const value = props.value ?? ''; + const newValue = value ?? ''; - if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!forceActiveLabel || newValue.length !== 0 || prefixCharacter) { return; } animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); isLabelActive.current = false; - }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value]); + }, [animateLabel, forceActiveLabel,prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { - if (props.onFocus) { - props.onFocus(event); + if (inputProps.onFocus) { + onFocus(event); } setIsFocused(true); }; const onBlur = (event: NativeSyntheticEvent) => { - if (props.onBlur) { - props.onBlur(event); + if (inputProps.onBlur) { + inputProps.onBlur(event); } setIsFocused(false); }; const onPress = (event?: GestureResponderEvent | KeyboardEvent) => { - if (!!props.disabled || !event) { + if (!!inputProps.disabled || !event) { return; } - if (props.onPress) { - props.onPress(event); + if (inputProps.onPress) { + inputProps.onPress(event); } if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { @@ -169,21 +169,21 @@ function BaseTextInput({ const onLayout = useCallback( (event: LayoutChangeEvent) => { - if (!props.autoGrowHeight && props.multiline) { + if (!autoGrowHeight && multiline) { return; } const layout = event.nativeEvent.layout; - setWidth((prevWidth: number | undefined) => (props.autoGrowHeight ? layout.width : prevWidth)); - setHeight((prevHeight: number) => (!props.multiline ? layout.height : prevHeight)); + setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, - [props.autoGrowHeight, props.multiline], + [autoGrowHeight, multiline], ); // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value ?? ''; + const inputValue = value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -205,26 +205,26 @@ function BaseTextInput({ // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || props.value) { + if (value === undefined || value) { return; } hasValueRef.current = false; - }, [props.value]); + }, [value]); /** * Set Value & activateLabel */ - const setValue = (value: string) => { - if (props.onInputChange) { - props.onInputChange(value); + const setValue = (newValue: string) => { + if (onInputChange) { + onInputChange(newValue); } - if (props.onChangeText) { - Str.result(props.onChangeText, value); + if (inputProps.onChangeText) { + Str.result(inputProps.onChangeText, newValue); } - if (value && value.length > 0) { + if (newValue && newValue.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: - if (props.value === undefined) { + if (value === undefined) { activateLabel(); } } else { @@ -251,21 +251,20 @@ function BaseTextInput({ } }; - const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label?.length); - const isReadOnly = props.readOnly ?? props.disabled; - const inputHelpText = props.errorText || props.hint; - const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; - const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles: StyleProp = StyleSheet.flatten([ + const hasLabel = Boolean(label?.length); + const isReadOnly = inputProps.readOnly ?? inputProps.disabled; + const inputHelpText = errorText || hint; + const newPlaceholder = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; + const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; + const textInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, - props.textInputContainerStyles, - props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), - !props.hideFocusedState && isFocused && styles.borderColorFocus, - (!!props.hasError || !!props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, + textInputContainerStyles, + autoGrow && StyleUtils.getWidthStyle(textInputWidth), + !hideFocusedState && isFocused && styles.borderColorFocus, + (!!hasError || !!errorText) && styles.borderColorDanger, + autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); - const isMultiline = props.multiline ?? props.autoGrowHeight; + const isMultiline = multiline ?? autoGrowHeight; /* To prevent text jumping caused by virtual DOM calculations on Safari and mobile Chrome, make sure to include the `lineHeight`. @@ -276,32 +275,32 @@ function BaseTextInput({ See https://github.com/Expensify/App/issues/13802 */ const lineHeight = useMemo(() => { - if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(props.inputStyle)) { - const lineHeightValue = props.inputStyle.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); + if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(inputStyle)) { + const lineHeightValue = inputStyle?.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); if (lineHeightValue && 'lineHeight' in lineHeightValue) { return lineHeightValue.lineHeight; } } return undefined; - }, [props.inputStyle]); + }, [inputStyle]); return ( <> {hasLabel ? ( @@ -322,49 +321,49 @@ function BaseTextInput({ {isMultiline && } ) : null} - {Boolean(props.prefixCharacter) && ( + {Boolean(prefixCharacter) && ( - {props.prefixCharacter} + {prefixCharacter} )} { - if (typeof props.innerRef === 'function') { - props.innerRef(ref); - } else if (props.innerRef && 'current' in props.innerRef) { + if (typeof innerRef === 'function') { + innerRef(ref); + } else if (innerRef && 'current' in innerRef) { // eslint-disable-next-line no-param-reassign - props.innerRef.current = ref; + innerRef.current = ref; } // @ts-expect-error We need to reassign this ref to the input ref input.current = ref; }} // eslint-disable-next-line {...inputProps} - autoCorrect={props.secureTextEntry ? false : props.autoCorrect} - placeholder={placeholder} + autoCorrect={inputProps.secureTextEntry ? false : autoCorrect} + placeholder={newPlaceholder} placeholderTextColor={themeColors.placeholderText} underlineColorAndroid="transparent" style={[ styles.flex1, styles.w100, - props.inputStyle, + inputStyle, (!hasLabel || isMultiline) && styles.pv0, - !!props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), - props.secureTextEntry && styles.secureInput, + !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), + inputProps.secureTextEntry && styles.secureInput, // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear // once it exceeds the input space (See https://github.com/Expensify/App/issues/13802) @@ -375,41 +374,41 @@ function BaseTextInput({ !isMultiline && Browser.isMobileChrome() && {boxSizing: 'content-box', height: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. - props.disabled && styles.textInputDisabled, + inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, ]} multiline={isMultiline} - maxLength={props.maxLength} + maxLength={maxLength} onFocus={onFocus} onBlur={onBlur} onChangeText={setValue} secureTextEntry={passwordHidden} - onPressOut={props.onPress} - showSoftInputOnFocus={!props.disableKeyboard} - inputMode={props.inputMode} - value={props.value} - selection={props.selection} + onPressOut={inputProps.onPress} + showSoftInputOnFocus={!disableKeyboard} + inputMode={inputProps.inputMode} + value={value} + selection={inputProps.selection} readOnly={isReadOnly} - defaultValue={props.defaultValue} + defaultValue={defaultValue} // FormSubmit Enter key handler does not have access to direct props. // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: isMultiline && props.submitOnEnter}} + dataSet={{submitOnEnter: isMultiline && submitOnEnter}} /> - {props.isLoading && ( + {inputProps.isLoading && ( )} - {Boolean(props.secureTextEntry) && ( + {Boolean(inputProps.secureTextEntry) && ( e.preventDefault()} - accessibilityLabel={props.translate?.('common.visible') ?? ''} + accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} > )} - {!props.secureTextEntry && Boolean(props.icon) && ( + {!inputProps.secureTextEntry && Boolean(icon) && ( @@ -430,7 +429,7 @@ function BaseTextInput({ {!inputHelpText && ( )} @@ -441,14 +440,14 @@ function BaseTextInput({ This text view is used to calculate width or height of the input value given textStyle in this component. This Text component is intentionally positioned out of the screen. */} - {(!!props.autoGrow || props.autoGrowHeight) && ( + {(!!autoGrow || autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 {/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */} - {props.value ? `${props.value}${props.value.endsWith('\n') ? '\u200B' : ''}` : props.placeholder} + {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} )} diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 044399ec6e11..c8dc21b97310 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,26 +1,26 @@ -import React, {useEffect, useRef} from 'react'; -import _ from 'underscore'; +import React, {ForwardedRef, useEffect, useRef} from 'react'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; import styles from '@styles/styles'; import BaseTextInput from './BaseTextInput'; import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; +import BaseTextInputProps from './BaseTextInput/types'; import * as styleConst from './styleConst'; -function TextInput(props) { - const textInputRef = useRef(null); - const removeVisibilityListenerRef = useRef(null); +function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { + const textInputRef = useRef(null); + const removeVisibilityListenerRef = useRef<() => void>(null); useEffect(() => { if (props.disableKeyboard) { - textInputRef.current.setAttribute('inputmode', 'none'); + textInputRef.current?.setAttribute('inputmode', 'none'); } if (props.name) { - textInputRef.current.setAttribute('name', props.name); + textInputRef.current?.setAttribute('name', props.name); } - + // @ts-expect-error We need to reassign this ref to the input ref removeVisibilityListenerRef.current = Visibility.onVisibilityChange(() => { if (!Browser.isMobileChrome() || !Visibility.isVisible() || !textInputRef.current || DomUtils.getActiveElement() !== textInputRef.current) { return; @@ -38,33 +38,37 @@ function TextInput(props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isLabeledMultiline = Boolean(props.label.length) && props.multiline; + const isLabeledMultiline = Boolean(props.label?.length) && props.multiline; const labelAnimationStyle = { + // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-translate-y': `${styleConst.ACTIVE_LABEL_TRANSLATE_Y}px`, + // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-scale': `${styleConst.ACTIVE_LABEL_SCALE}`, + // eslint-disable-next-line @typescript-eslint/naming-convention '--label-transition-duration': `${styleConst.LABEL_ANIMATION_DURATION}ms`, }; return ( { + innerRef={(el: HTMLFormElement | null) => { + // @ts-expect-error We need to reassign this ref to the input ref textInputRef.current = el; - if (!props.innerRef) { + if (!ref) { return; } - if (_.isFunction(props.innerRef)) { - props.innerRef(el); + if (typeof ref === 'function') { + ref(el); return; } // eslint-disable-next-line no-param-reassign - props.innerRef.current = el; + ref.current = el; }} - inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, ...props.inputStyle]} - textInputContainerStyles={[labelAnimationStyle, ...props.textInputContainerStyles]} + inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, props.inputStyle]} + textInputContainerStyles={[labelAnimationStyle, props.textInputContainerStyles]} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} /> ); } @@ -73,14 +77,4 @@ TextInput.displayName = 'TextInput'; TextInput.propTypes = baseTextInputPropTypes.propTypes; TextInput.defaultProps = baseTextInputPropTypes.defaultProps; -const TextInputWithRef = React.forwardRef((props, ref) => ( - -)); - -TextInputWithRef.displayName = 'TextInputWithRef'; - -export default TextInputWithRef; +export default React.forwardRef(TextInput); From 6a3a27004030f0d47c2ebff20974beb23190a4e4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 17 Nov 2023 15:38:55 +0100 Subject: [PATCH 009/231] fix: resolving types issues --- .../TextInput/BaseTextInput/index.native.tsx | 101 ++++++++--------- .../TextInput/BaseTextInput/index.tsx | 102 +++++++++--------- .../TextInput/BaseTextInput/types.ts | 15 ++- src/components/TextInput/index.native.tsx | 30 +++--- src/components/TextInput/index.tsx | 27 +++-- src/components/TextInput/types.ts | 0 6 files changed, 143 insertions(+), 132 deletions(-) delete mode 100644 src/components/TextInput/types.ts diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index bde3f880cfbf..d8ecb67517f2 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {Component, ForwardedRef, forwardRef, useCallback, useEffect, useRef, useState} from 'react'; import { ActivityIndicator, Animated, @@ -11,9 +11,11 @@ import { StyleSheet, TextInput, TextInputFocusEventData, + TextInputProps, View, ViewStyle, } from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -33,41 +35,42 @@ import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; -function BaseTextInput({ - label = '', - name = '', - value = undefined, - defaultValue = undefined, - placeholder = '', - errorText = '', - icon = null, - textInputContainerStyles = [], - containerStyles = [], - inputStyle = [], - forceActiveLabel = false, - autoFocus = false, - disableKeyboard = false, - autoGrow = false, - autoGrowHeight = false, - hideFocusedState = false, - innerRef = () => {}, - maxLength = null, - hint = '', - shouldSaveDraft = false, - onInputChange = () => {}, - shouldDelayFocus = false, - submitOnEnter = false, - multiline = false, - shouldUseDefaultValue = false, - shouldInterceptSwipe = false, - autoCorrect = true, - prefixCharacter, - inputID, - ...inputProps -}: BaseTextInputProps) { +function BaseTextInput( + { + label = '', + // name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + maxLength = undefined, + hint = '', + // shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + // shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + ...inputProps + }: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) { const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); @@ -195,7 +198,7 @@ function BaseTextInput({ isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current) + isInputAutoFilled(input.current as unknown as Element) ) { activateLabel(); } else { @@ -266,7 +269,7 @@ function BaseTextInput({ textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), !hideFocusedState && isFocused && styles.borderColorFocus, - (!!hasError || errorText) && styles.borderColorDanger, + (!!hasError || !!errorText) && styles.borderColorDanger, autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); const isMultiline = !!multiline || autoGrowHeight; @@ -284,7 +287,7 @@ function BaseTextInput({ accessibilityLabel={label ?? ''} accessible style={[ - autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), + autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, typeof maxHeight === 'number' ? maxHeight : 0), !isMultiline && styles.componentHeightLarge, containerStyles, ]} @@ -327,15 +330,15 @@ function BaseTextInput({ )} { - if (typeof innerRef === 'function') { - innerRef(ref); - } else if (innerRef && 'current' in innerRef) { + ref={(el) => { + if (typeof ref === 'function') { + ref(el); + } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - innerRef.current = ref; + ref.current = el; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = ref; + input.current = el; }} // eslint-disable-next-line {...inputProps} @@ -354,7 +357,7 @@ function BaseTextInput({ !isMultiline && {height, lineHeight: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, @@ -389,7 +392,7 @@ function BaseTextInput({ style={[styles.flex1, styles.textInputIconContainer]} onPress={togglePasswordVisibility} onMouseDown={(e) => e.preventDefault()} - accessibilityLabel={translate?.('common.visible') ?? ''} + accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} > )} - {!inputProps.secureTextEntry && Boolean(icon) && ( + {!inputProps.secureTextEntry && icon && ( - {!inputHelpText && ( + {!!inputHelpText && ( {}, - maxLength = null, - hint = '', - shouldSaveDraft = false, - onInputChange = () => {}, - shouldDelayFocus = false, - submitOnEnter = false, - multiline = false, - shouldUseDefaultValue = false, - shouldInterceptSwipe = false, - autoCorrect = true, - prefixCharacter, - inputID, - ...inputProps -}: BaseTextInputProps) { +function BaseTextInput( + { + label = '', + // name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + maxLength = undefined, + hint = '', + // shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + // shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + ...inputProps + }: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) { const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); @@ -137,7 +141,7 @@ function BaseTextInput({ animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); isLabelActive.current = false; - }, [animateLabel, forceActiveLabel,prefixCharacter, value]); + }, [animateLabel, forceActiveLabel, prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { if (inputProps.onFocus) { @@ -194,7 +198,7 @@ function BaseTextInput({ isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current) + isInputAutoFilled(input.current as unknown as Element) ) { activateLabel(); } else { @@ -256,7 +260,7 @@ function BaseTextInput({ const inputHelpText = errorText || hint; const newPlaceholder = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; - const textInputContainerStyles: StyleProp = StyleSheet.flatten([ + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), @@ -298,7 +302,7 @@ function BaseTextInput({ accessibilityLabel={label ?? ''} accessible style={[ - autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), + autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, typeof maxHeight === 'number' ? maxHeight : 0), !isMultiline && styles.componentHeightLarge, containerStyles, ]} @@ -308,10 +312,10 @@ function BaseTextInput({ // or if multiline is not supplied we calculate the textinput height, using onLayout. onLayout={onLayout} style={[ - textInputContainerStyles, + newTextInputContainerStyles, // When autoGrow is on and minWidth is not supplied, add a minWidth to allow the input to be focusable. - autoGrow && !textInputContainerStyles?.minWidth && styles.mnw2, + autoGrow && !newTextInputContainerStyles?.minWidth && styles.mnw2, ]} > {hasLabel ? ( @@ -341,15 +345,15 @@ function BaseTextInput({ )} { - if (typeof innerRef === 'function') { - innerRef(ref); - } else if (innerRef && 'current' in innerRef) { + ref={(el) => { + if (typeof ref === 'function') { + ref(el); + } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - innerRef.current = ref; + ref.current = el; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = ref; + input.current = el; }} // eslint-disable-next-line {...inputProps} @@ -362,7 +366,7 @@ function BaseTextInput({ styles.w100, inputStyle, (!hasLabel || isMultiline) && styles.pv0, - !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), + !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(prefixCharacter) + styles.pl1.paddingLeft), inputProps.secureTextEntry && styles.secureInput, // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear @@ -416,7 +420,7 @@ function BaseTextInput({ /> )} - {!inputProps.secureTextEntry && Boolean(icon) && ( + {!inputProps.secureTextEntry && icon && ( - {!inputHelpText && ( + {!!inputHelpText && ( ; /** Icon to display in right side of text input */ - icon: (props: SrcProps) => React.ReactNode; + icon: ((props: SrcProps) => React.ReactNode) | null; /** Customize the TextInput container */ textInputContainerStyles: StyleProp; @@ -60,9 +60,6 @@ type CustomBaseTextInputProps = { /** Hide the focus styles on TextInput */ hideFocusedState?: boolean; - /** Forward the inner ref */ - innerRef?: React.ForwardedRef, unknown, unknown> | null>; - /** Maximum characters allowed */ maxLength?: number; @@ -103,9 +100,9 @@ type CustomBaseTextInputProps = { hasError?: boolean; onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; isLoading?: boolean; - translate?: (key: string) => string; + autoCompleteType?: string; }; -type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; +type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; export default BaseTextInputProps; export type {CustomBaseTextInputProps}; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index a4d0c76337ab..4ddc64c50de4 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -1,12 +1,17 @@ -import React, {forwardRef, useEffect} from 'react'; -import {AppState, Keyboard} from 'react-native'; +import React, {Component, ForwardedRef, forwardRef, useEffect} from 'react'; +import {AppState, Keyboard, TextInputProps} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import styles from '@styles/styles'; import BaseTextInput from './BaseTextInput'; -import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; +import BaseTextInputProps from './BaseTextInput/types'; -const TextInput = forwardRef((props, ref) => { +// eslint-disable-next-line react/function-component-definition +const TextInput = ( + {inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) => { useEffect(() => { - if (!props.disableKeyboard) { + if (!disableKeyboard) { return; } @@ -21,8 +26,7 @@ const TextInput = forwardRef((props, ref) => { return () => { appStateSubscription.remove(); }; - }, [props.disableKeyboard]); - + }, [disableKeyboard]); return ( { // Setting autoCompleteType to new-password throws an error on Android/iOS, so fall back to password in that case // eslint-disable-next-line react/jsx-props-no-multi-spaces autoCompleteType={props.autoCompleteType === 'new-password' ? 'password' : props.autoCompleteType} - innerRef={ref} - inputStyle={[styles.baseTextInput, ...props.inputStyle]} + ref={ref} + inputStyle={[styles.baseTextInput, inputStyle]} /> ); -}); +}; -TextInput.propTypes = baseTextInputPropTypes.propTypes; -TextInput.defaultProps = baseTextInputPropTypes.defaultProps; +// TextInput.propTypes = baseTextInputPropTypes.propTypes; +// TextInput.defaultProps = baseTextInputPropTypes.defaultProps; TextInput.displayName = 'TextInput'; -export default TextInput; +export default forwardRef(TextInput); diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index c8dc21b97310..582c35e94958 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,4 +1,6 @@ -import React, {ForwardedRef, useEffect, useRef} from 'react'; +import React, {Component, ForwardedRef, useEffect, useRef} from 'react'; +import {StyleProp, TextInputProps, ViewStyle} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; @@ -8,17 +10,20 @@ import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes' import BaseTextInputProps from './BaseTextInput/types'; import * as styleConst from './styleConst'; -function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { +function TextInput( + {label = '', name = '', textInputContainerStyles, inputStyle, disableKeyboard = false, multiline = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) { const textInputRef = useRef(null); const removeVisibilityListenerRef = useRef<() => void>(null); useEffect(() => { - if (props.disableKeyboard) { + if (disableKeyboard) { textInputRef.current?.setAttribute('inputmode', 'none'); } - if (props.name) { - textInputRef.current?.setAttribute('name', props.name); + if (name) { + textInputRef.current?.setAttribute('name', name); } // @ts-expect-error We need to reassign this ref to the input ref removeVisibilityListenerRef.current = Visibility.onVisibilityChange(() => { @@ -38,7 +43,7 @@ function TextInput(props: BaseTextInputProps, ref: ForwardedRef // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isLabeledMultiline = Boolean(props.label?.length) && props.multiline; + const isLabeledMultiline = Boolean(label?.length) && multiline; const labelAnimationStyle = { // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-translate-y': `${styleConst.ACTIVE_LABEL_TRANSLATE_Y}px`, @@ -50,7 +55,9 @@ function TextInput(props: BaseTextInputProps, ref: ForwardedRef return ( { + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + ref={(el) => { // @ts-expect-error We need to reassign this ref to the input ref textInputRef.current = el; if (!ref) { @@ -65,10 +72,8 @@ function TextInput(props: BaseTextInputProps, ref: ForwardedRef // eslint-disable-next-line no-param-reassign ref.current = el; }} - inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, props.inputStyle]} - textInputContainerStyles={[labelAnimationStyle, props.textInputContainerStyles]} - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} + inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, inputStyle]} + textInputContainerStyles={[labelAnimationStyle as StyleProp, textInputContainerStyles]} /> ); } diff --git a/src/components/TextInput/types.ts b/src/components/TextInput/types.ts deleted file mode 100644 index e69de29bb2d1..000000000000 From a57d229f1fd88f721cf9662db01636d434b6ae17 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 17 Nov 2023 18:34:18 +0100 Subject: [PATCH 010/231] fix: remove getDisplayNameForTypingIndicator --- src/libs/actions/PersonalDetails.ts | 26 ---------- .../home/report/ReportTypingIndicator.js | 47 +++++++++---------- 2 files changed, 22 insertions(+), 51 deletions(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 2d51fbb9e8d2..725159c1c7f3 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -59,31 +59,6 @@ function getDisplayName(login: string, personalDetail: Pick value?.login === userAccountIDOrLogin)?.[1]; - - // It's possible for displayName to be empty string, so we must use "||" to fallback to userAccountIDOrLogin. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return detailsByLogin?.displayName || userAccountIDOrLogin; - } - - const detailsByAccountID = allPersonalDetails?.[accountID]; - - // It's possible for displayName to be empty string, so we must use "||" to fallback to login or defaultDisplayName. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return detailsByAccountID?.displayName || detailsByAccountID?.login || defaultDisplayName; -} - /** * Gets the first and last name from the user's personal details. * If the login is the same as the displayName, then they don't exist, @@ -584,7 +559,6 @@ export { extractFirstAndLastNameFromAvailableDetails, getCountryISO, getDisplayName, - getDisplayNameForTypingIndicator, getPrivatePersonalDetails, openPersonalDetailsPage, openPublicProfilePage, diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 3a2c611ac358..c2c9a0222542 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -2,24 +2,18 @@ import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; import Text from '@components/Text'; import TextWithEllipsis from '@components/TextWithEllipsis'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import compose from '@libs/compose'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import useThemeStyles from '@styles/useThemeStyles'; -import * as PersonalDetails from '@userActions/PersonalDetails'; import ONYXKEYS from '@src/ONYXKEYS'; const propTypes = { /** Key-value pairs of user accountIDs/logins and whether or not they are typing. Keys are accountIDs or logins. */ userTypingStatuses: PropTypes.objectOf(PropTypes.bool), - - /** Information about the network */ - network: networkPropTypes.isRequired, - - ...withLocalizePropTypes, }; const defaultProps = { @@ -27,13 +21,20 @@ const defaultProps = { }; function ReportTypingIndicator(props) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(props.userTypingStatuses), (loginOrAccountID) => props.userTypingStatuses[loginOrAccountID]), [props.userTypingStatuses]); // If we are offline, the user typing statuses are not up-to-date so do not show them - if (props.network.isOffline) { + if (isOffline) { return null; } + const firstUserTyping = usersTyping[0]; + const firstUserTypingID = Number.isNaN(firstUserTyping) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping; + const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false); + const numUsersTyping = _.size(usersTyping); // Decide on the Text element that will hold the display based on the number of users that are typing. @@ -44,8 +45,8 @@ function ReportTypingIndicator(props) { case 1: return ( - {props.translate('reportTypingIndicator.multipleUsers')} - {` ${props.translate('reportTypingIndicator.areTyping')}`} + {translate('reportTypingIndicator.multipleUsers')} + {` ${translate('reportTypingIndicator.areTyping')}`} ); } @@ -69,13 +70,9 @@ ReportTypingIndicator.propTypes = propTypes; ReportTypingIndicator.defaultProps = defaultProps; ReportTypingIndicator.displayName = 'ReportTypingIndicator'; -export default compose( - withLocalize, - withNetwork(), - withOnyx({ - userTypingStatuses: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, - initialValue: {}, - }, - }), -)(ReportTypingIndicator); +export default withOnyx({ + userTypingStatuses: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, + initialValue: {}, + }, +})(ReportTypingIndicator); From 1ff3017f95159fefe0c7318cad05b7ad4f1bd440 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 21 Nov 2023 10:17:04 +0100 Subject: [PATCH 011/231] fix: apply requested changes --- src/pages/home/report/ReportTypingIndicator.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index c2c9a0222542..9b0757668d59 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -26,13 +26,14 @@ function ReportTypingIndicator(props) { const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(props.userTypingStatuses), (loginOrAccountID) => props.userTypingStatuses[loginOrAccountID]), [props.userTypingStatuses]); + const firstUserTyping = usersTyping[0]; + const firstUserTypingID = useMemo(() => firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping, [firstUserTyping]); + // If we are offline, the user typing statuses are not up-to-date so do not show them if (isOffline) { return null; } - const firstUserTyping = usersTyping[0]; - const firstUserTypingID = Number.isNaN(firstUserTyping) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping; const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false); const numUsersTyping = _.size(usersTyping); From b30cb29aa57cf1ede0a2103aa87931fdc46c2138 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 21 Nov 2023 11:40:10 +0100 Subject: [PATCH 012/231] fix: run prettier --- src/pages/home/report/ReportTypingIndicator.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 9b0757668d59..9e0149e973e4 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -27,7 +27,10 @@ function ReportTypingIndicator(props) { const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(props.userTypingStatuses), (loginOrAccountID) => props.userTypingStatuses[loginOrAccountID]), [props.userTypingStatuses]); const firstUserTyping = usersTyping[0]; - const firstUserTypingID = useMemo(() => firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping, [firstUserTyping]); + const firstUserTypingID = useMemo( + () => (firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), + [firstUserTyping], + ); // If we are offline, the user typing statuses are not up-to-date so do not show them if (isOffline) { From 4810696d937d92af414c59fba2160537b801fde0 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 21 Nov 2023 23:58:29 +0700 Subject: [PATCH 013/231] Update all selectors to use new format for selected participant --- src/libs/OptionsListUtils.js | 4 +++- src/pages/RoomInvitePage.js | 19 +++++++++++-------- src/pages/workspace/WorkspaceInvitePage.js | 18 ++++++++++-------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index b97ae6daed11..f1e97151a062 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -1595,15 +1595,17 @@ function formatMemberForList(member, config = {}) { * @param {Array} betas * @param {String} searchValue * @param {Array} excludeLogins + * @param {Boolean} includeSelectedOptions * @returns {Object} */ -function getMemberInviteOptions(personalDetails, betas = [], searchValue = '', excludeLogins = []) { +function getMemberInviteOptions(personalDetails, betas = [], searchValue = '', excludeLogins = [], includeSelectedOptions = false) { return getOptions([], personalDetails, { betas, searchInputValue: searchValue.trim(), includePersonalDetails: true, excludeLogins, sortPersonalDetailsByAlphaAsc: true, + includeSelectedOptions, }); } diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index a1f7d22c3dc3..cacf603f8836 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -84,7 +84,7 @@ function RoomInvitePage(props) { }, []); useEffect(() => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails information const detailsMap = {}; @@ -104,13 +104,16 @@ function RoomInvitePage(props) { const sections = []; let indexOffset = 0; - sections.push({ - title: undefined, - data: selectedOptions, - shouldShow: true, - indexOffset, - }); - indexOffset += selectedOptions.length; + // Only show the selected participants if the search is empty + if (searchTerm === '') { + sections.push({ + title: undefined, + data: selectedOptions, + shouldShow: true, + indexOffset, + }); + indexOffset += selectedOptions.length; + } // Filtering out selected users from the search results const selectedLogins = _.map(selectedOptions, ({login}) => login); diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 4bef69c82414..eeffb35b4d23 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -90,7 +90,7 @@ function WorkspaceInvitePage(props) { const newPersonalDetailsDict = {}; const newSelectedOptionsDict = {}; - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyMembers information const detailsMap = {}; @@ -130,13 +130,15 @@ function WorkspaceInvitePage(props) { const sections = []; let indexOffset = 0; - sections.push({ - title: undefined, - data: selectedOptions, - shouldShow: true, - indexOffset, - }); - indexOffset += selectedOptions.length; + if (searchTerm === '') { + sections.push({ + title: undefined, + data: selectedOptions, + shouldShow: true, + indexOffset, + }); + indexOffset += selectedOptions.length; + } // Filtering out selected users from the search results const selectedLogins = _.map(selectedOptions, ({login}) => login); From 0c5bc5ec812e70ed87d86f2e17470bdb9139aed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=27fvlvte=27=20Fa=C5=82at?= Date: Mon, 27 Nov 2023 10:02:46 +0100 Subject: [PATCH 014/231] Conflicts & requested changes. --- src/components/withKeyboardState.tsx | 4 +--- src/hooks/useKeyboardState.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index 48f2f4c3b8c6..f6ebfc695529 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -1,7 +1,5 @@ -import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactNode, useEffect, useState} from 'react'; -import {Keyboard} from 'react-native'; import PropTypes from 'prop-types'; -import React, {createContext, forwardRef, useEffect, useMemo, useState} from 'react'; +import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactNode, useEffect, useMemo, useState} from 'react'; import {Keyboard} from 'react-native'; import getComponentDisplayName from '@libs/getComponentDisplayName'; diff --git a/src/hooks/useKeyboardState.ts b/src/hooks/useKeyboardState.ts index d7cc8d271503..edcbaa32cc9e 100644 --- a/src/hooks/useKeyboardState.ts +++ b/src/hooks/useKeyboardState.ts @@ -1,5 +1,5 @@ import {useContext} from 'react'; -import {KeyboardStateContext, KeyboardStateContextValue} from '../components/withKeyboardState'; +import {KeyboardStateContext, KeyboardStateContextValue} from '@components/withKeyboardState'; /** * Hook for getting current state of keyboard From b4123e2f7451fb5ad0d2c8cd58eb83244e01df3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=27fvlvte=27=20Fa=C5=82at?= Date: Mon, 27 Nov 2023 15:32:21 +0100 Subject: [PATCH 015/231] Requested changes. --- src/components/withKeyboardState.tsx | 45 +++++++++++++--------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index f6ebfc695529..f072c99d47f2 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -1,18 +1,14 @@ import PropTypes from 'prop-types'; -import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactNode, useEffect, useMemo, useState} from 'react'; +import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactElement, RefAttributes, useEffect, useMemo, useState} from 'react'; import {Keyboard} from 'react-native'; import getComponentDisplayName from '@libs/getComponentDisplayName'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; type KeyboardStateContextValue = { /** Whether the keyboard is open */ isKeyboardShown: boolean; }; -type KeyboardStateProviderProps = { - /* Actual content wrapped by this component */ - children: ReactNode; -}; - // TODO: Remove - left for backwards compatibility with existing components. const keyboardStatePropTypes = { /** Whether the keyboard is open */ @@ -21,7 +17,7 @@ const keyboardStatePropTypes = { const KeyboardStateContext = createContext(null); -function KeyboardStateProvider(props: KeyboardStateProviderProps) { +function KeyboardStateProvider(props: ChildrenProps): ReactElement | null { const {children} = props; const [isKeyboardShown, setIsKeyboardShown] = useState(false); useEffect(() => { @@ -47,23 +43,24 @@ function KeyboardStateProvider(props: KeyboardStateProviderProps) { return {children}; } -// eslint-disable-next-line @typescript-eslint/naming-convention -export default function withKeyboardState(WrappedComponent: ComponentType<{ref: ForwardedRef}>) { - const WithKeyboardState = forwardRef((props: Record, ref: React.ForwardedRef) => ( - - {(keyboardStateProps) => ( - - )} - - )); - (WithKeyboardState as unknown as {displayName: string}).displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent as ComponentType)})`; - return WithKeyboardState; +export default function withKeyboardState(WrappedComponent: ComponentType>): (props: TProps & React.RefAttributes) => ReactElement | null { + function WithKeyboardState(props: TProps, ref: ForwardedRef) { + return ( + + {(keyboardStateProps) => ( + + )} + + ); + } + WithKeyboardState.displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent as ComponentType)})`; + return forwardRef(WithKeyboardState); } export {KeyboardStateProvider, keyboardStatePropTypes, type KeyboardStateContextValue, KeyboardStateContext}; From ab4bc47815a9409a19ff647b60caea50a502eda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=27fvlvte=27=20Fa=C5=82at?= Date: Mon, 27 Nov 2023 15:41:12 +0100 Subject: [PATCH 016/231] Requested change. --- src/components/withKeyboardState.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index f072c99d47f2..87b606d4c113 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -17,8 +17,7 @@ const keyboardStatePropTypes = { const KeyboardStateContext = createContext(null); -function KeyboardStateProvider(props: ChildrenProps): ReactElement | null { - const {children} = props; +function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null { const [isKeyboardShown, setIsKeyboardShown] = useState(false); useEffect(() => { const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => { From 7fa124f244182cb2da393cc3b65711380a2cca55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=27fvlvte=27=20Fa=C5=82at?= Date: Mon, 27 Nov 2023 16:29:01 +0100 Subject: [PATCH 017/231] Requested changes. --- src/components/withKeyboardState.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index 87b606d4c113..fd00ef920399 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -19,6 +19,7 @@ const KeyboardStateContext = createContext(nul function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null { const [isKeyboardShown, setIsKeyboardShown] = useState(false); + useEffect(() => { const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => { setIsKeyboardShown(true); @@ -42,8 +43,10 @@ function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null { return {children}; } -export default function withKeyboardState(WrappedComponent: ComponentType>): (props: TProps & React.RefAttributes) => ReactElement | null { - function WithKeyboardState(props: TProps, ref: ForwardedRef) { +export default function withKeyboardState( + WrappedComponent: ComponentType>, +): (props: Omit & React.RefAttributes) => ReactElement | null { + function WithKeyboardState(props: Omit, ref: ForwardedRef) { return ( {(keyboardStateProps) => ( @@ -51,14 +54,14 @@ export default function withKeyboardState(WrappedComponent: Compon // eslint-disable-next-line react/jsx-props-no-spreading {...keyboardStateProps} // eslint-disable-next-line react/jsx-props-no-spreading - {...props} + {...(props as TProps)} ref={ref} /> )} ); } - WithKeyboardState.displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent as ComponentType)})`; + WithKeyboardState.displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent)})`; return forwardRef(WithKeyboardState); } From 2ffcec2417dd634c342460171de57b2bc847f049 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 30 Nov 2023 08:58:31 +0100 Subject: [PATCH 018/231] fix: destructure props --- src/pages/home/report/ReportTypingIndicator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 9e0149e973e4..afb18a98b350 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -20,12 +20,12 @@ const defaultProps = { userTypingStatuses: {}, }; -function ReportTypingIndicator(props) { +function ReportTypingIndicator({userTypingStatuses}) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const styles = useThemeStyles(); - const usersTyping = useMemo(() => _.filter(_.keys(props.userTypingStatuses), (loginOrAccountID) => props.userTypingStatuses[loginOrAccountID]), [props.userTypingStatuses]); + const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; const firstUserTypingID = useMemo( () => (firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), From 2b40a50d097874d6cd8e0ab65f39af8cebbd6693 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:26:35 +0100 Subject: [PATCH 019/231] migrate BlockingView and associated components to ts --- .../{BlockingView.js => BlockingView.tsx} | 71 +++++++------ ...tFoundView.js => FullPageNotFoundView.tsx} | 48 +++++---- .../FullPageOfflineBlockingView.js | 38 ------- .../FullPageOfflineBlockingView.tsx | 31 ++++++ src/components/Text.tsx | 31 +++--- src/components/TextLink.js | 99 ------------------- src/components/TextLink.tsx | 77 +++++++++++++++ 7 files changed, 182 insertions(+), 213 deletions(-) rename src/components/BlockingViews/{BlockingView.js => BlockingView.tsx} (59%) rename src/components/BlockingViews/{FullPageNotFoundView.js => FullPageNotFoundView.tsx} (69%) delete mode 100644 src/components/BlockingViews/FullPageOfflineBlockingView.js create mode 100644 src/components/BlockingViews/FullPageOfflineBlockingView.tsx delete mode 100644 src/components/TextLink.js create mode 100644 src/components/TextLink.tsx diff --git a/src/components/BlockingViews/BlockingView.js b/src/components/BlockingViews/BlockingView.tsx similarity index 59% rename from src/components/BlockingViews/BlockingView.js rename to src/components/BlockingViews/BlockingView.tsx index aec414cdeb74..ede40cfda752 100644 --- a/src/components/BlockingViews/BlockingView.js +++ b/src/components/BlockingViews/BlockingView.tsx @@ -1,6 +1,6 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; +import {SvgProps} from 'react-native-svg'; import AutoEmailLink from '@components/AutoEmailLink'; import Icon from '@components/Icon'; import Text from '@components/Text'; @@ -10,51 +10,52 @@ import Navigation from '@libs/Navigation/Navigation'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; +import {TranslationPaths} from '@src/languages/types'; -const propTypes = { +type BlockingViewProps = { /** Expensicon for the page */ - icon: PropTypes.func.isRequired, + icon: React.FC; /** Color for the icon (should be from theme) */ - iconColor: PropTypes.string, + iconColor?: string; /** Title message below the icon */ - title: PropTypes.string.isRequired, + title: string; /** Subtitle message below the title */ - subtitle: PropTypes.string, + subtitle?: string; /** Link message below the subtitle */ - linkKey: PropTypes.string, + linkKey?: TranslationPaths; /** Whether we should show a link to navigate elsewhere */ - shouldShowLink: PropTypes.bool, + shouldShowLink?: boolean; /** The custom icon width */ - iconWidth: PropTypes.number, + iconWidth?: number; /** The custom icon height */ - iconHeight: PropTypes.number, + iconHeight?: number; /** Function to call when pressing the navigation link */ - onLinkPress: PropTypes.func, + onLinkPress?: () => void; /** Whether we should embed the link with subtitle */ - shouldEmbedLinkWithSubtitle: PropTypes.bool, + shouldEmbedLinkWithSubtitle?: boolean; }; -const defaultProps = { - iconColor: undefined, - subtitle: '', - shouldShowLink: false, - linkKey: 'notFound.goBackHome', - iconWidth: variables.iconSizeSuperLarge, - iconHeight: variables.iconSizeSuperLarge, - onLinkPress: () => Navigation.dismissModal(), - shouldEmbedLinkWithSubtitle: false, -}; - -function BlockingView(props) { +function BlockingView({ + icon, + iconColor, + title, + subtitle = '', + linkKey = 'notFound.goBackHome', + shouldShowLink = false, + iconWidth = variables.iconSizeSuperLarge, + iconHeight = variables.iconSizeSuperLarge, + onLinkPress = () => Navigation.dismissModal, + shouldEmbedLinkWithSubtitle, +}: BlockingViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -63,14 +64,14 @@ function BlockingView(props) { <> - {props.shouldShowLink ? ( + {shouldShowLink ? ( - {translate(props.linkKey)} + {translate(linkKey)} ) : null} @@ -80,14 +81,14 @@ function BlockingView(props) { return ( - {props.title} + {title} - {props.shouldEmbedLinkWithSubtitle ? ( + {shouldEmbedLinkWithSubtitle ? ( {renderContent()} ) : ( {renderContent()} @@ -96,8 +97,6 @@ function BlockingView(props) { ); } -BlockingView.propTypes = propTypes; -BlockingView.defaultProps = defaultProps; BlockingView.displayName = 'BlockingView'; export default BlockingView; diff --git a/src/components/BlockingViews/FullPageNotFoundView.js b/src/components/BlockingViews/FullPageNotFoundView.tsx similarity index 69% rename from src/components/BlockingViews/FullPageNotFoundView.js rename to src/components/BlockingViews/FullPageNotFoundView.tsx index b82474aa0694..3893c83dc45a 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.js +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -7,52 +6,51 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; +import {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import BlockingView from './BlockingView'; -const propTypes = { +type FullPageNotFoundViewProps = { /** Child elements */ - children: PropTypes.node, + children?: React.ReactNode; /** If true, child components are replaced with a blocking "not found" view */ - shouldShow: PropTypes.bool, + shouldShow?: boolean; /** The key in the translations file to use for the title */ - titleKey: PropTypes.string, + titleKey?: TranslationPaths; /** The key in the translations file to use for the subtitle */ - subtitleKey: PropTypes.string, + subtitleKey?: TranslationPaths; /** Whether we should show a link to navigate elsewhere */ - shouldShowLink: PropTypes.bool, + shouldShowLink?: boolean; /** Whether we should show the back button on the header */ - shouldShowBackButton: PropTypes.bool, + shouldShowBackButton?: boolean; /** The key in the translations file to use for the go back link */ - linkKey: PropTypes.string, + linkKey?: TranslationPaths; /** Method to trigger when pressing the back button of the header */ - onBackButtonPress: PropTypes.func, + onBackButtonPress: () => void; /** Function to call when pressing the navigation link */ - onLinkPress: PropTypes.func, -}; - -const defaultProps = { - children: null, - shouldShow: false, - titleKey: 'notFound.notHere', - subtitleKey: 'notFound.pageNotFound', - linkKey: 'notFound.goBackHome', - onBackButtonPress: () => Navigation.goBack(ROUTES.HOME), - shouldShowLink: true, - shouldShowBackButton: true, - onLinkPress: () => Navigation.dismissModal(), + onLinkPress: () => void; }; // eslint-disable-next-line rulesdir/no-negated-variables -function FullPageNotFoundView({children, shouldShow, titleKey, subtitleKey, linkKey, onBackButtonPress, shouldShowLink, shouldShowBackButton, onLinkPress}) { +function FullPageNotFoundView({ + children = null, + shouldShow = false, + titleKey = 'notFound.notHere', + subtitleKey = 'notFound.pageNotFound', + linkKey = 'notFound.goBackHome', + onBackButtonPress = () => Navigation.goBack(ROUTES.HOME), + shouldShowLink = true, + shouldShowBackButton = true, + onLinkPress = () => Navigation.dismissModal, +}: FullPageNotFoundViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); if (shouldShow) { @@ -81,8 +79,6 @@ function FullPageNotFoundView({children, shouldShow, titleKey, subtitleKey, link return children; } -FullPageNotFoundView.propTypes = propTypes; -FullPageNotFoundView.defaultProps = defaultProps; FullPageNotFoundView.displayName = 'FullPageNotFoundView'; export default FullPageNotFoundView; diff --git a/src/components/BlockingViews/FullPageOfflineBlockingView.js b/src/components/BlockingViews/FullPageOfflineBlockingView.js deleted file mode 100644 index 5a876ecb8b1c..000000000000 --- a/src/components/BlockingViews/FullPageOfflineBlockingView.js +++ /dev/null @@ -1,38 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import * as Expensicons from '@components/Icon/Expensicons'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import compose from '@libs/compose'; -import BlockingView from './BlockingView'; - -const propTypes = { - /** Child elements */ - children: PropTypes.node.isRequired, - - /** Props to fetch translation features */ - ...withLocalizePropTypes, - - /** Props to detect online status */ - network: networkPropTypes.isRequired, -}; - -function FullPageOfflineBlockingView(props) { - if (props.network.isOffline) { - return ( - - ); - } - - return props.children; -} - -FullPageOfflineBlockingView.propTypes = propTypes; -FullPageOfflineBlockingView.displayName = 'FullPageOfflineBlockingView'; - -export default compose(withLocalize, withNetwork())(FullPageOfflineBlockingView); diff --git a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx new file mode 100644 index 000000000000..c8f5592287f3 --- /dev/null +++ b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import * as Expensicons from '@components/Icon/Expensicons'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import BlockingView from './BlockingView'; + +type FullPageOfflineBlockingViewProps = { + /** Child elements */ + children: React.ReactNode; +}; + +function FullPageOfflineBlockingView({children}: FullPageOfflineBlockingViewProps) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + + if (isOffline) { + return ( + + ); + } + + return children; +} + +FullPageOfflineBlockingView.displayName = 'FullPageOfflineBlockingView'; + +export default FullPageOfflineBlockingView; diff --git a/src/components/Text.tsx b/src/components/Text.tsx index 58a5cf300699..a289e224fb72 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -1,28 +1,29 @@ import React, {ForwardedRef} from 'react'; -// eslint-disable-next-line no-restricted-imports import {Text as RNText, TextProps as RNTextProps, StyleSheet} from 'react-native'; import type {TextStyle} from 'react-native'; import fontFamily from '@styles/fontFamily'; import useTheme from '@styles/themes/useTheme'; import variables from '@styles/variables'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; -type TextProps = RNTextProps & { - /** The color of the text */ - color?: string; +type TextProps = RNTextProps & + ChildrenProps & { + /** The color of the text */ + color?: string; - /** The size of the text */ - fontSize?: number; - /** The alignment of the text */ - textAlign?: 'left' | 'right' | 'auto' | 'center' | 'justify'; - /** Any children to display */ - children: React.ReactNode; + /** The size of the text */ + fontSize?: number; - /** The family of the font to use */ - family?: keyof typeof fontFamily; -}; + /** The alignment of the text */ + textAlign?: TextStyle['textAlign']; -function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', children = null, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef) { + /** The family of the font to use */ + family?: keyof typeof fontFamily; + }; + +function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', children, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef) { const theme = useTheme(); + const componentStyle: TextStyle = { color: color ?? theme.text, fontSize, @@ -34,6 +35,7 @@ function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', c if (!componentStyle.lineHeight && componentStyle.fontSize === variables.fontSizeNormal) { componentStyle.lineHeight = variables.fontSizeNormalHeight; } + return ( event.preventDefault(), -}; - -function TextLink(props) { - const styles = useThemeStyles(); - const rest = _.omit(props, _.keys(propTypes)); - const additionalStyles = _.isArray(props.style) ? props.style : [props.style]; - - /** - * @param {Event} event - */ - const openLink = (event) => { - event.preventDefault(); - if (props.onPress) { - props.onPress(); - return; - } - - Link.openExternalLink(props.href); - }; - - /** - * @param {Event} event - */ - const openLinkIfEnterKeyPressed = (event) => { - if (event.key !== 'Enter') { - return; - } - openLink(event); - }; - - return ( - - {props.children} - - ); -} - -TextLink.defaultProps = defaultProps; -TextLink.propTypes = propTypes; -TextLink.displayName = 'TextLink'; - -const TextLinkWithRef = React.forwardRef((props, ref) => ( - -)); - -TextLinkWithRef.displayName = 'TextLinkWithRef'; - -export default TextLinkWithRef; diff --git a/src/components/TextLink.tsx b/src/components/TextLink.tsx new file mode 100644 index 000000000000..fae4a71eeafa --- /dev/null +++ b/src/components/TextLink.tsx @@ -0,0 +1,77 @@ +import React, {ForwardedRef, forwardRef, KeyboardEventHandler, MouseEventHandler} from 'react'; +import {GestureResponderEvent, Text as RNText, StyleProp, TextStyle} from 'react-native'; +import useThemeStyles from '@styles/useThemeStyles'; +import * as Link from '@userActions/Link'; +import CONST from '@src/CONST'; +import Text, {TextProps} from './Text'; + +type LinkProps = { + /** Link to open in new tab */ + href: string; + + onPress?: undefined; +}; + +type PressProps = { + href?: undefined; + + /** Overwrites the default link behavior with a custom callback */ + onPress: () => void; +}; + +type TextLinkProps = (LinkProps | PressProps) & + TextProps & { + /** Additional style props */ + style?: StyleProp; + + /** Callback that is called when mousedown is triggered */ + onMouseDown?: MouseEventHandler; + }; + +function TextLink({href, onPress, children, style, onMouseDown = (event) => event.preventDefault(), ...rest}: TextLinkProps, ref: ForwardedRef) { + const styles = useThemeStyles(); + + const openLink = () => { + if (onPress) { + onPress(); + } else { + Link.openExternalLink(href); + } + }; + + const openLinkOnTap = (event: GestureResponderEvent) => { + event.preventDefault(); + + openLink(); + }; + + const openLinkOnEnterKey: KeyboardEventHandler = (event) => { + if (event.key !== 'Enter') { + return; + } + event.preventDefault(); + + openLink(); + }; + + return ( + + {children} + + ); +} + +TextLink.displayName = 'TextLink'; + +export default forwardRef(TextLink); From fe1e320e2fe8270aa45aac8d83548f933a0a9e50 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Dec 2023 09:43:54 +0100 Subject: [PATCH 020/231] fix: apply requested changes --- .../home/report/ReportTypingIndicator.js | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index afb18a98b350..a519afaaa8ba 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -27,47 +27,40 @@ function ReportTypingIndicator({userTypingStatuses}) { const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; + + // If the user is typing on OldDot, firstUserTyping will be a string (the user's login) const firstUserTypingID = useMemo( () => (firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), [firstUserTyping], ); // If we are offline, the user typing statuses are not up-to-date so do not show them - if (isOffline) { + if (isOffline || !firstUserTyping) { return null; } const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false); - const numUsersTyping = _.size(usersTyping); - - // Decide on the Text element that will hold the display based on the number of users that are typing. - switch (numUsersTyping) { - case 0: - return null; - - case 1: - return ( - - ); - - default: - return ( - - {translate('reportTypingIndicator.multipleUsers')} - {` ${translate('reportTypingIndicator.areTyping')}`} - - ); + if (usersTyping.length === 1) { + return ( + + ) } + return ( + + {translate('reportTypingIndicator.multipleUsers')} + {` ${translate('reportTypingIndicator.areTyping')}`} + + ); } ReportTypingIndicator.propTypes = propTypes; From e7d667c2edc8300b5fcb649490785a153c98aaeb Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Dec 2023 09:49:58 +0100 Subject: [PATCH 021/231] fix: run prettier --- src/pages/home/report/ReportTypingIndicator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index a519afaaa8ba..c141a4d141e1 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -50,7 +50,7 @@ function ReportTypingIndicator({userTypingStatuses}) { wrapperStyle={[styles.chatItemComposeSecondaryRow, styles.flex1]} leadingTextParentStyle={styles.chatItemComposeSecondaryRowOffset} /> - ) + ); } return ( Date: Fri, 1 Dec 2023 12:48:51 +0100 Subject: [PATCH 022/231] Use PersonalDetailsUtils.getDisplayNameOrDefault to escape merged account prefix --- src/libs/OptionsListUtils.js | 17 +++++++++++------ src/libs/PersonalDetailsUtils.js | 3 ++- src/libs/ReportUtils.ts | 5 +++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index e3b6ec77380e..e80f8e7dd1ba 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -200,7 +200,7 @@ function isPersonalDetailsReady(personalDetails) { function getParticipantsOption(participant, personalDetails) { const detail = getPersonalDetailsForAccountIDs([participant.accountID], personalDetails)[participant.accountID]; const login = detail.login || participant.login; - const displayName = detail.displayName || LocalePhoneNumber.formatPhoneNumber(login); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, 'displayName', LocalePhoneNumber.formatPhoneNumber(login)); return { keyForList: String(detail.accountID), login, @@ -245,7 +245,8 @@ function getParticipantNames(personalDetailList) { participantNames.add(participant.lastName.toLowerCase()); } if (participant.displayName) { - participantNames.add(participant.displayName.toLowerCase()); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(participant, 'displayName'); + participantNames.add(displayName.toLowerCase()); } }); return participantNames; @@ -298,7 +299,11 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic // The regex below is used to remove dots only from the local part of the user email (local-part@domain) // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) // More info https://github.com/Expensify/App/issues/8007 - searchTerms = searchTerms.concat([personalDetail.displayName, personalDetail.login, personalDetail.login.replace(/\.(?=[^\s@]*@)/g, '')]); + searchTerms = searchTerms.concat([ + PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName'), + personalDetail.login, + personalDetail.login.replace(/\.(?=[^\s@]*@)/g, '') + ]); } } } @@ -507,7 +512,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { const lastMessageTextFromReport = getLastMessageTextForReport(report); const lastActorDetails = personalDetailMap[report.lastActorAccountID] || null; - let lastMessageText = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${lastActorDetails.displayName}: ` : ''; + let lastMessageText = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName')}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; if (result.isArchivedRoom) { @@ -1429,8 +1434,8 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail, amountText) { const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login); return { - text: personalDetail.displayName || formattedLogin, - alternateText: formattedLogin || personalDetail.displayName, + text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName', formattedLogin), + alternateText: formattedLogin || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName'), icons: [ { source: UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 560480dcec9d..30b710e9d15c 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -1,6 +1,7 @@ import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; import _ from 'underscore'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; @@ -23,7 +24,7 @@ Onyx.connect({ * @returns {String} */ function getDisplayNameOrDefault(passedPersonalDetails, pathToDisplayName, defaultValue = '') { - const displayName = lodashGet(passedPersonalDetails, pathToDisplayName); + const displayName = lodashGet(passedPersonalDetails, pathToDisplayName, '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); return displayName || defaultValue || Localize.translateLocal('common.hidden'); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f6c3090143f4..b914658fe448 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -31,6 +31,7 @@ import linkingConfig from './Navigation/linkingConfig'; import Navigation from './Navigation/Navigation'; import * as NumberUtils from './NumberUtils'; import Permissions from './Permissions'; +import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import {LastVisibleMessage} from './ReportActionsUtils'; @@ -1270,7 +1271,7 @@ function getIcons( const parentReportAction = ReportActionsUtils.getParentReportAction(report); const actorAccountID = parentReportAction.actorAccountID; - const actorDisplayName = allPersonalDetails?.[actorAccountID ?? -1]?.displayName ?? ''; + const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails, [actorAccountID ?? -1, 'displayName']); const actorIcon = { id: actorAccountID, source: UserUtils.getAvatar(personalDetails?.[actorAccountID ?? -1]?.avatar ?? '', actorAccountID ?? -1), @@ -1394,7 +1395,7 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return formattedLogin; } - const longName = personalDetails.displayName ? personalDetails.displayName : formattedLogin; + const longName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, 'displayName', formattedLogin); const shortName = personalDetails.firstName ? personalDetails.firstName : longName; From 8af48118292e47736055ec99cf4f0bbe745f3d3c Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Dec 2023 15:38:16 +0100 Subject: [PATCH 023/231] fix: minor fix --- src/pages/home/report/ReportTypingIndicator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index c141a4d141e1..6c6531955115 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -28,7 +28,6 @@ function ReportTypingIndicator({userTypingStatuses}) { const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; - // If the user is typing on OldDot, firstUserTyping will be a string (the user's login) const firstUserTypingID = useMemo( () => (firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), [firstUserTyping], @@ -39,7 +38,8 @@ function ReportTypingIndicator({userTypingStatuses}) { return null; } - const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false); + // If the user is typing on OldDot, firstUserTyping will be a string (the user's login) + const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false) || firstUserTyping; if (usersTyping.length === 1) { return ( From bddaeb2f30d441548816d2fb435f6525dced5d3d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 4 Dec 2023 09:35:29 +0100 Subject: [PATCH 024/231] fix: types --- src/components/Checkbox.tsx | 4 +- .../BaseTextInput/baseTextInputPropTypes.js | 138 ------------------ .../TextInput/BaseTextInput/index.native.tsx | 16 +- .../TextInput/BaseTextInput/index.tsx | 4 +- src/components/TextInput/index.tsx | 3 - 5 files changed, 13 insertions(+), 152 deletions(-) delete mode 100644 src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 22577ec2b7f9..39c8a484613b 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,4 +1,4 @@ -import React, {ForwardedRef, forwardRef, KeyboardEvent as ReactKeyboardEvent} from 'react'; +import React, {ForwardedRef, forwardRef, MouseEventHandler, KeyboardEvent as ReactKeyboardEvent} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; @@ -29,7 +29,7 @@ type CheckboxProps = ChildrenProps & { containerStyle?: StyleProp; /** Callback that is called when mousedown is triggered. */ - onMouseDown?: () => void; + onMouseDown?: MouseEventHandler; /** The size of the checkbox container */ containerSize?: number; diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js deleted file mode 100644 index 5387d1ff81d1..000000000000 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ /dev/null @@ -1,138 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** Input label */ - label: PropTypes.string, - - /** Name attribute for the input */ - name: PropTypes.string, - - /** Input value */ - value: PropTypes.string, - - /** Default value - used for non controlled inputs */ - defaultValue: PropTypes.string, - - /** Input value placeholder */ - placeholder: PropTypes.string, - - /** Error text to display */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), - - /** Icon to display in right side of text input */ - icon: PropTypes.func, - - /** Customize the TextInput container */ - textInputContainerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customize the main container */ - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** input style */ - inputStyle: PropTypes.arrayOf(PropTypes.object), - - /** If present, this prop forces the label to remain in a position where it will not collide with input text */ - forceActiveLabel: PropTypes.bool, - - /** Should the input auto focus? */ - autoFocus: PropTypes.bool, - - /** Disable the virtual keyboard */ - disableKeyboard: PropTypes.bool, - - /** - * Autogrow input container length based on the entered text. - * Note: If you use this prop, the text input has to be controlled - * by a value prop. - */ - autoGrow: PropTypes.bool, - - /** - * Autogrow input container height based on the entered text - * Note: If you use this prop, the text input has to be controlled - * by a value prop. - */ - autoGrowHeight: PropTypes.bool, - - /** Hide the focus styles on TextInput */ - hideFocusedState: PropTypes.bool, - - /** Forward the inner ref */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - - /** Maximum characters allowed */ - maxLength: PropTypes.number, - - /** Hint text to display below the TextInput */ - hint: PropTypes.string, - - /** Prefix character */ - prefixCharacter: PropTypes.string, - - /** Whether autoCorrect functionality should enable */ - autoCorrect: PropTypes.bool, - - /** Form props */ - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - shouldSaveDraft: PropTypes.bool, - - /** Callback to update the value on Form when input is used in the Form component. */ - onInputChange: PropTypes.func, - - /** Whether we should wait before focusing the TextInput, useful when using transitions */ - shouldDelayFocus: PropTypes.bool, - - /** Indicate whether pressing Enter on multiline input is allowed to submit the form. */ - submitOnEnter: PropTypes.bool, - - /** Indicate whether input is multiline */ - multiline: PropTypes.bool, - - /** Set the default value to the input if there is a valid saved value */ - shouldUseDefaultValue: PropTypes.bool, - - /** Indicate whether or not the input should prevent swipe actions in tabs */ - shouldInterceptSwipe: PropTypes.bool, -}; - -const defaultProps = { - label: '', - name: '', - errorText: '', - placeholder: '', - hasError: false, - containerStyles: [], - textInputContainerStyles: [], - inputStyle: [], - autoFocus: false, - autoCorrect: true, - - /** - * To be able to function as either controlled or uncontrolled component we should not - * assign a default prop value for `value` or `defaultValue` props - */ - value: undefined, - defaultValue: undefined, - forceActiveLabel: false, - disableKeyboard: false, - autoGrow: false, - autoGrowHeight: false, - hideFocusedState: false, - innerRef: () => {}, - shouldSaveDraft: false, - maxLength: null, - hint: '', - prefixCharacter: '', - onInputChange: () => {}, - shouldDelayFocus: false, - submitOnEnter: false, - icon: null, - shouldUseDefaultValue: false, - multiline: false, - shouldInterceptSwipe: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 1a4108bc1803..c5b57a8bc1f6 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -222,17 +222,17 @@ function BaseTextInput( * Set Value & activateLabel */ const setValue = (newValue: string) => { - const value = isMultiline ? newValue : newValue.replace(/\n/g, ' '); + const formattedValue = isMultiline ? newValue : newValue.replace(/\n/g, ' '); if (onInputChange) { - onInputChange(newValue); + onInputChange(formattedValue); } if (inputProps.onChangeText) { - Str.result(inputProps.onChangeText, newValue); + Str.result(inputProps.onChangeText, formattedValue); } - if (newValue && newValue.length > 0) { + if (formattedValue && formattedValue.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: if (value === undefined) { @@ -361,7 +361,7 @@ function BaseTextInput( !isMultiline && {height, lineHeight: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, @@ -395,7 +395,9 @@ function BaseTextInput( e.preventDefault()} + onMouseDown={(e) => { + e.preventDefault(); + }} accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} > diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 365715f588cd..01b727f9875f 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -293,7 +293,7 @@ function BaseTextInput( return ( <> @@ -378,7 +378,7 @@ function BaseTextInput( !isMultiline && Browser.isMobileChrome() && {boxSizing: 'content-box', height: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 761ad84ae2ca..550bcd0a6e18 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -6,7 +6,6 @@ import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; import BaseTextInputProps from './BaseTextInput/types'; import * as styleConst from './styleConst'; @@ -80,7 +79,5 @@ function TextInput( } TextInput.displayName = 'TextInput'; -TextInput.propTypes = baseTextInputPropTypes.propTypes; -TextInput.defaultProps = baseTextInputPropTypes.defaultProps; export default React.forwardRef(TextInput); From 4bc71cd09ed669d7565b81a58aa33a64059f4d31 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Dec 2023 16:16:55 +0100 Subject: [PATCH 025/231] fix: removed commented code --- src/components/TextInput/BaseTextInput/index.native.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 5432d485531c..3aae53046b97 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -111,12 +111,10 @@ function BaseTextInput( Animated.parallel([ Animated.spring(labelTranslateY, { toValue: translateY, - // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), Animated.spring(labelScale, { toValue: scale, - // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), ]).start(); From cd7f52b1a39a6b60613dac842d0c4fd6d3092667 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Tue, 5 Dec 2023 21:10:45 +0530 Subject: [PATCH 026/231] fix reposition of popover when window resize --- src/CONST.ts | 1 + .../settings/Wallet/PaymentMethodList.js | 5 +++-- .../settings/Wallet/WalletPage/WalletPage.js | 21 ++++++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 3d69c83c5c22..b461c702c53f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -689,6 +689,7 @@ const CONST = { TRIE_INITIALIZATION: 'trie_initialization', COMMENT_LENGTH_DEBOUNCE_TIME: 500, SEARCH_FOR_REPORTS_DEBOUNCE_TIME: 300, + RESIZE_DEBOUNCE_TIME: 100, }, PRIORITY_MODE: { GSD: 'gsd', diff --git a/src/pages/settings/Wallet/PaymentMethodList.js b/src/pages/settings/Wallet/PaymentMethodList.js index 5af4129aefbc..5ac623008fd4 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.js +++ b/src/pages/settings/Wallet/PaymentMethodList.js @@ -289,10 +289,11 @@ function PaymentMethodList({ title={translate('walletPage.addBankAccount')} icon={Expensicons.Plus} wrapperStyle={styles.paymentMethod} + ref={buttonRef} /> ), - [onPress, styles.paymentMethod, translate], + [onPress, styles.paymentMethod, translate, buttonRef], ); /** @@ -346,10 +347,10 @@ function PaymentMethodList({ keyExtractor={keyExtractor} ListEmptyComponent={shouldShowEmptyListMessage ? renderListEmptyComponent : null} ListHeaderComponent={listHeaderComponent} - ListFooterComponent={shouldShowAddBankAccount ? renderListFooterComponent : null} onContentSizeChange={onListContentSizeChange} scrollEnabled={shouldEnableScroll} /> + {shouldShowAddBankAccount && renderListFooterComponent()} {shouldShowAddPaymentMethodButton && ( diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js index 6f452eed3629..9dc6c878a1bf 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {ActivityIndicator, InteractionManager, ScrollView, View} from 'react-native'; +import {ActivityIndicator, Dimensions, InteractionManager, ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; @@ -283,8 +283,23 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod if (!shouldListenForResize) { return; } - setMenuPosition(); - }, [shouldListenForResize, setMenuPosition]); + const popoverPositionListener = Dimensions.addEventListener('change', () => { + if (!shouldShowAddPaymentMenu && !shouldShowDefaultDeleteMenu) { + return; + } + if (shouldShowAddPaymentMenu) { + _.debounce(setMenuPosition, CONST.TIMING.RESIZE_DEBOUNCE_TIME)(); + return; + } + setMenuPosition(); + }); + return () => { + if (!popoverPositionListener) { + return; + } + popoverPositionListener.remove(); + }; + }, [shouldShowAddPaymentMenu, shouldShowDefaultDeleteMenu, setMenuPosition, shouldListenForResize]); useEffect(() => { if (!shouldShowDefaultDeleteMenu) { From bbe0c5df315db8ca83fc7b4d30a6ba902f4b8a58 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Dec 2023 17:16:24 +0100 Subject: [PATCH 027/231] fix: resolve comments --- src/components/Checkbox.tsx | 2 +- .../TextInput/BaseTextInput/index.native.tsx | 12 ++++++------ src/components/TextInput/BaseTextInput/index.tsx | 7 ++----- src/components/TextInput/BaseTextInput/types.ts | 7 +++++++ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 39c8a484613b..90546296083e 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,4 +1,4 @@ -import React, {ForwardedRef, forwardRef, MouseEventHandler, KeyboardEvent as ReactKeyboardEvent} from 'react'; +import React, {type ForwardedRef, forwardRef, type MouseEventHandler, type KeyboardEvent as ReactKeyboardEvent} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 3aae53046b97..507882bca9ff 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -4,9 +4,9 @@ import { ActivityIndicator, Animated, FlexStyle, - GestureResponderEvent, - LayoutChangeEvent, - NativeSyntheticEvent, + type GestureResponderEvent, + type LayoutChangeEvent, + type NativeSyntheticEvent, StyleProp, StyleSheet, TextInput, @@ -35,7 +35,7 @@ import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import BaseTextInputProps from './types'; +import type BaseTextInputProps from './types'; function BaseTextInput( { @@ -83,7 +83,7 @@ function BaseTextInput( const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; @@ -180,7 +180,7 @@ function BaseTextInput( const layout = event.nativeEvent.layout; - setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setWidth((prevWidth: number | null) => (autoGrowHeight ? layout.width : prevWidth)); setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, [autoGrowHeight, multiline], diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index bf74d6b1cfc8..2fec81618fd7 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -40,7 +40,6 @@ import BaseTextInputProps from './types'; function BaseTextInput( { label = '', - // name = '', value = undefined, defaultValue = undefined, placeholder = '', @@ -57,12 +56,10 @@ function BaseTextInput( hideFocusedState = false, maxLength = undefined, hint = '', - // shouldSaveDraft = false, onInputChange = () => {}, shouldDelayFocus = false, submitOnEnter = false, multiline = false, - // shouldUseDefaultValue = false, shouldInterceptSwipe = false, autoCorrect = true, prefixCharacter, @@ -82,7 +79,7 @@ function BaseTextInput( const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; @@ -179,7 +176,7 @@ function BaseTextInput( const layout = event.nativeEvent.layout; - setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setWidth((prevWidth: number | null) => (autoGrowHeight ? layout.width : prevWidth)); setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, [autoGrowHeight, multiline], diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index e1a123fb0691..99f5d7d728ea 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -97,9 +97,16 @@ type CustomBaseTextInputProps = { /** Indicate whether or not the input should prevent swipe actions in tabs */ shouldInterceptSwipe?: boolean; + /** Should there be an error displayed */ hasError?: boolean; + + /** On Press handler */ onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; + + /** Should loading state should be displayed */ isLoading?: boolean; + + /** Type of autocomplete */ autoCompleteType?: string; }; From c0a9c6a47e613e22ac9df761ab0f8a7c1d062742 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 6 Dec 2023 09:45:15 +0100 Subject: [PATCH 028/231] fix: minor fix --- src/pages/home/report/ReportTypingIndicator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 6c6531955115..03d04f947f39 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -38,7 +38,7 @@ function ReportTypingIndicator({userTypingStatuses}) { return null; } - // If the user is typing on OldDot, firstUserTyping will be a string (the user's login) + // If the user is typing on OldDot, firstUserTyping will be a string (the user's displayName) const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false) || firstUserTyping; if (usersTyping.length === 1) { From e6ed623ea26306a6bc7c76f9ca61822acdaf8fd9 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 6 Dec 2023 10:31:24 +0100 Subject: [PATCH 029/231] fix: display name that should be hidden as someone --- src/pages/home/report/ReportTypingIndicator.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 03d04f947f39..dc3bc98e0c78 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -27,10 +27,11 @@ function ReportTypingIndicator({userTypingStatuses}) { const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; + const isUserTypingADisplayName = Number.isNaN(Number(firstUserTyping)) const firstUserTypingID = useMemo( - () => (firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), - [firstUserTyping], + () => (firstUserTyping && isUserTypingADisplayName ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), + [firstUserTyping, isUserTypingADisplayName], ); // If we are offline, the user typing statuses are not up-to-date so do not show them @@ -39,7 +40,7 @@ function ReportTypingIndicator({userTypingStatuses}) { } // If the user is typing on OldDot, firstUserTyping will be a string (the user's displayName) - const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false) || firstUserTyping; + const firstUserTypingDisplayName = isUserTypingADisplayName ? firstUserTyping : ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false); if (usersTyping.length === 1) { return ( From e186fd3a15fe08861d5bdfbf7b9dbaf02a046eda Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 6 Dec 2023 10:36:20 +0100 Subject: [PATCH 030/231] fix: run prettier --- src/pages/home/report/ReportTypingIndicator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index dc3bc98e0c78..b289d87f2fd1 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -27,7 +27,7 @@ function ReportTypingIndicator({userTypingStatuses}) { const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; - const isUserTypingADisplayName = Number.isNaN(Number(firstUserTyping)) + const isUserTypingADisplayName = Number.isNaN(Number(firstUserTyping)); const firstUserTypingID = useMemo( () => (firstUserTyping && isUserTypingADisplayName ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), From 1f5cac75d088c89f2e5442d42b5c28ad2a308542 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 6 Dec 2023 11:21:30 +0100 Subject: [PATCH 031/231] fix: address comments --- .../TextInput/BaseTextInput/index.native.tsx | 29 ++++++++----------- .../TextInput/BaseTextInput/index.tsx | 22 +++++++------- .../TextInput/BaseTextInput/types.ts | 1 + .../TextInput/TextInputLabel/index.tsx | 4 +-- src/components/TextInput/index.tsx | 11 +++---- 5 files changed, 32 insertions(+), 35 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 507882bca9ff..f2066a8b8271 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -40,29 +40,26 @@ import type BaseTextInputProps from './types'; function BaseTextInput( { label = '', - // name = '', - value = undefined, - defaultValue = undefined, + value, + defaultValue, placeholder = '', errorText = '', icon = null, - textInputContainerStyles = [], - containerStyles = [], - inputStyle = [], + textInputContainerStyles, + containerStyles, + inputStyle, forceActiveLabel = false, autoFocus = false, disableKeyboard = false, autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = undefined, + maxLength, hint = '', - // shouldSaveDraft = false, onInputChange = () => {}, shouldDelayFocus = false, submitOnEnter = false, multiline = false, - // shouldUseDefaultValue = false, shouldInterceptSwipe = false, autoCorrect = true, prefixCharacter, @@ -75,7 +72,7 @@ function BaseTextInput( const styles = useThemeStyles(); const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; - const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; const isMultiline = multiline || autoGrowHeight; const [isFocused, setIsFocused] = useState(false); @@ -251,7 +248,7 @@ function BaseTextInput( // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix: string) => { + const getCharacterPadding = (prefix: string): number => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -260,8 +257,6 @@ function BaseTextInput( } }; - // eslint-disable-next-line react/forbid-foreign-prop-types - // const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); const hasLabel = Boolean(label?.length); const isReadOnly = inputProps.readOnly ?? inputProps.disabled; const inputHelpText = errorText || hint; @@ -332,15 +327,15 @@ function BaseTextInput( )} { + ref={(element) => { if (typeof ref === 'function') { - ref(el); + ref(element); } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - ref.current = el; + ref.current = element; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = el; + input.current = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 2fec81618fd7..602d7c7cf6de 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -40,8 +40,8 @@ import BaseTextInputProps from './types'; function BaseTextInput( { label = '', - value = undefined, - defaultValue = undefined, + value, + defaultValue, placeholder = '', errorText = '', icon = null, @@ -54,7 +54,7 @@ function BaseTextInput( autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = undefined, + maxLength, hint = '', onInputChange = () => {}, shouldDelayFocus = false, @@ -66,13 +66,13 @@ function BaseTextInput( inputID, ...inputProps }: BaseTextInputProps, - ref: ForwardedRef, unknown, unknown>>, + ref: ForwardedRef>>, ) { const theme = useTheme(); const styles = useThemeStyles(); const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; - const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(inputProps.secureTextEntry); @@ -243,7 +243,7 @@ function BaseTextInput( // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix: string) => { + const getCharacterPadding = (prefix: string): number => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -276,7 +276,7 @@ function BaseTextInput( const lineHeight = useMemo(() => { if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(inputStyle)) { - const lineHeightValue = inputStyle?.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); + const lineHeightValue = inputStyle?.find((style) => style && 'lineHeight' in style && style.lineHeight !== undefined); if (lineHeightValue && 'lineHeight' in lineHeightValue) { return lineHeightValue.lineHeight; } @@ -340,15 +340,15 @@ function BaseTextInput( )} { + ref={(element) => { if (typeof ref === 'function') { - ref(el); + ref(element); } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - ref.current = el; + ref.current = element; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = el; + input.current = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 99f5d7d728ea..e2d761d4c067 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -111,5 +111,6 @@ type CustomBaseTextInputProps = { }; type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; + export default BaseTextInputProps; export type {CustomBaseTextInputProps}; diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx index 07ae16b74fd7..086218eb38d0 100644 --- a/src/components/TextInput/TextInputLabel/index.tsx +++ b/src/components/TextInput/TextInputLabel/index.tsx @@ -6,13 +6,13 @@ import TextInputLabelProps from './types'; function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: TextInputLabelProps) { const styles = useThemeStyles(); - const labelRef = useRef(null); + const labelRef = useRef(null); useEffect(() => { if (!inputId || !labelRef.current) { return; } - (labelRef.current as unknown as HTMLFormElement).setAttribute('for', inputId); + labelRef.current.setAttribute('for', inputId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 550bcd0a6e18..e83d3bd1efca 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -57,20 +57,21 @@ function TextInput( { - // @ts-expect-error We need to reassign this ref to the input ref - textInputRef.current = el; + ref={(element) => { + if (element) { + (textInputRef.current as HTMLElement | Component>) = element; + } if (!ref) { return; } if (typeof ref === 'function') { - ref(el); + ref(element); return; } // eslint-disable-next-line no-param-reassign - ref.current = el; + ref.current = element; }} inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, inputStyle]} textInputContainerStyles={[labelAnimationStyle as StyleProp, textInputContainerStyles]} From 052610c05133cdf2990323e85dd2cebf6f2b39ac Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 7 Dec 2023 10:03:40 +0100 Subject: [PATCH 032/231] fix: resolve comments --- src/components/RNTextInput.tsx | 5 ++++- .../TextInput/BaseTextInput/index.native.tsx | 9 ++++---- .../TextInput/BaseTextInput/index.tsx | 21 +++++++++---------- .../TextInput/BaseTextInput/types.ts | 7 +++++-- src/components/TextInput/index.native.tsx | 12 ++++------- src/components/TextInput/index.tsx | 20 +++++++++++------- 6 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index 28555abe3266..5c3a65998e25 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -1,8 +1,9 @@ -import React, {ForwardedRef} from 'react'; +import React, {Component, ForwardedRef} from 'react'; // eslint-disable-next-line no-restricted-imports import {TextInput, TextInputProps} from 'react-native'; import Animated, {AnimatedProps} from 'react-native-reanimated'; +type AnimatedTextInputRef = Component>; // Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); @@ -27,3 +28,5 @@ function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef, unknown, unknown>>, + ref: BaseTextInputRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -334,8 +335,8 @@ function BaseTextInput( // eslint-disable-next-line no-param-reassign ref.current = element; } - // @ts-expect-error We need to reassign this ref to the input ref - input.current = element; + + (input.current as AnimatedTextInputRef | null) = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 602d7c7cf6de..148d668dbdd4 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import React, {Component, ForwardedRef, forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import { ActivityIndicator, Animated, @@ -11,17 +11,15 @@ import { StyleSheet, TextInput, TextInputFocusEventData, - TextInputProps, View, ViewStyle, } from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import RNTextInput from '@components/RNTextInput'; +import RNTextInput, {AnimatedTextInputRef} from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; @@ -35,7 +33,8 @@ import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import BaseTextInputProps from './types'; +import type BaseTextInputProps from './types'; +import type {BaseTextInputRef} from './types'; function BaseTextInput( { @@ -45,9 +44,9 @@ function BaseTextInput( placeholder = '', errorText = '', icon = null, - textInputContainerStyles = [], - containerStyles = [], - inputStyle = [], + textInputContainerStyles, + containerStyles, + inputStyle, forceActiveLabel = false, autoFocus = false, disableKeyboard = false, @@ -66,7 +65,7 @@ function BaseTextInput( inputID, ...inputProps }: BaseTextInputProps, - ref: ForwardedRef>>, + ref: BaseTextInputRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -347,8 +346,8 @@ function BaseTextInput( // eslint-disable-next-line no-param-reassign ref.current = element; } - // @ts-expect-error We need to reassign this ref to the input ref - input.current = element; + + (input.current as AnimatedTextInputRef | null) = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index e2d761d4c067..b8171fc91b01 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,5 +1,6 @@ -import React from 'react'; +import React, {Component, ForwardedRef} from 'react'; import {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import {SrcProps} from '@components/Icon'; import {LocaleContextProps} from '@components/LocaleContextProvider'; @@ -110,7 +111,9 @@ type CustomBaseTextInputProps = { autoCompleteType?: string; }; +type BaseTextInputRef = ForwardedRef>>; + type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; export default BaseTextInputProps; -export type {CustomBaseTextInputProps}; +export type {CustomBaseTextInputProps, BaseTextInputRef}; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index 7b974e1233bc..bf8145b4b2cc 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -1,15 +1,11 @@ -import React, {Component, ForwardedRef, forwardRef, useEffect} from 'react'; -import {AppState, Keyboard, TextInputProps} from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; +import React, {forwardRef, useEffect} from 'react'; +import {AppState, Keyboard} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import BaseTextInputProps from './BaseTextInput/types'; +import BaseTextInputProps, {BaseTextInputRef} from './BaseTextInput/types'; // eslint-disable-next-line react/function-component-definition -const TextInput = ( - {inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, - ref: ForwardedRef, unknown, unknown>>, -) => { +const TextInput = ({inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, ref: BaseTextInputRef) => { const styles = useThemeStyles(); useEffect(() => { diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index e83d3bd1efca..aff41d67cf88 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,4 +1,4 @@ -import React, {Component, ForwardedRef, useEffect, useRef} from 'react'; +import React, {Component, useEffect, useRef} from 'react'; import {StyleProp, TextInputProps, ViewStyle} from 'react-native'; import {AnimatedProps} from 'react-native-reanimated'; import * as Browser from '@libs/Browser'; @@ -6,18 +6,22 @@ import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import BaseTextInputProps from './BaseTextInput/types'; +import type BaseTextInputProps from './BaseTextInput/types'; +import type {BaseTextInputRef} from './BaseTextInput/types'; import * as styleConst from './styleConst'; +type RemoveVisibilityListener = () => void; + function TextInput( {label = '', name = '', textInputContainerStyles, inputStyle, disableKeyboard = false, multiline = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, - ref: ForwardedRef, unknown, unknown>>, + ref: BaseTextInputRef, ) { const styles = useThemeStyles(); const textInputRef = useRef(null); - const removeVisibilityListenerRef = useRef<() => void>(null); + const removeVisibilityListenerRef = useRef(null); useEffect(() => { + let removeVisibilityListener = removeVisibilityListenerRef.current; if (disableKeyboard) { textInputRef.current?.setAttribute('inputmode', 'none'); } @@ -25,8 +29,8 @@ function TextInput( if (name) { textInputRef.current?.setAttribute('name', name); } - // @ts-expect-error We need to reassign this ref to the input ref - removeVisibilityListenerRef.current = Visibility.onVisibilityChange(() => { + + removeVisibilityListener = Visibility.onVisibilityChange(() => { if (!Browser.isMobileChrome() || !Visibility.isVisible() || !textInputRef.current || DomUtils.getActiveElement() !== textInputRef.current) { return; } @@ -35,10 +39,10 @@ function TextInput( }); return () => { - if (!removeVisibilityListenerRef.current) { + if (!removeVisibilityListener) { return; } - removeVisibilityListenerRef.current(); + removeVisibilityListener(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From 0b22b3b02f2ae18fd630b2f5109393994c468eb2 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 7 Dec 2023 10:25:16 +0100 Subject: [PATCH 033/231] fix: lint error --- src/components/TextInput/BaseTextInput/index.native.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 0bc9de782b92..fe6d483c4acf 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import React, {Component, ForwardedRef, forwardRef, useCallback, useEffect, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react'; import { ActivityIndicator, Animated, @@ -11,11 +11,9 @@ import { StyleSheet, TextInput, TextInputFocusEventData, - TextInputProps, View, ViewStyle, } from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; From 618b9492c7ee0d351e3c582ff7e8320c50e98bf7 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:21:58 +0100 Subject: [PATCH 034/231] fix review suggestions --- src/components/BlockingViews/FullPageNotFoundView.tsx | 1 + .../BlockingViews/FullPageOfflineBlockingView.tsx | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index 3893c83dc45a..31cc63ef01da 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -53,6 +53,7 @@ function FullPageNotFoundView({ }: FullPageNotFoundViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + if (shouldShow) { return ( <> diff --git a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx index c8f5592287f3..5f6cd31227fe 100644 --- a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx +++ b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx @@ -2,14 +2,10 @@ import React from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; import BlockingView from './BlockingView'; -type FullPageOfflineBlockingViewProps = { - /** Child elements */ - children: React.ReactNode; -}; - -function FullPageOfflineBlockingView({children}: FullPageOfflineBlockingViewProps) { +function FullPageOfflineBlockingView({children}: ChildrenProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); From cb7ba8208be15db5589ec7cddd53fe77a367ea48 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:36:38 +0100 Subject: [PATCH 035/231] migrate MoneyRequestHeaderStatusBar to typescript --- ...rStatusBar.js => MoneyRequestHeaderStatusBar.tsx} | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) rename src/components/{MoneyRequestHeaderStatusBar.js => MoneyRequestHeaderStatusBar.tsx} (80%) diff --git a/src/components/MoneyRequestHeaderStatusBar.js b/src/components/MoneyRequestHeaderStatusBar.tsx similarity index 80% rename from src/components/MoneyRequestHeaderStatusBar.js rename to src/components/MoneyRequestHeaderStatusBar.tsx index fc91678cf6fb..09ff6560e14d 100644 --- a/src/components/MoneyRequestHeaderStatusBar.js +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -1,21 +1,20 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import Text from './Text'; -const propTypes = { +type MoneyRequestHeaderStatusBarProps = { /** Title displayed in badge */ - title: PropTypes.string.isRequired, + title: string; /** Banner Description */ - description: PropTypes.string.isRequired, + description: string; /** Whether we show the border bottom */ - shouldShowBorderBottom: PropTypes.bool.isRequired, + shouldShowBorderBottom: boolean; }; -function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom}) { +function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom}: MoneyRequestHeaderStatusBarProps) { const styles = useThemeStyles(); const borderBottomStyle = shouldShowBorderBottom ? styles.borderBottom : {}; return ( @@ -31,6 +30,5 @@ function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom } MoneyRequestHeaderStatusBar.displayName = 'MoneyRequestHeaderStatusBar'; -MoneyRequestHeaderStatusBar.propTypes = propTypes; export default MoneyRequestHeaderStatusBar; From 3741ecf64429d71571f68b8f70373622125ae091 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 8 Dec 2023 14:04:43 +0700 Subject: [PATCH 036/231] fix: inconsistency between autocomplete address and displayed address --- src/components/AddressSearch/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 2fed1d153947..3b8727cada8d 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -192,7 +192,7 @@ function AddressSearch({ // amount of data massaging needs to happen for what the parent expects to get from this function. if (_.size(details)) { onPress({ - address: lodashGet(details, 'description'), + address: autocompleteData.description || lodashGet(details, 'formatted_address', ''), lat: lodashGet(details, 'geometry.location.lat', 0), lng: lodashGet(details, 'geometry.location.lng', 0), name: lodashGet(details, 'name'), @@ -261,7 +261,7 @@ function AddressSearch({ lat: lodashGet(details, 'geometry.location.lat', 0), lng: lodashGet(details, 'geometry.location.lng', 0), - address: lodashGet(details, 'formatted_address', ''), + address: autocompleteData.description || lodashGet(details, 'formatted_address', ''), }; // If the address is not in the US, use the full length state name since we're displaying the address's From a4013d8df0a453facfdb512008355da900a1b49e Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 8 Dec 2023 14:09:35 +0700 Subject: [PATCH 037/231] fix typo --- src/components/AddressSearch/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 3b8727cada8d..379804bf67c6 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -192,7 +192,7 @@ function AddressSearch({ // amount of data massaging needs to happen for what the parent expects to get from this function. if (_.size(details)) { onPress({ - address: autocompleteData.description || lodashGet(details, 'formatted_address', ''), + address: autocompleteData.description || lodashGet(details, 'description', ''), lat: lodashGet(details, 'geometry.location.lat', 0), lng: lodashGet(details, 'geometry.location.lng', 0), name: lodashGet(details, 'name'), From 2db612015f48d8a6e4ed57bb3554c45fdaabdabe Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sun, 10 Dec 2023 14:11:52 +0100 Subject: [PATCH 038/231] Use PersonalDetailsUtils.getDisplayNameOrDefault where possible --- src/components/ArchivedReportFooter.tsx | 6 +++--- src/libs/OptionsListUtils.js | 18 +++++++++--------- src/libs/PersonalDetailsUtils.js | 11 ++++++----- src/libs/ReportUtils.ts | 8 ++++---- src/libs/SidebarUtils.ts | 2 +- src/libs/actions/Task.js | 3 ++- src/pages/DetailsPage.js | 10 ++++++---- src/pages/ProfilePage.js | 3 ++- src/pages/ReportParticipantsPage.js | 2 +- .../ReportActionCompose/SuggestionMention.js | 6 ++++-- src/pages/home/report/ReportActionItem.js | 4 ++-- 11 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 3187bf3604e8..0f5956836773 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -30,14 +30,14 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}} const originalMessage = reportClosedAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED ? reportClosedAction.originalMessage : null; const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; - let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [report.ownerAccountID, 'displayName']); + let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[report.ownerAccountID ?? -1]); let oldDisplayName: string | undefined; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { const newAccountID = originalMessage?.newAccountID; const oldAccountID = originalMessage?.oldAccountID; - displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [newAccountID, 'displayName']); - oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [oldAccountID, 'displayName']); + displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[newAccountID ?? -1]); + oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[oldAccountID ?? -1]); } const shouldRenderHTML = archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index f06ca8e12fcc..6276dc0b0dea 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -201,7 +201,7 @@ function isPersonalDetailsReady(personalDetails) { function getParticipantsOption(participant, personalDetails) { const detail = getPersonalDetailsForAccountIDs([participant.accountID], personalDetails)[participant.accountID]; const login = detail.login || participant.login; - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, 'displayName', LocalePhoneNumber.formatPhoneNumber(login)); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login)); return { keyForList: String(detail.accountID), login, @@ -246,8 +246,7 @@ function getParticipantNames(personalDetailList) { participantNames.add(participant.lastName.toLowerCase()); } if (participant.displayName) { - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(participant, 'displayName'); - participantNames.add(displayName.toLowerCase()); + participantNames.add(PersonalDetailsUtils.getDisplayNameOrDefault(participant).toLowerCase()); } }); return participantNames; @@ -301,9 +300,9 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) // More info https://github.com/Expensify/App/issues/8007 searchTerms = searchTerms.concat([ - PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName'), + PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false), personalDetail.login, - personalDetail.login.replace(/\.(?=[^\s@]*@)/g, '') + personalDetail.login.replace(/\.(?=[^\s@]*@)/g, ''), ]); } } @@ -517,7 +516,8 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { const lastMessageTextFromReport = getLastMessageTextForReport(report); const lastActorDetails = personalDetailMap[report.lastActorAccountID] || null; - let lastMessageText = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName')}: ` : ''; + let lastMessageText = + hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails)}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; if (result.isArchivedRoom) { @@ -525,7 +525,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason) || CONST.REPORT.ARCHIVE_REASON.DEFAULT; lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), + displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails), policyName: ReportUtils.getPolicyName(report), }); } @@ -1438,8 +1438,8 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail, amountText) { const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login); return { - text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName', formattedLogin), - alternateText: formattedLogin || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName'), + text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, formattedLogin), + alternateText: formattedLogin || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false), icons: [ { source: UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 30b710e9d15c..0525540d1ee1 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -18,15 +18,16 @@ Onyx.connect({ }); /** - * @param {Object | Null} passedPersonalDetails - * @param {Array | String} pathToDisplayName + * @param {Object | Null | Undefined} passedPersonalDetails * @param {String} [defaultValue] optional default display name value + * @param {Boolean} [shouldFallbackToHidden] whether to fall back to 'hidden' if the display name and default value are empty * @returns {String} */ -function getDisplayNameOrDefault(passedPersonalDetails, pathToDisplayName, defaultValue = '') { - const displayName = lodashGet(passedPersonalDetails, pathToDisplayName, '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); +function getDisplayNameOrDefault(passedPersonalDetails, defaultValue = '', shouldFallbackToHidden = true) { + const displayName = lodashGet(passedPersonalDetails, 'displayName', '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); + const fallbackValue = shouldFallbackToHidden ? Localize.translateLocal('common.hidden') : ''; - return displayName || defaultValue || Localize.translateLocal('common.hidden'); + return displayName || defaultValue || fallbackValue; } /** diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2329827dd376..3bf8dbeca4aa 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1289,7 +1289,7 @@ function getIcons( const parentReportAction = ReportActionsUtils.getParentReportAction(report); const actorAccountID = parentReportAction.actorAccountID; - const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails, [actorAccountID ?? -1, 'displayName']); + const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[actorAccountID ?? -1]); const actorIcon = { id: actorAccountID, source: UserUtils.getAvatar(personalDetails?.[actorAccountID ?? -1]?.avatar ?? '', actorAccountID ?? -1), @@ -1413,13 +1413,13 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return formattedLogin; } - const longName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, 'displayName', formattedLogin); - - const shortName = personalDetails.firstName ? personalDetails.firstName : longName; + const longName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, formattedLogin, false); if (!longName && shouldFallbackToHidden) { return Localize.translateLocal('common.hidden'); } + + const shortName = personalDetails.firstName ? personalDetails.firstName : longName; return shouldUseShortForm ? shortName : longName; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 1da1469a2687..786846cc3d61 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -346,7 +346,7 @@ function getOptionData( case CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED: { lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { policyName: ReportUtils.getPolicyName(report, false, policy), - displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), + displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails), }); break; } diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index e5037d250d2e..b81f2a743ee8 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -8,6 +8,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; @@ -668,7 +669,7 @@ function getAssignee(assigneeAccountID, personalDetails) { } return { icons: ReportUtils.getIconsForParticipants([details.accountID], personalDetails), - displayName: details.displayName, + displayName: PersonalDetailsUtils.getDisplayNameOrDefault(details), subtitle: details.login, }; } diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 66345107dbb1..bf899ca27d4b 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -21,6 +21,7 @@ import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import compose from '@libs/compose'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import useThemeStyles from '@styles/useThemeStyles'; @@ -120,6 +121,7 @@ function DetailsPage(props) { const phoneNumber = getPhoneNumber(details); const phoneOrEmail = isSMSLogin ? getPhoneNumber(details) : details.login; + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details, '', false); const isCurrentUser = props.session.accountID === details.accountID; @@ -132,7 +134,7 @@ function DetailsPage(props) { )} - {Boolean(details.displayName) && ( + {Boolean(displayName) && ( - {details.displayName} + {displayName} )} {details.login ? ( @@ -194,7 +196,7 @@ function DetailsPage(props) { {!isCurrentUser && ( Report.navigateToAndOpenReport([login])} diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 97ec3f99da3c..1b9127e15ad2 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -25,6 +25,7 @@ import UserDetailsTooltip from '@components/UserDetailsTooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; @@ -103,7 +104,7 @@ function ProfilePage(props) { const accountID = Number(lodashGet(props.route.params, 'accountID', 0)); const details = lodashGet(props.personalDetails, accountID, ValidationUtils.isValidAccountRoute(accountID) ? {} : {isloading: false}); - const displayName = details.displayName ? details.displayName : props.translate('common.hidden'); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details); const avatar = lodashGet(details, 'avatar', UserUtils.getDefaultAvatar()); const fallbackIcon = lodashGet(details, 'fallbackIcon', ''); const originalFileName = lodashGet(details, 'originalFileName', ''); diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index ceaa53a41a6b..b40bdc1f7de1 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -60,7 +60,7 @@ const getAllParticipants = (report, personalDetails, translate) => .map((accountID, index) => { const userPersonalDetail = lodashGet(personalDetails, accountID, {displayName: personalDetails.displayName || translate('common.hidden'), avatar: ''}); const userLogin = LocalePhoneNumber.formatPhoneNumber(userPersonalDetail.login || '') || translate('common.hidden'); - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(userPersonalDetail, 'displayName'); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(userPersonalDetail); return { alternateText: userLogin, diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index e55b96ad99f5..af3074eec06d 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -7,6 +7,7 @@ import {usePersonalDetails} from '@components/OnyxProvider'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; @@ -149,7 +150,8 @@ function SuggestionMention({ if (!detail.login || detail.isOptimisticPersonalDetail) { return false; } - const displayText = detail.displayName === formatPhoneNumber(detail.login) ? detail.displayName : `${detail.displayName} ${detail.login}`; + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail); + const displayText = displayName === formatPhoneNumber(detail.login) ? displayName : `${displayName} ${detail.login}`; if (searchValue && !displayText.toLowerCase().includes(searchValue.toLowerCase())) { return false; } @@ -159,7 +161,7 @@ function SuggestionMention({ const sortedPersonalDetails = _.sortBy(filteredPersonalDetails, (detail) => detail.displayName || detail.login); _.each(_.first(sortedPersonalDetails, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length), (detail) => { suggestions.push({ - text: detail.displayName, + text: PersonalDetailsUtils.getDisplayNameOrDefault(detail), alternateText: formatPhoneNumber(detail.login), login: detail.login, icons: [ diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index f850daaa1ffb..c6484b93e93e 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -373,7 +373,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [props.report.ownerAccountID, 'displayName']); + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); const paymentType = lodashGet(props.action, 'originalMessage.paymentType', ''); const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID); @@ -421,7 +421,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [props.report.ownerAccountID, 'displayName']); + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); const amount = CurrencyUtils.convertToDisplayString(props.report.total, props.report.currency); children = ; From 12f6197d5e983f38f2775dabdff487ba9009fdf4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 10:10:51 +0100 Subject: [PATCH 039/231] fix: types --- src/components/TextInput/BaseTextInput/index.native.tsx | 1 + src/components/TextInput/BaseTextInput/index.tsx | 1 + src/components/TextInput/BaseTextInput/types.ts | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 8479e30f19ae..1e4671b32e1d 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -71,6 +71,7 @@ function BaseTextInput( const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {hasError = false} = inputProps; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; const isMultiline = multiline || autoGrowHeight; diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index f22a90867dc4..939a385bccf6 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -71,6 +71,7 @@ function BaseTextInput( const styles = useThemeStyles(); const {hasError = false} = inputProps; const StyleUtils = useStyleUtils(); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index b8171fc91b01..dcdfbc4a2c34 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -3,6 +3,7 @@ import {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, import {AnimatedProps} from 'react-native-reanimated'; import {SrcProps} from '@components/Icon'; import {LocaleContextProps} from '@components/LocaleContextProvider'; +import {MaybePhraseKey} from '@libs/Localize'; type CustomBaseTextInputProps = { /** Input label */ @@ -21,7 +22,7 @@ type CustomBaseTextInputProps = { placeholder?: string; /** Error text to display */ - errorText: string | string[] | Record; + errorText: MaybePhraseKey; /** Icon to display in right side of text input */ icon: ((props: SrcProps) => React.ReactNode) | null; From b48cee20ef14702a7f293f8507c52562060db404 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 12:29:41 +0100 Subject: [PATCH 040/231] fix: bring back proptypes as they are used in different file --- .../BaseTextInput/baseTextInputPropTypes.js | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js new file mode 100644 index 000000000000..5387d1ff81d1 --- /dev/null +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -0,0 +1,138 @@ +import PropTypes from 'prop-types'; + +const propTypes = { + /** Input label */ + label: PropTypes.string, + + /** Name attribute for the input */ + name: PropTypes.string, + + /** Input value */ + value: PropTypes.string, + + /** Default value - used for non controlled inputs */ + defaultValue: PropTypes.string, + + /** Input value placeholder */ + placeholder: PropTypes.string, + + /** Error text to display */ + errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + + /** Icon to display in right side of text input */ + icon: PropTypes.func, + + /** Customize the TextInput container */ + textInputContainerStyles: PropTypes.arrayOf(PropTypes.object), + + /** Customize the main container */ + containerStyles: PropTypes.arrayOf(PropTypes.object), + + /** input style */ + inputStyle: PropTypes.arrayOf(PropTypes.object), + + /** If present, this prop forces the label to remain in a position where it will not collide with input text */ + forceActiveLabel: PropTypes.bool, + + /** Should the input auto focus? */ + autoFocus: PropTypes.bool, + + /** Disable the virtual keyboard */ + disableKeyboard: PropTypes.bool, + + /** + * Autogrow input container length based on the entered text. + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrow: PropTypes.bool, + + /** + * Autogrow input container height based on the entered text + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrowHeight: PropTypes.bool, + + /** Hide the focus styles on TextInput */ + hideFocusedState: PropTypes.bool, + + /** Forward the inner ref */ + innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + + /** Maximum characters allowed */ + maxLength: PropTypes.number, + + /** Hint text to display below the TextInput */ + hint: PropTypes.string, + + /** Prefix character */ + prefixCharacter: PropTypes.string, + + /** Whether autoCorrect functionality should enable */ + autoCorrect: PropTypes.bool, + + /** Form props */ + /** The ID used to uniquely identify the input in a Form */ + inputID: PropTypes.string, + + /** Saves a draft of the input value when used in a form */ + shouldSaveDraft: PropTypes.bool, + + /** Callback to update the value on Form when input is used in the Form component. */ + onInputChange: PropTypes.func, + + /** Whether we should wait before focusing the TextInput, useful when using transitions */ + shouldDelayFocus: PropTypes.bool, + + /** Indicate whether pressing Enter on multiline input is allowed to submit the form. */ + submitOnEnter: PropTypes.bool, + + /** Indicate whether input is multiline */ + multiline: PropTypes.bool, + + /** Set the default value to the input if there is a valid saved value */ + shouldUseDefaultValue: PropTypes.bool, + + /** Indicate whether or not the input should prevent swipe actions in tabs */ + shouldInterceptSwipe: PropTypes.bool, +}; + +const defaultProps = { + label: '', + name: '', + errorText: '', + placeholder: '', + hasError: false, + containerStyles: [], + textInputContainerStyles: [], + inputStyle: [], + autoFocus: false, + autoCorrect: true, + + /** + * To be able to function as either controlled or uncontrolled component we should not + * assign a default prop value for `value` or `defaultValue` props + */ + value: undefined, + defaultValue: undefined, + forceActiveLabel: false, + disableKeyboard: false, + autoGrow: false, + autoGrowHeight: false, + hideFocusedState: false, + innerRef: () => {}, + shouldSaveDraft: false, + maxLength: null, + hint: '', + prefixCharacter: '', + onInputChange: () => {}, + shouldDelayFocus: false, + submitOnEnter: false, + icon: null, + shouldUseDefaultValue: false, + multiline: false, + shouldInterceptSwipe: false, +}; + +export {propTypes, defaultProps}; From f327d28c594b9b5464604f71b869acc9b63d896b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 15:28:22 +0100 Subject: [PATCH 041/231] fix: tests --- .../TextInput/BaseTextInput/index.native.tsx | 11 +++---- .../TextInput/BaseTextInput/index.tsx | 7 +++-- .../TextInput/BaseTextInput/types.ts | 3 +- src/components/TextInput/index.native.tsx | 13 ++++----- src/components/TextInput/index.tsx | 29 ++++++++----------- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 1e4671b32e1d..b6db50dfea29 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -24,7 +24,7 @@ import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; import TextInputLabel from '@components/TextInput/TextInputLabel'; -import withLocalize from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import getSecureEntryKeyboardType from '@libs/getSecureEntryKeyboardType'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import useNativeDriver from '@libs/useNativeDriver'; @@ -34,7 +34,7 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type BaseTextInputProps from './types'; -import {BaseTextInputRef} from './types'; +import type {BaseTextInputRef} from './types'; function BaseTextInput( { @@ -70,6 +70,7 @@ function BaseTextInput( const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); const {hasError = false} = inputProps; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; @@ -262,7 +263,7 @@ function BaseTextInput( const isReadOnly = inputProps.readOnly ?? inputProps.disabled; const inputHelpText = errorText || hint; const placeholderValue = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; - const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; + const maxHeight = StyleSheet.flatten(containerStyles)?.maxHeight; const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, @@ -394,7 +395,7 @@ function BaseTextInput( onMouseDown={(e) => { e.preventDefault(); }} - accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} + accessibilityLabel={translate?.('common.visible') ?? ''} > 0 || !!prefixCharacter; @@ -413,7 +414,7 @@ function BaseTextInput( onMouseDown={(e) => { e.preventDefault(); }} - accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} + accessibilityLabel={translate?.('common.visible') ?? ''} > >>; -type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; +type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; export default BaseTextInputProps; export type {CustomBaseTextInputProps, BaseTextInputRef}; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index bf8145b4b2cc..58d562caa5cd 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -4,12 +4,11 @@ import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; import BaseTextInputProps, {BaseTextInputRef} from './BaseTextInput/types'; -// eslint-disable-next-line react/function-component-definition -const TextInput = ({inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, ref: BaseTextInputRef) => { +function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { const styles = useThemeStyles(); useEffect(() => { - if (!disableKeyboard) { + if (!props.disableKeyboard) { return; } @@ -24,20 +23,20 @@ const TextInput = ({inputStyle, disableKeyboard = false, prefixCharacter, inputI return () => { appStateSubscription.remove(); }; - }, [disableKeyboard]); + }, [props.disableKeyboard]); return ( ); -}; +} TextInput.displayName = 'TextInput'; diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index aff41d67cf88..6e09fb327bf3 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,6 +1,5 @@ -import React, {Component, useEffect, useRef} from 'react'; -import {StyleProp, TextInputProps, ViewStyle} from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; +import React, {useEffect, useRef} from 'react'; +import {StyleProp, ViewStyle} from 'react-native'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; @@ -12,22 +11,19 @@ import * as styleConst from './styleConst'; type RemoveVisibilityListener = () => void; -function TextInput( - {label = '', name = '', textInputContainerStyles, inputStyle, disableKeyboard = false, multiline = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, - ref: BaseTextInputRef, -) { +function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { const styles = useThemeStyles(); - const textInputRef = useRef(null); + const textInputRef = useRef(null); const removeVisibilityListenerRef = useRef(null); useEffect(() => { let removeVisibilityListener = removeVisibilityListenerRef.current; - if (disableKeyboard) { + if (props.disableKeyboard) { textInputRef.current?.setAttribute('inputmode', 'none'); } - if (name) { - textInputRef.current?.setAttribute('name', name); + if (props.name) { + textInputRef.current?.setAttribute('name', props.name); } removeVisibilityListener = Visibility.onVisibilityChange(() => { @@ -47,7 +43,7 @@ function TextInput( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isLabeledMultiline = Boolean(label?.length) && multiline; + const isLabeledMultiline = Boolean(props.label?.length) && props.multiline; const labelAnimationStyle = { // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-translate-y': `${styleConst.ACTIVE_LABEL_TRANSLATE_Y}px`, @@ -62,9 +58,8 @@ function TextInput( // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={(element) => { - if (element) { - (textInputRef.current as HTMLElement | Component>) = element; - } + textInputRef.current = element as HTMLElement; + if (!ref) { return; } @@ -77,8 +72,8 @@ function TextInput( // eslint-disable-next-line no-param-reassign ref.current = element; }} - inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, inputStyle]} - textInputContainerStyles={[labelAnimationStyle as StyleProp, textInputContainerStyles]} + inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, props.inputStyle]} + textInputContainerStyles={[labelAnimationStyle as StyleProp, props.textInputContainerStyles]} /> ); } From b4597cbe4a921ce6349429a94685292751f1f177 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 16:13:35 +0100 Subject: [PATCH 042/231] fix: import types --- .../TextInput/BaseTextInput/index.native.tsx | 18 +++--------------- .../TextInput/BaseTextInput/index.tsx | 16 ++-------------- .../TextInput/TextInputLabel/index.native.tsx | 2 +- .../TextInput/TextInputLabel/index.tsx | 2 +- src/components/TextInput/index.native.tsx | 5 +++-- src/components/TextInput/index.tsx | 2 +- 6 files changed, 11 insertions(+), 34 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index b6db50dfea29..c0ed78922727 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,25 +1,13 @@ import Str from 'expensify-common/lib/str'; import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react'; -import { - ActivityIndicator, - Animated, - FlexStyle, - type GestureResponderEvent, - type LayoutChangeEvent, - type NativeSyntheticEvent, - StyleProp, - StyleSheet, - TextInput, - TextInputFocusEventData, - View, - ViewStyle, -} from 'react-native'; +import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; +import type {FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import RNTextInput, {AnimatedTextInputRef} from '@components/RNTextInput'; +import RNTextInput, {type AnimatedTextInputRef} from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 93c764c330bc..9d42d3b1be78 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,19 +1,7 @@ import Str from 'expensify-common/lib/str'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import { - ActivityIndicator, - Animated, - FlexStyle, - GestureResponderEvent, - LayoutChangeEvent, - NativeSyntheticEvent, - StyleProp, - StyleSheet, - TextInput, - TextInputFocusEventData, - View, - ViewStyle, -} from 'react-native'; +import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; +import type {FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; diff --git a/src/components/TextInput/TextInputLabel/index.native.tsx b/src/components/TextInput/TextInputLabel/index.native.tsx index 701ac6992d7d..c9ca3dbda7a8 100644 --- a/src/components/TextInput/TextInputLabel/index.native.tsx +++ b/src/components/TextInput/TextInputLabel/index.native.tsx @@ -2,7 +2,7 @@ import React, {useState} from 'react'; import {Animated} from 'react-native'; import * as styleConst from '@components/TextInput/styleConst'; import useThemeStyles from '@styles/useThemeStyles'; -import TextInputLabelProps from './types'; +import type TextInputLabelProps from './types'; function TextInputLabel({isLabelActive, label, labelScale, labelTranslateY}: TextInputLabelProps) { const styles = useThemeStyles(); diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx index 086218eb38d0..3a441e6ecaa0 100644 --- a/src/components/TextInput/TextInputLabel/index.tsx +++ b/src/components/TextInput/TextInputLabel/index.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useRef} from 'react'; import {Animated, Text} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -import TextInputLabelProps from './types'; +import type TextInputLabelProps from './types'; function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: TextInputLabelProps) { const styles = useThemeStyles(); diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index 58d562caa5cd..2041f33a641a 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -2,7 +2,8 @@ import React, {forwardRef, useEffect} from 'react'; import {AppState, Keyboard} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import BaseTextInputProps, {BaseTextInputRef} from './BaseTextInput/types'; +import type BaseTextInputProps from './BaseTextInput/types'; +import type {BaseTextInputRef} from './BaseTextInput/types'; function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { const styles = useThemeStyles(); @@ -27,12 +28,12 @@ function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { return ( ); diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 6e09fb327bf3..94640debf3d4 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useRef} from 'react'; -import {StyleProp, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; From 3abace7c03af33716326c87eb2953d6f59cb5b04 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 16:15:40 +0100 Subject: [PATCH 043/231] fix: import types --- src/components/TextInput/BaseTextInput/types.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 0430644b0357..c71be6dce94e 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,8 +1,9 @@ -import React, {Component, ForwardedRef} from 'react'; -import {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; -import {SrcProps} from '@components/Icon'; -import {MaybePhraseKey} from '@libs/Localize'; +import React from 'react'; +import type {Component, ForwardedRef} from 'react'; +import type {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import type {AnimatedProps} from 'react-native-reanimated'; +import type {SrcProps} from '@components/Icon'; +import type {MaybePhraseKey} from '@libs/Localize'; type CustomBaseTextInputProps = { /** Input label */ From 3bfdf189a251a7ea7132a21f48adc0d50dc94728 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 12 Dec 2023 14:51:25 +0700 Subject: [PATCH 044/231] display loading page for workspace page with section --- src/libs/actions/BankAccounts.ts | 34 ++++++++++++++++++- .../workspace/WorkspacePageWithSections.js | 34 +++++++++++++------ 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 8d0c59ab8d60..f7b7ec89c670 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -404,7 +404,39 @@ function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoD } function openWorkspaceView() { - API.read('OpenWorkspaceView', {}, {}); + API.read( + 'OpenWorkspaceView', + {}, + { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: true, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: false, + }, + }, + ], + }, + ); } function handlePlaidError(bankAccountID: number, error: string, errorDescription: string, plaidRequestID: string) { diff --git a/src/pages/workspace/WorkspacePageWithSections.js b/src/pages/workspace/WorkspacePageWithSections.js index bea94ed3ef4e..2450e2b29fb0 100644 --- a/src/pages/workspace/WorkspacePageWithSections.js +++ b/src/pages/workspace/WorkspacePageWithSections.js @@ -1,10 +1,11 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useEffect} from 'react'; +import React, {useEffect, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; @@ -87,12 +88,19 @@ function WorkspacePageWithSections({backButtonRoute, children, footer, guidesCal const styles = useThemeStyles(); useNetwork({onReconnect: () => fetchData(shouldSkipVBBACall)}); + const isLoading = lodashGet(reimbursementAccount, 'isLoading', true); const achState = lodashGet(reimbursementAccount, 'achData.state', ''); const hasVBA = achState === BankAccount.STATE.OPEN; const isUsingECard = lodashGet(user, 'isUsingExpensifyCard', false); const policyID = lodashGet(route, 'params.policyID'); const policyName = lodashGet(policy, 'name'); const content = children(hasVBA, policyID, isUsingECard); + const firstRender = useRef(true); + + useEffect(() => { + // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true + firstRender.current = false; + }, []); useEffect(() => { fetchData(shouldSkipVBBACall); @@ -117,17 +125,23 @@ function WorkspacePageWithSections({backButtonRoute, children, footer, guidesCal guidesCallTaskID={guidesCallTaskID} onBackButtonPress={() => Navigation.goBack(backButtonRoute || ROUTES.WORKSPACE_INITIAL.getRoute(policyID))} /> - {shouldUseScrollView ? ( - - {content} - + {isLoading || firstRender.current ? ( + ) : ( - content + <> + {shouldUseScrollView ? ( + + {content} + + ) : ( + content + )} + {footer} + )} - {footer} ); From cba69d03b7cd37e95affc751d3b3e44408212824 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 12 Dec 2023 10:56:08 +0100 Subject: [PATCH 045/231] fix: resolve comments --- src/components/TextInput/BaseTextInput/types.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index c71be6dce94e..38341c721928 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,6 +1,6 @@ import React from 'react'; import type {Component, ForwardedRef} from 'react'; -import type {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; import type {AnimatedProps} from 'react-native-reanimated'; import type {SrcProps} from '@components/Icon'; import type {MaybePhraseKey} from '@libs/Localize'; @@ -22,19 +22,19 @@ type CustomBaseTextInputProps = { placeholder?: string; /** Error text to display */ - errorText: MaybePhraseKey; + errorText?: MaybePhraseKey; /** Icon to display in right side of text input */ icon: ((props: SrcProps) => React.ReactNode) | null; /** Customize the TextInput container */ - textInputContainerStyles: StyleProp; + textInputContainerStyles?: StyleProp; /** Customize the main container */ - containerStyles: StyleProp; + containerStyles?: StyleProp; /** input style */ - inputStyle: StyleProp; + inputStyle?: StyleProp; /** If present, this prop forces the label to remain in a position where it will not collide with input text */ forceActiveLabel?: boolean; From 886aa60d76d7a07710294585c3bd20b9ab9c8c24 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 12 Dec 2023 14:10:56 +0100 Subject: [PATCH 046/231] fix: resolved comments --- src/components/RNTextInput.tsx | 1 - .../TextInput/BaseTextInput/index.native.tsx | 49 +++++++------- .../TextInput/BaseTextInput/index.tsx | 65 ++++++++++--------- .../TextInput/BaseTextInput/types.ts | 2 +- src/libs/isInputAutoFilled.ts | 3 +- 5 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index 5c3a65998e25..db75dc936589 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -7,7 +7,6 @@ type AnimatedTextInputRef = Component>; // Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); -// eslint-disable-next-line @typescript-eslint/no-explicit-any function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef>>) { return ( {}, shouldDelayFocus = false, @@ -49,17 +53,20 @@ function BaseTextInput( multiline = false, shouldInterceptSwipe = false, autoCorrect = true, - prefixCharacter, + prefixCharacter = '', inputID, - ...inputProps + ...props }: BaseTextInputProps, ref: BaseTextInputRef, ) { + const inputProps = {shouldSaveDraft: false, shouldUseDefaultValue: false, ...props}; const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); + const {hasError = false} = inputProps; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; @@ -73,8 +80,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -132,16 +138,12 @@ function BaseTextInput( }, [animateLabel, forceActiveLabel, prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { - if (inputProps.onFocus) { - inputProps.onFocus(event); - } + inputProps.onFocus?.(event); setIsFocused(true); }; const onBlur = (event: NativeSyntheticEvent) => { - if (inputProps.onBlur) { - inputProps.onBlur(event); - } + inputProps.onBlur?.(event); setIsFocused(false); }; @@ -150,9 +152,7 @@ function BaseTextInput( return; } - if (inputProps.onPress) { - inputProps.onPress(event); - } + inputProps.onPress?.(event); if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { input.current?.focus(); @@ -186,7 +186,7 @@ function BaseTextInput( isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current as unknown as Element) + isInputAutoFilled(input.current) ) { activateLabel(); } else { @@ -209,9 +209,7 @@ function BaseTextInput( const setValue = (newValue: string) => { const formattedValue = isMultiline ? newValue : newValue.replace(/\n/g, ' '); - if (onInputChange) { - onInputChange(formattedValue); - } + onInputChange?.(formattedValue); if (inputProps.onChangeText) { Str.result(inputProps.onChangeText, formattedValue); @@ -219,7 +217,7 @@ function BaseTextInput( if (formattedValue && formattedValue.length > 0) { hasValueRef.current = true; - // When the componment is uncontrolled, we need to manually activate the label: + // When the component is uncontrolled, we need to manually activate the label: if (value === undefined) { activateLabel(); } @@ -252,7 +250,7 @@ function BaseTextInput( const inputHelpText = errorText || hint; const placeholderValue = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; const maxHeight = StyleSheet.flatten(containerStyles)?.maxHeight; - const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), @@ -271,8 +269,7 @@ function BaseTextInput( } {}, shouldDelayFocus = false, @@ -49,8 +53,10 @@ function BaseTextInput( multiline = false, shouldInterceptSwipe = false, autoCorrect = true, - prefixCharacter, + prefixCharacter = '', inputID, + shouldSaveDraft = false, + shouldUseDefaultValue = false, ...inputProps }: BaseTextInputProps, ref: BaseTextInputRef, @@ -60,6 +66,8 @@ function BaseTextInput( const {hasError = false} = inputProps; const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); + + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; @@ -72,8 +80,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -109,7 +116,9 @@ function BaseTextInput( ); const activateLabel = useCallback(() => { - const newValue = value ?? ''; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const newValue = value || ''; if (newValue.length < 0 || isLabelActive.current) { return; @@ -120,7 +129,9 @@ function BaseTextInput( }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - const newValue = value ?? ''; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const newValue = value || ''; if (!!forceActiveLabel || newValue.length !== 0 || prefixCharacter) { return; @@ -131,16 +142,12 @@ function BaseTextInput( }, [animateLabel, forceActiveLabel, prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { - if (inputProps.onFocus) { - onFocus(event); - } + inputProps.onFocus?.(event); setIsFocused(true); }; const onBlur = (event: NativeSyntheticEvent) => { - if (inputProps.onBlur) { - inputProps.onBlur(event); - } + inputProps.onBlur?.(event); setIsFocused(false); }; @@ -149,9 +156,7 @@ function BaseTextInput( return; } - if (inputProps.onPress) { - inputProps.onPress(event); - } + inputProps.onPress?.(event); if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { input.current?.focus(); @@ -174,7 +179,9 @@ function BaseTextInput( // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = value ?? ''; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const inputValue = value || ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -185,7 +192,7 @@ function BaseTextInput( isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current as unknown as Element) + isInputAutoFilled(input.current) ) { activateLabel(); } else { @@ -206,9 +213,8 @@ function BaseTextInput( * Set Value & activateLabel */ const setValue = (newValue: string) => { - if (onInputChange) { - onInputChange(newValue); - } + onInputChange?.(newValue); + if (inputProps.onChangeText) { Str.result(inputProps.onChangeText, newValue); } @@ -247,7 +253,7 @@ function BaseTextInput( const inputHelpText = errorText || hint; const newPlaceholder = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; - const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), @@ -265,10 +271,10 @@ function BaseTextInput( See https://github.com/Expensify/App/issues/13802 */ const lineHeight = useMemo(() => { - if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(inputStyle)) { - const lineHeightValue = inputStyle?.find((style) => style && 'lineHeight' in style && style.lineHeight !== undefined); - if (lineHeightValue && 'lineHeight' in lineHeightValue) { - return lineHeightValue.lineHeight; + if (Browser.isSafari() || Browser.isMobileChrome()) { + const lineHeightValue = StyleSheet.flatten(inputStyle).lineHeight; + if (lineHeightValue !== undefined) { + return lineHeightValue; } } @@ -285,8 +291,7 @@ function BaseTextInput( } Date: Tue, 12 Dec 2023 19:55:56 +0100 Subject: [PATCH 047/231] remove files from another PR --- src/components/Text.tsx | 31 ++++++------ src/components/TextLink.js | 99 +++++++++++++++++++++++++++++++++++++ src/components/TextLink.tsx | 77 ----------------------------- 3 files changed, 113 insertions(+), 94 deletions(-) create mode 100644 src/components/TextLink.js delete mode 100644 src/components/TextLink.tsx diff --git a/src/components/Text.tsx b/src/components/Text.tsx index a289e224fb72..58a5cf300699 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -1,29 +1,28 @@ import React, {ForwardedRef} from 'react'; +// eslint-disable-next-line no-restricted-imports import {Text as RNText, TextProps as RNTextProps, StyleSheet} from 'react-native'; import type {TextStyle} from 'react-native'; import fontFamily from '@styles/fontFamily'; import useTheme from '@styles/themes/useTheme'; import variables from '@styles/variables'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; -type TextProps = RNTextProps & - ChildrenProps & { - /** The color of the text */ - color?: string; +type TextProps = RNTextProps & { + /** The color of the text */ + color?: string; - /** The size of the text */ - fontSize?: number; + /** The size of the text */ + fontSize?: number; + /** The alignment of the text */ + textAlign?: 'left' | 'right' | 'auto' | 'center' | 'justify'; + /** Any children to display */ + children: React.ReactNode; - /** The alignment of the text */ - textAlign?: TextStyle['textAlign']; + /** The family of the font to use */ + family?: keyof typeof fontFamily; +}; - /** The family of the font to use */ - family?: keyof typeof fontFamily; - }; - -function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', children, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef) { +function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', children = null, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef) { const theme = useTheme(); - const componentStyle: TextStyle = { color: color ?? theme.text, fontSize, @@ -35,7 +34,6 @@ function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', c if (!componentStyle.lineHeight && componentStyle.fontSize === variables.fontSizeNormal) { componentStyle.lineHeight = variables.fontSizeNormalHeight; } - return ( event.preventDefault(), +}; + +function TextLink(props) { + const styles = useThemeStyles(); + const rest = _.omit(props, _.keys(propTypes)); + const additionalStyles = _.isArray(props.style) ? props.style : [props.style]; + + /** + * @param {Event} event + */ + const openLink = (event) => { + event.preventDefault(); + if (props.onPress) { + props.onPress(); + return; + } + + Link.openExternalLink(props.href); + }; + + /** + * @param {Event} event + */ + const openLinkIfEnterKeyPressed = (event) => { + if (event.key !== 'Enter') { + return; + } + openLink(event); + }; + + return ( + + {props.children} + + ); +} + +TextLink.defaultProps = defaultProps; +TextLink.propTypes = propTypes; +TextLink.displayName = 'TextLink'; + +const TextLinkWithRef = React.forwardRef((props, ref) => ( + +)); + +TextLinkWithRef.displayName = 'TextLinkWithRef'; + +export default TextLinkWithRef; diff --git a/src/components/TextLink.tsx b/src/components/TextLink.tsx deleted file mode 100644 index fae4a71eeafa..000000000000 --- a/src/components/TextLink.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, {ForwardedRef, forwardRef, KeyboardEventHandler, MouseEventHandler} from 'react'; -import {GestureResponderEvent, Text as RNText, StyleProp, TextStyle} from 'react-native'; -import useThemeStyles from '@styles/useThemeStyles'; -import * as Link from '@userActions/Link'; -import CONST from '@src/CONST'; -import Text, {TextProps} from './Text'; - -type LinkProps = { - /** Link to open in new tab */ - href: string; - - onPress?: undefined; -}; - -type PressProps = { - href?: undefined; - - /** Overwrites the default link behavior with a custom callback */ - onPress: () => void; -}; - -type TextLinkProps = (LinkProps | PressProps) & - TextProps & { - /** Additional style props */ - style?: StyleProp; - - /** Callback that is called when mousedown is triggered */ - onMouseDown?: MouseEventHandler; - }; - -function TextLink({href, onPress, children, style, onMouseDown = (event) => event.preventDefault(), ...rest}: TextLinkProps, ref: ForwardedRef) { - const styles = useThemeStyles(); - - const openLink = () => { - if (onPress) { - onPress(); - } else { - Link.openExternalLink(href); - } - }; - - const openLinkOnTap = (event: GestureResponderEvent) => { - event.preventDefault(); - - openLink(); - }; - - const openLinkOnEnterKey: KeyboardEventHandler = (event) => { - if (event.key !== 'Enter') { - return; - } - event.preventDefault(); - - openLink(); - }; - - return ( - - {children} - - ); -} - -TextLink.displayName = 'TextLink'; - -export default forwardRef(TextLink); From ac1564eeb8d1c326cf3ed367547e529de5bf3ce2 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 10:49:31 +0100 Subject: [PATCH 048/231] fix: fixed default values for props --- src/components/TextInput/BaseTextInput/index.native.tsx | 2 +- src/components/TextInput/BaseTextInput/index.tsx | 6 ++---- src/components/TextInput/BaseTextInput/types.ts | 3 --- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index e7160b8d631f..49788a4646af 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -45,7 +45,7 @@ function BaseTextInput( autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = null, + maxLength = undefined, hint = '', onInputChange = () => {}, shouldDelayFocus = false, diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 1e997018b537..cf8c58160c96 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,7 +1,7 @@ import Str from 'expensify-common/lib/str'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; -import type {FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -45,7 +45,7 @@ function BaseTextInput( autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = null, + maxLength = undefined, hint = '', onInputChange = () => {}, shouldDelayFocus = false, @@ -55,8 +55,6 @@ function BaseTextInput( autoCorrect = true, prefixCharacter = '', inputID, - shouldSaveDraft = false, - shouldUseDefaultValue = false, ...inputProps }: BaseTextInputProps, ref: BaseTextInputRef, diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index ee3574ee6d04..33addec32784 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -62,9 +62,6 @@ type CustomBaseTextInputProps = { /** Hide the focus styles on TextInput */ hideFocusedState?: boolean; - /** Maximum characters allowed */ - maxLength?: number | null; - /** Hint text to display below the TextInput */ hint?: string; From d69e08774e3fe2bf9eb4802c7277ed555f401012 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 13:20:42 +0100 Subject: [PATCH 049/231] fix: adress comments --- .../TextInput/BaseTextInput/index.native.tsx | 11 +++++------ src/components/TextInput/BaseTextInput/index.tsx | 15 ++++----------- src/components/TextInput/BaseTextInput/types.ts | 3 +-- src/components/TextInput/index.tsx | 3 +-- src/libs/isInputAutoFilled.ts | 4 ++-- 5 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 49788a4646af..671b3fdbad5f 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -21,8 +21,7 @@ import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type BaseTextInputProps from './types'; -import type {BaseTextInputRef} from './types'; +import type {BaseTextInputProps, BaseTextInputRef} from './types'; function BaseTextInput( { @@ -66,7 +65,7 @@ function BaseTextInput( const {translate} = useLocalize(); const {hasError = false} = inputProps; - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // Disabling this line for safeness as nullish coalescing works only if the value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; @@ -80,7 +79,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -377,8 +376,8 @@ function BaseTextInput( { - e.preventDefault(); + onMouseDown={(event) => { + event.preventDefault(); }} accessibilityLabel={translate?.('common.visible') ?? ''} > diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index cf8c58160c96..b2eae4887baf 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -21,8 +21,7 @@ import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type BaseTextInputProps from './types'; -import type {BaseTextInputRef} from './types'; +import type {BaseTextInputProps, BaseTextInputRef} from './types'; function BaseTextInput( { @@ -114,9 +113,7 @@ function BaseTextInput( ); const activateLabel = useCallback(() => { - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const newValue = value || ''; + const newValue = value ?? ''; if (newValue.length < 0 || isLabelActive.current) { return; @@ -127,9 +124,7 @@ function BaseTextInput( }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const newValue = value || ''; + const newValue = value ?? ''; if (!!forceActiveLabel || newValue.length !== 0 || prefixCharacter) { return; @@ -177,9 +172,7 @@ function BaseTextInput( // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const inputValue = value || ''; + const inputValue = value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 33addec32784..37dcb0de8f31 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -113,5 +113,4 @@ type BaseTextInputRef = ForwardedRef void; diff --git a/src/libs/isInputAutoFilled.ts b/src/libs/isInputAutoFilled.ts index f74b04108cd9..fbe6240def47 100644 --- a/src/libs/isInputAutoFilled.ts +++ b/src/libs/isInputAutoFilled.ts @@ -4,8 +4,8 @@ import isSelectorSupported from './isSelectorSupported'; /** * Check the input is auto filled or not */ -export default function isInputAutoFilled(input: (TextInput & HTMLElement) | null): boolean { - if (!input?.matches) { +export default function isInputAutoFilled(input: (TextInput | HTMLElement) | null): boolean { + if ((!!input && !('matches' in input)) || !input?.matches) { return false; } if (isSelectorSupported(':autofill')) { From 76cb656e4a593998e9712940e43ff9f462c8836b Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 14 Dec 2023 00:48:36 +0700 Subject: [PATCH 050/231] fix prevent user from replacing receipt when scanning --- src/components/AttachmentModal.js | 2 +- src/libs/ReportUtils.ts | 5 ++++- src/pages/EditRequestPage.js | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 79be536945ac..38c4ceef13a4 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -366,7 +366,7 @@ function AttachmentModal(props) { const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; const canEdit = - ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, props.parentReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT) && + ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, props.parentReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT, props.transaction) && !TransactionUtils.isDistanceRequest(props.transaction); if (canEdit) { menuItems.push({ diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 63037416c923..20d663bce8ea 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1839,7 +1839,7 @@ function canEditMoneyRequest(reportAction: OnyxEntry, fieldToEdit * Checks if the current user can edit the provided property of a money request * */ -function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, reportID: string, fieldToEdit: ValueOf): boolean { +function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, reportID: string, fieldToEdit: ValueOf, transaction: Transaction): boolean { // A list of fields that cannot be edited by anyone, once a money request has been settled const nonEditableFieldsWhenSettled: string[] = [ CONST.EDIT_REQUEST_FIELD.AMOUNT, @@ -1853,6 +1853,9 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, repor if (!canEditMoneyRequest(reportAction, fieldToEdit)) { return false; // User doesn't have permission to edit } + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT && TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { + return false; + } // Checks if the report is settled // Checks if the provided property is a restricted one diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 95313bea142d..b10d312303c3 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -107,7 +107,7 @@ function EditRequestPage({report, route, parentReport, policyCategories, policyT // Decides whether to allow or disallow editing a money request useEffect(() => { // Do not dismiss the modal, when a current user can edit this property of the money request. - if (ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, parentReport.reportID, fieldToEdit)) { + if (ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, parentReport.reportID, fieldToEdit, transaction)) { return; } @@ -115,7 +115,7 @@ function EditRequestPage({report, route, parentReport, policyCategories, policyT Navigation.isNavigationReady().then(() => { Navigation.dismissModal(); }); - }, [parentReportAction, parentReport.reportID, fieldToEdit]); + }, [parentReportAction, parentReport.reportID, fieldToEdit, transaction]); // Update the transaction object and close the modal function editMoneyRequest(transactionChanges) { From 53ce40255edc8cb021c759735b2bb010a9d23c2d Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Wed, 13 Dec 2023 19:54:50 +0100 Subject: [PATCH 051/231] add review suggested changes --- src/components/BlockingViews/BlockingView.tsx | 4 ++-- src/components/BlockingViews/FullPageNotFoundView.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/BlockingViews/BlockingView.tsx b/src/components/BlockingViews/BlockingView.tsx index ede40cfda752..e48f2a6917c0 100644 --- a/src/components/BlockingViews/BlockingView.tsx +++ b/src/components/BlockingViews/BlockingView.tsx @@ -53,8 +53,8 @@ function BlockingView({ shouldShowLink = false, iconWidth = variables.iconSizeSuperLarge, iconHeight = variables.iconSizeSuperLarge, - onLinkPress = () => Navigation.dismissModal, - shouldEmbedLinkWithSubtitle, + onLinkPress = () => Navigation.dismissModal(), + shouldEmbedLinkWithSubtitle = false, }: BlockingViewProps) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index 31cc63ef01da..733662251515 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -49,7 +49,7 @@ function FullPageNotFoundView({ onBackButtonPress = () => Navigation.goBack(ROUTES.HOME), shouldShowLink = true, shouldShowBackButton = true, - onLinkPress = () => Navigation.dismissModal, + onLinkPress = () => Navigation.dismissModal(), }: FullPageNotFoundViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); From 1bcec292dc90e3ae49503607486793e6f5201e7e Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Thu, 14 Dec 2023 00:24:10 +0100 Subject: [PATCH 052/231] Show the GBR only if the harvesting is disabled --- src/libs/actions/IOU.js | 33 +++++++++++++++++++++++------ src/types/onyx/Policy.ts | 5 ++++- tests/utils/LHNTestUtils.js | 1 + tests/utils/collections/policies.ts | 1 + 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7513989d08c4..638e0e55631f 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -293,6 +293,7 @@ function buildOnyxDataForMoneyRequest( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, + hasOutstandingChildRequest = false, ) { const optimisticData = [ { @@ -304,6 +305,7 @@ function buildOnyxDataForMoneyRequest( lastReadTime: DateUtils.getDBTime(), lastMessageTranslationKey: '', iouReportID: iouReport.reportID, + hasOutstandingChildRequest, ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), }, }, @@ -612,12 +614,21 @@ function getMoneyRequestInformation( const isNewIOUReport = !chatReport.iouReportID || ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(chatReport); let iouReport = isNewIOUReport ? null : allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]; - // If the linked expense report on paid policy is not draft, we need to create a new draft expense report - if (isPolicyExpenseChat && iouReport) { - const policyType = ReportUtils.getPolicy(iouReport.policyID).type || ''; - const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; - if (isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport)) { - iouReport = null; + // Check if the scheduled submit is enabled in case of expense report + let needsToBeManuallySubmitted = false; + let policy = {}; + let isFromPaidPolicy = false; + if (isPolicyExpenseChat) { + policy = ReportUtils.getPolicy(iouReport.policyID); + const policyType = policy.type || ''; + isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; + needsToBeManuallySubmitted = policy.isHarvestingEnabled || false; + + // If the linked expense report on paid policy is not draft, we need to create a new draft expense report + if (iouReport) { + if (isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport)) { + iouReport = null; + } } } @@ -728,6 +739,10 @@ function getMoneyRequestInformation( }, } : undefined; + + // The policy expense chat should have the GBR only when its a paid policy and the scheduled submit is turned off + // so the employee has to submit to tits manager manually. + const hasOutstandingChildRequest = isPolicyExpenseChat && isFromPaidPolicy && needsToBeManuallySubmitted; // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest( @@ -743,6 +758,7 @@ function getMoneyRequestInformation( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, + hasOutstandingChildRequest, ); return { @@ -2945,6 +2961,7 @@ function approveMoneyRequest(expenseReport) { function submitReport(expenseReport) { const optimisticSubmittedReportAction = ReportUtils.buildOptimisticSubmittedReportAction(expenseReport.total, expenseReport.currency, expenseReport.reportID); const parentReport = ReportUtils.getReport(expenseReport.parentReportID); + const isCurrentUserManager = currentUserPersonalDetails.accountID === expenseReport.managerID; const optimisticData = [ { @@ -2976,7 +2993,9 @@ function submitReport(expenseReport) { key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, value: { ...parentReport, - hasOutstandingChildRequest: false, + + // In case its a manager who force submitted the report, they are the next user who needs to take an action + hasOutstandingChildRequest: isCurrentUserManager, iouReportID: null, }, }, diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index a55a7c052b01..d1f893c3c855 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -68,12 +68,15 @@ type Policy = { /** Whether policy expense chats can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ isPolicyExpenseChatEnabled: boolean; - /** Whether the scheduled submit is enabled */ + /** Whether the auto reporting is enabled */ autoReporting: boolean; /** The scheduled submit frequency set up on the this policy */ autoReportingFrequency: ValueOf; + /** Whether the scheduled submit is enabled */ + isHarvestingEnabled: boolean, + /** The employee list of the policy */ employeeList?: []; }; diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index 8385b3fd3e4b..91e13046689b 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -256,6 +256,7 @@ function getFakePolicy(id = 1, name = 'Workspace-Test-001') { lastModified: 1697323926777105, autoReporting: true, autoReportingFrequency: 'immediate', + isHarvestingEnabled: true, defaultBillable: false, disabledFields: {defaultBillable: true, reimbursable: false}, }; diff --git a/tests/utils/collections/policies.ts b/tests/utils/collections/policies.ts index 266c8bba2d72..4f608e074694 100644 --- a/tests/utils/collections/policies.ts +++ b/tests/utils/collections/policies.ts @@ -11,6 +11,7 @@ export default function createRandomPolicy(index: number): Policy { autoReporting: randBoolean(), isPolicyExpenseChatEnabled: randBoolean(), autoReportingFrequency: rand(Object.values(CONST.POLICY.AUTO_REPORTING_FREQUENCIES)), + isHarvestingEnabled: randBoolean(), outputCurrency: randCurrencyCode(), role: rand(Object.values(CONST.POLICY.ROLE)), owner: randEmail(), From d09f2d8890d0b0f22c504c9b84c2c5edd8005ca8 Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Thu, 14 Dec 2023 00:25:22 +0100 Subject: [PATCH 053/231] Fix style --- src/libs/actions/IOU.js | 2 +- src/types/onyx/Policy.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 638e0e55631f..30da17cf9ce8 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -739,7 +739,7 @@ function getMoneyRequestInformation( }, } : undefined; - + // The policy expense chat should have the GBR only when its a paid policy and the scheduled submit is turned off // so the employee has to submit to tits manager manually. const hasOutstandingChildRequest = isPolicyExpenseChat && isFromPaidPolicy && needsToBeManuallySubmitted; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index d1f893c3c855..bfed87262d63 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -75,7 +75,7 @@ type Policy = { autoReportingFrequency: ValueOf; /** Whether the scheduled submit is enabled */ - isHarvestingEnabled: boolean, + isHarvestingEnabled: boolean; /** The employee list of the policy */ employeeList?: []; From e4247c3b3a5471c9b81faf4558e645be28eae93a Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Thu, 14 Dec 2023 00:58:19 +0100 Subject: [PATCH 054/231] Get the policyID from the chat report --- src/libs/actions/IOU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 30da17cf9ce8..6e7e71c6eade 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -619,7 +619,7 @@ function getMoneyRequestInformation( let policy = {}; let isFromPaidPolicy = false; if (isPolicyExpenseChat) { - policy = ReportUtils.getPolicy(iouReport.policyID); + policy = ReportUtils.getPolicy(chatReport.policyID); const policyType = policy.type || ''; isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; needsToBeManuallySubmitted = policy.isHarvestingEnabled || false; From fa251dfb5e427b3045790da5e78bb091f0f9641f Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 14 Dec 2023 14:04:55 +0100 Subject: [PATCH 055/231] fix: typecheck --- src/components/TextInput/BaseTextInput/index.tsx | 2 +- src/components/TextInput/index.native.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index f1bf7c0e6b83..dae61d82b609 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -286,7 +286,7 @@ function BaseTextInput( style={[ autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, typeof maxHeight === 'number' ? maxHeight : 0), !isMultiline && styles.componentHeightLarge, - ...props.containerStyles, + containerStyles, ]} > Date: Thu, 14 Dec 2023 16:44:23 +0100 Subject: [PATCH 056/231] [TS migration] Migrate 'OptionRow.js' component --- src/components/Icon/index.tsx | 1 + src/components/OfflineWithFeedback.tsx | 6 +- .../{OptionRow.js => OptionRow.tsx} | 239 +++++++++--------- .../Pressable/GenericPressable/types.ts | 2 +- src/libs/ReportUtils.ts | 13 +- src/styles/utils/index.ts | 6 +- src/types/onyx/Report.ts | 2 +- 7 files changed, 137 insertions(+), 132 deletions(-) rename src/components/{OptionRow.js => OptionRow.tsx} (57%) diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 80abe1872c12..9be549efac26 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -103,3 +103,4 @@ class Icon extends PureComponent { } export default withTheme(withThemeStyles(withStyleUtils(Icon))); +export type {SrcProps}; diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 5fcf1fe7442b..29fd0c2700dc 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -18,13 +18,13 @@ import MessagesRow from './MessagesRow'; type OfflineWithFeedbackProps = ChildrenProps & { /** The type of action that's pending */ - pendingAction: OnyxCommon.PendingAction; + pendingAction?: OnyxCommon.PendingAction; /** Determine whether to hide the component's children if deletion is pending */ shouldHideOnDelete?: boolean; /** The errors to display */ - errors?: OnyxCommon.Errors; + errors?: OnyxCommon.Errors | null; /** Whether we should show the error messages */ shouldShowErrorMessages?: boolean; @@ -56,7 +56,7 @@ type OfflineWithFeedbackProps = ChildrenProps & { type StrikethroughProps = Partial & {style: Array}; -function omitBy(obj: Record | undefined, predicate: (value: T) => boolean) { +function omitBy(obj: Record | undefined | null, predicate: (value: T) => boolean) { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars return Object.fromEntries(Object.entries(obj ?? {}).filter(([_, value]) => !predicate(value))); } diff --git a/src/components/OptionRow.js b/src/components/OptionRow.tsx similarity index 57% rename from src/components/OptionRow.js rename to src/components/OptionRow.tsx index 1a8c395ddd8b..d5661d3ef5bf 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.tsx @@ -1,13 +1,13 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import lodashIsEqual from 'lodash/isEqual'; import React, {useEffect, useRef, useState} from 'react'; -import {InteractionManager, StyleSheet, View} from 'react-native'; -import _ from 'underscore'; +import {InteractionManager, StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import type {OptionData} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import Button from './Button'; import DisplayNames from './DisplayNames'; @@ -16,156 +16,150 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import MultipleAvatars from './MultipleAvatars'; import OfflineWithFeedback from './OfflineWithFeedback'; -import optionPropTypes from './optionPropTypes'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import SelectCircle from './SelectCircle'; import SubscriptAvatar from './SubscriptAvatar'; import Text from './Text'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -const propTypes = { +type OptionRowProps = { /** Style for hovered state */ - // eslint-disable-next-line react/forbid-prop-types - hoverStyle: PropTypes.object, + hoverStyle?: StyleProp; /** Option to allow the user to choose from can be type 'report' or 'user' */ - option: optionPropTypes.isRequired, + option: OptionData; /** Whether this option is currently in focus so we can modify its style */ - optionIsFocused: PropTypes.bool, + optionIsFocused?: boolean; /** A function that is called when an option is selected. Selected option is passed as a param */ - onSelectRow: PropTypes.func, + onSelectRow?: (option: OptionData, refElement: View | HTMLDivElement | null) => void | Promise; /** Whether we should show the selected state */ - showSelectedState: PropTypes.bool, + showSelectedState?: boolean; /** Whether to show a button pill instead of a tickbox */ - shouldShowSelectedStateAsButton: PropTypes.bool, + shouldShowSelectedStateAsButton?: boolean; /** Text for button pill */ - selectedStateButtonText: PropTypes.string, + selectedStateButtonText?: string; /** Callback to fire when the multiple selector (tickbox or button) is clicked */ - onSelectedStatePressed: PropTypes.func, + onSelectedStatePressed?: (option: OptionData) => void; /** Whether we highlight selected option */ - highlightSelected: PropTypes.bool, + highlightSelected?: boolean; /** Whether this item is selected */ - isSelected: PropTypes.bool, + isSelected?: boolean; /** Display the text of the option in bold font style */ - boldStyle: PropTypes.bool, + boldStyle?: boolean; /** Whether to show the title tooltip */ - showTitleTooltip: PropTypes.bool, + showTitleTooltip?: boolean; /** Whether this option should be disabled */ - isDisabled: PropTypes.bool, + isDisabled?: boolean; /** Whether to show a line separating options in list */ - shouldHaveOptionSeparator: PropTypes.bool, + shouldHaveOptionSeparator?: boolean; /** Whether to remove the lateral padding and align the content with the margins */ - shouldDisableRowInnerPadding: PropTypes.bool, + shouldDisableRowInnerPadding?: boolean; /** Whether to prevent default focusing on select */ - shouldPreventDefaultFocusOnSelectRow: PropTypes.bool, + shouldPreventDefaultFocusOnSelectRow?: boolean; /** Whether to wrap large text up to 2 lines */ - isMultilineSupported: PropTypes.bool, + isMultilineSupported?: boolean; - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style?: StyleProp; - ...withLocalizePropTypes, + backgroundColor?: string; }; -const defaultProps = { - hoverStyle: undefined, - showSelectedState: false, - shouldShowSelectedStateAsButton: false, - selectedStateButtonText: 'Select', - onSelectedStatePressed: () => {}, - highlightSelected: false, - isSelected: false, - boldStyle: false, - showTitleTooltip: false, - onSelectRow: undefined, - isDisabled: false, - optionIsFocused: false, - isMultilineSupported: false, - style: null, - shouldHaveOptionSeparator: false, - shouldDisableRowInnerPadding: false, - shouldPreventDefaultFocusOnSelectRow: false, -}; - -function OptionRow(props) { +function OptionRow({ + option, + onSelectRow, + style, + hoverStyle, + isDisabled: isOptionDisabled = false, + isMultilineSupported = false, + shouldShowSelectedStateAsButton = false, + highlightSelected = false, + selectedStateButtonText = 'Select', + shouldHaveOptionSeparator = false, + showTitleTooltip = false, + optionIsFocused = false, + boldStyle = false, + onSelectedStatePressed = () => {}, + backgroundColor, + isSelected = false, + showSelectedState = false, + shouldDisableRowInnerPadding = false, + shouldPreventDefaultFocusOnSelectRow = false, +}: OptionRowProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const pressableRef = useRef(null); - const [isDisabled, setIsDisabled] = useState(props.isDisabled); + const {translate} = useLocalize(); + const pressableRef = useRef(null); + const [isDisabled, setIsDisabled] = useState(isOptionDisabled); useEffect(() => { - setIsDisabled(props.isDisabled); - }, [props.isDisabled]); + setIsDisabled(isOptionDisabled); + }, [isOptionDisabled]); - const text = lodashGet(props.option, 'text', ''); - const fullTitle = props.isMultilineSupported ? text.trimStart() : text; + const text = option.text ?? ''; + const fullTitle = isMultilineSupported ? text.trimStart() : text; const indentsLength = text.length - fullTitle.length; const paddingLeft = Math.floor(indentsLength / CONST.INDENTS.length) * styles.ml3.marginLeft; - const textStyle = props.optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; - const textUnreadStyle = props.boldStyle || props.option.boldStyle ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; - const displayNameStyle = StyleUtils.combineStyles( + const textStyle = optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; + const textUnreadStyle = boldStyle || option.boldStyle ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; + const displayNameStyle: StyleProp = [ styles.optionDisplayName, textUnreadStyle, - props.style, + style, styles.pre, isDisabled ? styles.optionRowDisabled : {}, - props.isMultilineSupported ? {paddingLeft} : {}, - ); - const alternateTextStyle = StyleUtils.combineStyles( + isMultilineSupported ? {paddingLeft} : {}, + ]; + const alternateTextStyle: StyleProp = [ textStyle, styles.optionAlternateText, styles.textLabelSupporting, - props.style, - lodashGet(props.option, 'alternateTextMaxLines', 1) === 1 ? styles.pre : styles.preWrap, - ); + style, + (option.alternateTextMaxLines ?? 1) === 1 ? styles.pre : styles.preWrap, + ]; const contentContainerStyles = [styles.flex1]; + const flattenHoverStyle = StyleSheet.flatten(hoverStyle); const sidebarInnerRowStyle = StyleSheet.flatten([styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter]); - const hoveredBackgroundColor = - (props.hoverStyle || styles.sidebarLinkHover) && (props.hoverStyle || styles.sidebarLinkHover).backgroundColor - ? (props.hoverStyle || styles.sidebarLinkHover).backgroundColor - : props.backgroundColor; + const hoveredStyle = flattenHoverStyle ?? styles.sidebarLinkHover; + const hoveredBackgroundColor = hoveredStyle?.backgroundColor ? (hoveredStyle.backgroundColor as string) : backgroundColor; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; - const isMultipleParticipant = lodashGet(props.option, 'participantsList.length', 0) > 1; - const defaultSubscriptSize = props.option.isExpenseRequest ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; + const isMultipleParticipant = (option.participantsList?.length ?? 0) > 1; + const defaultSubscriptSize = option.isExpenseRequest ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips( - (props.option.participantsList || (props.option.accountID ? [props.option] : [])).slice(0, 10), - isMultipleParticipant, - ); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((option.participantsList || (option.accountID ? [option] : [])).slice(0, 10), isMultipleParticipant); let subscriptColor = theme.appBG; - if (props.optionIsFocused) { + if (optionIsFocused) { subscriptColor = focusedBackgroundColor; } return ( {(hovered) => ( (pressableRef.current = el)} + ref={pressableRef} onPress={(e) => { - if (!props.onSelectRow) { + if (!onSelectRow) { return; } @@ -173,12 +167,13 @@ function OptionRow(props) { if (e) { e.preventDefault(); } - let result = props.onSelectRow(props.option, pressableRef.current); + let result = onSelectRow(option, pressableRef.current); if (!(result instanceof Promise)) { result = Promise.resolve(); } + InteractionManager.runAfterInteractions(() => { - result.finally(() => setIsDisabled(props.isDisabled)); + result?.finally(() => setIsDisabled(isOptionDisabled)); }); }} disabled={isDisabled} @@ -187,68 +182,64 @@ function OptionRow(props) { styles.alignItemsCenter, styles.justifyContentBetween, styles.sidebarLink, - !props.isDisabled && styles.cursorPointer, - props.shouldDisableRowInnerPadding ? null : styles.sidebarLinkInner, - props.optionIsFocused ? styles.sidebarLinkActive : null, - props.shouldHaveOptionSeparator && styles.borderTop, - !props.onSelectRow && !props.isDisabled ? styles.cursorDefault : null, + !isOptionDisabled && styles.cursorPointer, + shouldDisableRowInnerPadding ? null : styles.sidebarLinkInner, + optionIsFocused ? styles.sidebarLinkActive : null, + shouldHaveOptionSeparator && styles.borderTop, + !onSelectRow && !isOptionDisabled ? styles.cursorDefault : null, ]} - accessibilityLabel={props.option.text} + accessibilityLabel={option.text} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} - hoverStyle={!props.optionIsFocused ? props.hoverStyle || styles.sidebarLinkHover : undefined} - needsOffscreenAlphaCompositing={lodashGet(props.option, 'icons.length', 0) >= 2} - onMouseDown={props.shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} + hoverStyle={!optionIsFocused ? hoverStyle ?? styles.sidebarLinkHover : undefined} + needsOffscreenAlphaCompositing={(option.icons?.length ?? 0) >= 2} + onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} > - {!_.isEmpty(props.option.icons) && - (props.option.shouldShowSubscript ? ( + {!!option.icons?.length && + (option.shouldShowSubscript ? ( ) : ( ))} - {props.option.alternateText ? ( + {option.alternateText ? ( - {props.option.alternateText} + {option.alternateText} ) : null} - {props.option.descriptiveText ? ( + {option.descriptiveText ? ( - {props.option.descriptiveText} + {option.descriptiveText} ) : null} - {props.option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( + {option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( )} - {props.showSelectedState && ( + {showSelectedState && ( <> - {props.shouldShowSelectedStateAsButton && !props.isSelected ? ( + {shouldShowSelectedStateAsButton && !isSelected ? (