From 4eeca487836db481054a09ee65c084b2c4011271 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 13 Dec 2023 19:12:06 +0100 Subject: [PATCH 001/111] Start migrating new form --- src/ONYXKEYS.ts | 10 +- src/components/Form/FormContext.js | 4 - src/components/Form/FormContext.tsx | 13 ++ src/components/Form/FormProvider.js | 24 +++ src/components/Form/FormWrapper.js | 217 ----------------------- src/components/Form/FormWrapper.tsx | 151 ++++++++++++++++ src/components/Form/InputWrapper.js | 45 ----- src/components/Form/InputWrapper.tsx | 21 +++ src/components/Form/errorsPropType.js | 11 -- src/components/Form/types.ts | 64 +++++++ src/components/SafeAreaConsumer/types.ts | 6 +- src/components/ScrollViewWithContext.tsx | 22 +-- 12 files changed, 293 insertions(+), 295 deletions(-) delete mode 100644 src/components/Form/FormContext.js create mode 100644 src/components/Form/FormContext.tsx delete mode 100644 src/components/Form/FormWrapper.js create mode 100644 src/components/Form/FormWrapper.tsx delete mode 100644 src/components/Form/InputWrapper.js create mode 100644 src/components/Form/InputWrapper.tsx delete mode 100644 src/components/Form/errorsPropType.js create mode 100644 src/components/Form/types.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a268c008cee8..402d1623b06c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -462,8 +462,8 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; // Forms - [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.AddDebitCardForm; - [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT]: OnyxTypes.AddDebitCardForm; + [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: OnyxTypes.Form; @@ -482,8 +482,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.LEGAL_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: OnyxTypes.DateOfBirthForm; - [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.DateOfBirthForm; + [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.Form; @@ -523,7 +523,7 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form | undefined; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/components/Form/FormContext.js b/src/components/Form/FormContext.js deleted file mode 100644 index 40edaa7cca69..000000000000 --- a/src/components/Form/FormContext.js +++ /dev/null @@ -1,4 +0,0 @@ -import {createContext} from 'react'; - -const FormContext = createContext({}); -export default FormContext; diff --git a/src/components/Form/FormContext.tsx b/src/components/Form/FormContext.tsx new file mode 100644 index 000000000000..23a2ea615eda --- /dev/null +++ b/src/components/Form/FormContext.tsx @@ -0,0 +1,13 @@ +import {createContext} from 'react'; + +type FormContextType = { + registerInput: (key: string, ref: any) => object; +}; + +const FormContext = createContext({ + registerInput: () => { + throw new Error('Registered input should be wrapped with FormWrapper'); + }, +}); + +export default FormContext; diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 63953d8303db..cbfc6a7315ca 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -14,6 +14,30 @@ import CONST from '@src/CONST'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; +// type ErrorsType = string | Record>; +// const errorsPropType = PropTypes.oneOfType([ +// PropTypes.string, +// PropTypes.objectOf( +// PropTypes.oneOfType([ +// PropTypes.string, +// PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]))])), +// ]), +// ), +// ]); + +// const defaultProps = { +// isSubmitButtonVisible: true, +// formState: { +// isLoading: false, +// }, +// enabledWhenOffline: false, +// isSubmitActionDangerous: false, +// scrollContextEnabled: false, +// footerContent: null, +// style: [], +// submitButtonStyles: [], +// }; + const propTypes = { /** A unique Onyx key identifying the form */ formID: PropTypes.string.isRequired, diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js deleted file mode 100644 index 638b6e5f8d19..000000000000 --- a/src/components/Form/FormWrapper.js +++ /dev/null @@ -1,217 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useMemo, useRef} from 'react'; -import {Keyboard, ScrollView, StyleSheet} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; -import FormSubmit from '@components/FormSubmit'; -import refPropTypes from '@components/refPropTypes'; -import SafeAreaConsumer from '@components/SafeAreaConsumer'; -import ScrollViewWithContext from '@components/ScrollViewWithContext'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import stylePropTypes from '@styles/stylePropTypes'; -import useThemeStyles from '@styles/useThemeStyles'; -import errorsPropType from './errorsPropType'; - -const propTypes = { - /** A unique Onyx key identifying the form */ - formID: PropTypes.string.isRequired, - - /** Text to be displayed in the submit button */ - submitButtonText: PropTypes.string.isRequired, - - /** Controls the submit button's visibility */ - isSubmitButtonVisible: PropTypes.bool, - - /** Callback to submit the form */ - onSubmit: PropTypes.func.isRequired, - - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /* Onyx Props */ - - /** Contains the form state that must be accessed outside of the component */ - formState: PropTypes.shape({ - /** Controls the loading state of the form */ - isLoading: PropTypes.bool, - - /** Server side errors keyed by microtime */ - errors: errorsPropType, - - /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - }), - - /** Should the button be enabled when offline */ - enabledWhenOffline: PropTypes.bool, - - /** Whether the form submit action is dangerous */ - isSubmitActionDangerous: PropTypes.bool, - - /** Whether ScrollWithContext should be used instead of regular ScrollView. - * Set to true when there's a nested Picker component in Form. - */ - scrollContextEnabled: PropTypes.bool, - - /** Container styles */ - style: stylePropTypes, - - /** Submit button styles */ - submitButtonStyles: stylePropTypes, - - /** Custom content to display in the footer after submit button */ - footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), - - errors: errorsPropType.isRequired, - - inputRefs: PropTypes.objectOf(refPropTypes).isRequired, -}; - -const defaultProps = { - isSubmitButtonVisible: true, - formState: { - isLoading: false, - }, - enabledWhenOffline: false, - isSubmitActionDangerous: false, - scrollContextEnabled: false, - footerContent: null, - style: [], - submitButtonStyles: [], -}; - -function FormWrapper(props) { - const styles = useThemeStyles(); - const { - onSubmit, - children, - formState, - errors, - inputRefs, - submitButtonText, - footerContent, - isSubmitButtonVisible, - style, - submitButtonStyles, - enabledWhenOffline, - isSubmitActionDangerous, - formID, - } = props; - const formRef = useRef(null); - const formContentRef = useRef(null); - const errorMessage = useMemo(() => { - const latestErrorMessage = ErrorUtils.getLatestErrorMessage(formState); - return typeof latestErrorMessage === 'string' ? latestErrorMessage : ''; - }, [formState]); - - const scrollViewContent = useCallback( - (safeAreaPaddingBottomStyle) => ( - - {children} - {isSubmitButtonVisible && ( - 0 || Boolean(errorMessage) || !_.isEmpty(formState.errorFields)} - isLoading={formState.isLoading} - message={_.isEmpty(formState.errorFields) ? errorMessage : null} - onSubmit={onSubmit} - footerContent={footerContent} - onFixTheErrorsLinkPressed={() => { - const errorFields = !_.isEmpty(errors) ? errors : formState.errorFields; - const focusKey = _.find(_.keys(inputRefs.current), (key) => _.keys(errorFields).includes(key)); - const focusInput = inputRefs.current[focusKey].current; - - // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. - if (typeof focusInput.isFocused !== 'function') { - Keyboard.dismiss(); - } - - // We subtract 10 to scroll slightly above the input - if (focusInput.measureLayout && typeof focusInput.measureLayout === 'function') { - // We measure relative to the content root, not the scroll view, as that gives - // consistent results across mobile and web - focusInput.measureLayout(formContentRef.current, (x, y) => - formRef.current.scrollTo({ - y: y - 10, - animated: false, - }), - ); - } - - // Focus the input after scrolling, as on the Web it gives a slightly better visual result - if (focusInput.focus && typeof focusInput.focus === 'function') { - focusInput.focus(); - } - }} - containerStyles={[styles.mh0, styles.mt5, styles.flex1, ...submitButtonStyles]} - enabledWhenOffline={enabledWhenOffline} - isSubmitActionDangerous={isSubmitActionDangerous} - disablePressOnEnter - /> - )} - - ), - [ - children, - enabledWhenOffline, - errorMessage, - errors, - footerContent, - formID, - formState.errorFields, - formState.isLoading, - inputRefs, - isSubmitActionDangerous, - isSubmitButtonVisible, - onSubmit, - style, - styles.flex1, - styles.mh0, - styles.mt5, - submitButtonStyles, - submitButtonText, - ], - ); - - return ( - - {({safeAreaPaddingBottomStyle}) => - props.scrollContextEnabled ? ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) : ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) - } - - ); -} - -FormWrapper.displayName = 'FormWrapper'; -FormWrapper.propTypes = propTypes; -FormWrapper.defaultProps = defaultProps; - -export default withOnyx({ - formState: { - key: (props) => props.formID, - }, -})(FormWrapper); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx new file mode 100644 index 000000000000..705ad5e0b6c2 --- /dev/null +++ b/src/components/Form/FormWrapper.tsx @@ -0,0 +1,151 @@ +import React, {useCallback, useMemo, useRef} from 'react'; +import {Keyboard, ScrollView, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; +import FormSubmit from '@components/FormSubmit'; +import SafeAreaConsumer from '@components/SafeAreaConsumer'; +import {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; +import ScrollViewWithContext from '@components/ScrollViewWithContext'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import useThemeStyles from '@styles/useThemeStyles'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {FormWrapperOnyxProps, FormWrapperProps} from './types'; + +function FormWrapper({ + onSubmit, + children, + formState, + errors, + inputRefs, + submitButtonText, + footerContent, + isSubmitButtonVisible, + style, + submitButtonStyles, + enabledWhenOffline, + isSubmitActionDangerous, + formID, + scrollContextEnabled, +}: FormWrapperProps) { + const styles = useThemeStyles(); + const formRef = useRef(null); + const formContentRef = useRef(null); + const errorMessage = useMemo(() => formState && ErrorUtils.getLatestErrorMessage(formState), [formState]); + + const scrollViewContent = useCallback( + (safeAreaPaddingBottomStyle: SafeAreaChildrenProps['safeAreaPaddingBottomStyle']) => ( + + {children} + {isSubmitButtonVisible && ( + { + const errorFields = !isEmptyObject(errors) ? errors : formState?.errorFields ?? {}; + const focusKey = Object.keys(inputRefs.current ?? {}).find((key) => Object.keys(errorFields).includes(key)); + + if (!focusKey) { + return; + } + + const focusInput = inputRefs.current?.[focusKey].current; + + // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. + if (typeof focusInput?.isFocused !== 'function') { + Keyboard.dismiss(); + } + + // We subtract 10 to scroll slightly above the input + if (focusInput?.measureLayout && formContentRef.current && typeof focusInput.measureLayout === 'function') { + // We measure relative to the content root, not the scroll view, as that gives + // consistent results across mobile and web + // eslint-disable-next-line @typescript-eslint/naming-convention + focusInput.measureLayout(formContentRef.current, (_x, y) => + formRef.current?.scrollTo({ + y: y - 10, + animated: false, + }), + ); + } + + // Focus the input after scrolling, as on the Web it gives a slightly better visual result + if (focusInput?.focus && typeof focusInput.focus === 'function') { + focusInput.focus(); + } + }} + // @ts-expect-error FormAlertWithSubmitButton migration + containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} + enabledWhenOffline={enabledWhenOffline} + isSubmitActionDangerous={isSubmitActionDangerous} + disablePressOnEnter + /> + )} + + ), + [ + children, + enabledWhenOffline, + errorMessage, + errors, + footerContent, + formID, + formState?.errorFields, + formState?.isLoading, + inputRefs, + isSubmitActionDangerous, + isSubmitButtonVisible, + onSubmit, + style, + styles.flex1, + styles.mh0, + styles.mt5, + submitButtonStyles, + submitButtonText, + ], + ); + + return ( + + {({safeAreaPaddingBottomStyle}) => + scrollContextEnabled ? ( + + {scrollViewContent(safeAreaPaddingBottomStyle)} + + ) : ( + + {scrollViewContent(safeAreaPaddingBottomStyle)} + + ) + } + + ); +} + +FormWrapper.displayName = 'FormWrapper'; + +export default withOnyx({ + formState: { + // FIX: Fabio plz help 😂 + key: (props) => props.formID as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + }, +})(FormWrapper); diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js deleted file mode 100644 index 9a31210195c4..000000000000 --- a/src/components/Form/InputWrapper.js +++ /dev/null @@ -1,45 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {forwardRef, useContext} from 'react'; -import refPropTypes from '@components/refPropTypes'; -import TextInput from '@components/TextInput'; -import FormContext from './FormContext'; - -const propTypes = { - InputComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.elementType]).isRequired, - inputID: PropTypes.string.isRequired, - valueType: PropTypes.string, - forwardedRef: refPropTypes, -}; - -const defaultProps = { - forwardedRef: undefined, - valueType: 'string', -}; - -function InputWrapper(props) { - const {InputComponent, inputID, forwardedRef, ...rest} = props; - const {registerInput} = useContext(FormContext); - // There are inputs that dont have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to - // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were - // calling some methods too early or twice, so we had to add this check to prevent that side effect. - // For now this side effect happened only in `TextInput` components. - const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; - // eslint-disable-next-line react/jsx-props-no-spreading - return ; -} - -InputWrapper.propTypes = propTypes; -InputWrapper.defaultProps = defaultProps; -InputWrapper.displayName = 'InputWrapper'; - -const InputWrapperWithRef = forwardRef((props, ref) => ( - -)); - -InputWrapperWithRef.displayName = 'InputWrapperWithRef'; - -export default InputWrapperWithRef; diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx new file mode 100644 index 000000000000..1b32409ea1d2 --- /dev/null +++ b/src/components/Form/InputWrapper.tsx @@ -0,0 +1,21 @@ +import React, {ForwardedRef, forwardRef, useContext} from 'react'; +import TextInput from '@components/TextInput'; +import FormContext from './FormContext'; +import {InputWrapperProps} from './types'; + +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { + const {registerInput} = useContext(FormContext); + + // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to + // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were + // calling some methods too early or twice, so we had to add this check to prevent that side effect. + // For now this side effect happened only in `TextInput` components. + const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +InputWrapper.displayName = 'InputWrapper'; + +export default forwardRef(InputWrapper); diff --git a/src/components/Form/errorsPropType.js b/src/components/Form/errorsPropType.js deleted file mode 100644 index 3a02bb74e942..000000000000 --- a/src/components/Form/errorsPropType.js +++ /dev/null @@ -1,11 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.oneOfType([ - PropTypes.string, - PropTypes.objectOf( - PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]))])), - ]), - ), -]); diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts new file mode 100644 index 000000000000..8db4909327e0 --- /dev/null +++ b/src/components/Form/types.ts @@ -0,0 +1,64 @@ +import {ElementType, ReactNode, RefObject} from 'react'; +import {StyleProp, TextInput, ViewStyle} from 'react-native'; +import {OnyxEntry} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; +import ONYXKEYS from '@src/ONYXKEYS'; +import Form from '@src/types/onyx/Form'; +import {Errors} from '@src/types/onyx/OnyxCommon'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; + +type ValueType = 'string' | 'boolean' | 'date'; + +type InputWrapperProps = { + InputComponent: TInput; + inputID: string; + valueType?: ValueType; +}; + +type FormWrapperOnyxProps = { + /** Contains the form state that must be accessed outside of the component */ + formState: OnyxEntry
; +}; + +type FormWrapperProps = ChildrenProps & + FormWrapperOnyxProps & { + /** A unique Onyx key identifying the form */ + formID: ValueOf; + + /** Text to be displayed in the submit button */ + submitButtonText: string; + + /** Controls the submit button's visibility */ + isSubmitButtonVisible?: boolean; + + /** Callback to submit the form */ + onSubmit: () => void; + + /** Should the button be enabled when offline */ + enabledWhenOffline?: boolean; + + /** Whether the form submit action is dangerous */ + isSubmitActionDangerous?: boolean; + + /** Whether ScrollWithContext should be used instead of regular ScrollView. + * Set to true when there's a nested Picker component in Form. + */ + scrollContextEnabled?: boolean; + + /** Container styles */ + style?: StyleProp; + + /** Submit button styles */ + submitButtonStyles?: StyleProp; + + /** Custom content to display in the footer after submit button */ + footerContent?: ReactNode; + + /** Server side errors keyed by microtime */ + errors: Errors; + + // Assuming refs are React refs + inputRefs: RefObject>>; + }; + +export type {InputWrapperProps, FormWrapperProps, FormWrapperOnyxProps}; diff --git a/src/components/SafeAreaConsumer/types.ts b/src/components/SafeAreaConsumer/types.ts index bc81de96a082..8e162a3b37fc 100644 --- a/src/components/SafeAreaConsumer/types.ts +++ b/src/components/SafeAreaConsumer/types.ts @@ -1,7 +1,7 @@ import {DimensionValue} from 'react-native'; import {EdgeInsets} from 'react-native-safe-area-context'; -type ChildrenProps = { +type SafeAreaChildrenProps = { paddingTop?: DimensionValue; paddingBottom?: DimensionValue; insets?: EdgeInsets; @@ -11,7 +11,9 @@ type ChildrenProps = { }; type SafeAreaConsumerProps = { - children: React.FC; + children: React.FC; }; export default SafeAreaConsumerProps; + +export type {SafeAreaChildrenProps}; diff --git a/src/components/ScrollViewWithContext.tsx b/src/components/ScrollViewWithContext.tsx index 7c75ae2f71b2..285da94092c2 100644 --- a/src/components/ScrollViewWithContext.tsx +++ b/src/components/ScrollViewWithContext.tsx @@ -1,5 +1,5 @@ -import React, {ForwardedRef, useMemo, useRef, useState} from 'react'; -import {NativeScrollEvent, NativeSyntheticEvent, ScrollView} from 'react-native'; +import React, {createContext, ForwardedRef, forwardRef, ReactNode, useMemo, useRef, useState} from 'react'; +import {NativeScrollEvent, NativeSyntheticEvent, ScrollView, ScrollViewProps} from 'react-native'; const MIN_SMOOTH_SCROLL_EVENT_THROTTLE = 16; @@ -8,16 +8,16 @@ type ScrollContextValue = { scrollViewRef: ForwardedRef; }; -const ScrollContext = React.createContext({ +const ScrollContext = createContext({ contentOffsetY: 0, scrollViewRef: null, }); type ScrollViewWithContextProps = { - onScroll: (event: NativeSyntheticEvent) => void; - children?: React.ReactNode; - scrollEventThrottle: number; -} & Partial; + onScroll?: (event: NativeSyntheticEvent) => void; + children?: ReactNode; + scrollEventThrottle?: number; +} & Partial; /* * is a wrapper around that provides a ref to the . @@ -26,7 +26,7 @@ type ScrollViewWithContextProps = { * Using this wrapper will automatically handle scrolling to the picker's * when the picker modal is opened */ -function ScrollViewWithContextWithRef({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) { +function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) { const [contentOffsetY, setContentOffsetY] = useState(0); const defaultScrollViewRef = useRef(null); const scrollViewRef = ref ?? defaultScrollViewRef; @@ -52,15 +52,15 @@ function ScrollViewWithContextWithRef({onScroll, scrollEventThrottle, children, {...restProps} ref={scrollViewRef} onScroll={setContextScrollPosition} - scrollEventThrottle={scrollEventThrottle || MIN_SMOOTH_SCROLL_EVENT_THROTTLE} + scrollEventThrottle={scrollEventThrottle ?? MIN_SMOOTH_SCROLL_EVENT_THROTTLE} > {children} ); } -ScrollViewWithContextWithRef.displayName = 'ScrollViewWithContextWithRef'; +ScrollViewWithContext.displayName = 'ScrollViewWithContext'; -export default React.forwardRef(ScrollViewWithContextWithRef); +export default forwardRef(ScrollViewWithContext); export {ScrollContext}; export type {ScrollContextValue}; From 22b5aaa0639f4bd8d2c0caa8a51311b500703403 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sun, 17 Dec 2023 22:20:33 +0100 Subject: [PATCH 002/111] Type FormProvider --- src/components/FloatingActionButton/index.js | 53 +-- src/components/Form/FormContext.tsx | 9 +- src/components/Form/FormProvider.js | 428 ------------------ src/components/Form/FormProvider.tsx | 364 +++++++++++++++ src/components/Form/FormWrapper.tsx | 40 +- src/components/Form/InputWrapper.tsx | 8 +- src/components/Form/types.ts | 95 ++-- src/libs/actions/FormActions.ts | 4 +- .../FloatingActionButtonAndPopover.js | 13 - 9 files changed, 459 insertions(+), 555 deletions(-) delete mode 100644 src/components/Form/FormProvider.js create mode 100644 src/components/Form/FormProvider.tsx diff --git a/src/components/FloatingActionButton/index.js b/src/components/FloatingActionButton/index.js index d341396c44b7..8e963d49b10c 100644 --- a/src/components/FloatingActionButton/index.js +++ b/src/components/FloatingActionButton/index.js @@ -26,58 +26,7 @@ const propTypes = { role: PropTypes.string.isRequired, }; -const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => { - const theme = useTheme(); - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const fabPressable = useRef(null); - const animatedValue = useSharedValue(isActive ? 1 : 0); - const buttonRef = ref; - - useEffect(() => { - animatedValue.value = withTiming(isActive ? 1 : 0, { - duration: 340, - easing: Easing.inOut(Easing.ease), - }); - }, [isActive, animatedValue]); - - const animatedStyle = useAnimatedStyle(() => { - const backgroundColor = interpolateColor(animatedValue.value, [0, 1], [theme.success, theme.buttonDefaultBG]); - - return { - transform: [{rotate: `${animatedValue.value * 135}deg`}], - backgroundColor, - borderRadius: styles.floatingActionButton.borderRadius, - }; - }); - - return ( - - - { - fabPressable.current = el; - if (buttonRef) { - buttonRef.current = el; - } - }} - accessibilityLabel={accessibilityLabel} - role={role} - pressDimmingValue={1} - onPress={(e) => { - // Drop focus to avoid blue focus ring. - fabPressable.current.blur(); - onPress(e); - }} - onLongPress={() => {}} - style={[styles.floatingActionButton, animatedStyle]} - > - - - - - ); -}); +const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => null); FloatingActionButton.propTypes = propTypes; FloatingActionButton.displayName = 'FloatingActionButton'; diff --git a/src/components/Form/FormContext.tsx b/src/components/Form/FormContext.tsx index 23a2ea615eda..dcc8f3b516de 100644 --- a/src/components/Form/FormContext.tsx +++ b/src/components/Form/FormContext.tsx @@ -1,13 +1,12 @@ import {createContext} from 'react'; +import {RegisterInput} from './types'; -type FormContextType = { - registerInput: (key: string, ref: any) => object; +type FormContext = { + registerInput: RegisterInput; }; -const FormContext = createContext({ +export default createContext({ registerInput: () => { throw new Error('Registered input should be wrapped with FormWrapper'); }, }); - -export default FormContext; diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js deleted file mode 100644 index c0537c01be7c..000000000000 --- a/src/components/Form/FormProvider.js +++ /dev/null @@ -1,428 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import compose from '@libs/compose'; -import * as ValidationUtils from '@libs/ValidationUtils'; -import Visibility from '@libs/Visibility'; -import stylePropTypes from '@styles/stylePropTypes'; -import * as FormActions from '@userActions/FormActions'; -import CONST from '@src/CONST'; -import FormContext from './FormContext'; -import FormWrapper from './FormWrapper'; - -// type ErrorsType = string | Record>; -// const errorsPropType = PropTypes.oneOfType([ -// PropTypes.string, -// PropTypes.objectOf( -// PropTypes.oneOfType([ -// PropTypes.string, -// PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]))])), -// ]), -// ), -// ]); - -// const defaultProps = { -// isSubmitButtonVisible: true, -// formState: { -// isLoading: false, -// }, -// enabledWhenOffline: false, -// isSubmitActionDangerous: false, -// scrollContextEnabled: false, -// footerContent: null, -// style: [], -// submitButtonStyles: [], -// }; - -const propTypes = { - /** A unique Onyx key identifying the form */ - formID: PropTypes.string.isRequired, - - /** Text to be displayed in the submit button */ - submitButtonText: PropTypes.string.isRequired, - - /** Controls the submit button's visibility */ - isSubmitButtonVisible: PropTypes.bool, - - /** Callback to validate the form */ - validate: PropTypes.func, - - /** Callback to submit the form */ - onSubmit: PropTypes.func.isRequired, - - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /* Onyx Props */ - - /** Contains the form state that must be accessed outside of the component */ - formState: PropTypes.shape({ - /** Controls the loading state of the form */ - isLoading: PropTypes.bool, - - /** Server side errors keyed by microtime */ - errors: PropTypes.objectOf(PropTypes.string), - - /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - }), - - /** Contains draft values for each input in the form */ - draftValues: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number, PropTypes.objectOf(Date)])), - - /** Should the button be enabled when offline */ - enabledWhenOffline: PropTypes.bool, - - /** Whether the form submit action is dangerous */ - isSubmitActionDangerous: PropTypes.bool, - - /** Whether ScrollWithContext should be used instead of regular ScrollView. Set to true when there's a nested Picker component in Form. */ - scrollContextEnabled: PropTypes.bool, - - /** Container styles */ - style: stylePropTypes, - - /** Custom content to display in the footer after submit button */ - footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), - - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** Should validate function be called when input loose focus */ - shouldValidateOnBlur: PropTypes.bool, - - /** Should validate function be called when the value of the input is changed */ - shouldValidateOnChange: PropTypes.bool, -}; - -// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. -// 200ms delay was chosen as a result of empirical testing. -// More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 -const VALIDATE_DELAY = 200; - -const defaultProps = { - isSubmitButtonVisible: true, - formState: { - isLoading: false, - }, - draftValues: {}, - enabledWhenOffline: false, - isSubmitActionDangerous: false, - scrollContextEnabled: false, - footerContent: null, - style: [], - validate: () => {}, - shouldValidateOnBlur: true, - shouldValidateOnChange: true, -}; - -function getInitialValueByType(valueType) { - switch (valueType) { - case 'string': - return ''; - case 'boolean': - return false; - case 'date': - return new Date(); - default: - return ''; - } -} - -const FormProvider = forwardRef( - ({validate, formID, shouldValidateOnBlur, shouldValidateOnChange, children, formState, network, enabledWhenOffline, draftValues, onSubmit, ...rest}, forwardedRef) => { - const inputRefs = useRef({}); - const touchedInputs = useRef({}); - const [inputValues, setInputValues] = useState(() => ({...draftValues})); - const [errors, setErrors] = useState({}); - const hasServerError = useMemo(() => Boolean(formState) && !_.isEmpty(formState.errors), [formState]); - - const onValidate = useCallback( - (values, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values); - - if (shouldClearServerError) { - FormActions.setErrors(formID, null); - } - FormActions.setErrorFields(formID, null); - - const validateErrors = validate(trimmedStringValues) || {}; - - // Validate the input for html tags. It should supercede any other error - _.each(trimmedStringValues, (inputValue, inputID) => { - // If the input value is empty OR is non-string, we don't need to validate it for HTML tags - if (!inputValue || !_.isString(inputValue)) { - return; - } - const foundHtmlTagIndex = inputValue.search(CONST.VALIDATE_FOR_HTML_TAG_REGEX); - const leadingSpaceIndex = inputValue.search(CONST.VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX); - - // Return early if there are no HTML characters - if (leadingSpaceIndex === -1 && foundHtmlTagIndex === -1) { - return; - } - - const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX); - let isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(inputValue)); - // Check for any matches that the original regex (foundHtmlTagIndex) matched - if (matchedHtmlTags) { - // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed. - for (let i = 0; i < matchedHtmlTags.length; i++) { - const htmlTag = matchedHtmlTags[i]; - isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(htmlTag)); - if (!isMatch) { - break; - } - } - } - - if (isMatch && leadingSpaceIndex === -1) { - return; - } - - // Add a validation error here because it is a string value that contains HTML characters - validateErrors[inputID] = 'common.error.invalidCharacter'; - }); - - if (!_.isObject(validateErrors)) { - throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); - } - - const touchedInputErrors = _.pick(validateErrors, (inputValue, inputID) => Boolean(touchedInputs.current[inputID])); - - if (!_.isEqual(errors, touchedInputErrors)) { - setErrors(touchedInputErrors); - } - - return touchedInputErrors; - }, - [errors, formID, validate], - ); - - /** - * @param {String} inputID - The inputID of the input being touched - */ - const setTouchedInput = useCallback( - (inputID) => { - touchedInputs.current[inputID] = true; - }, - [touchedInputs], - ); - - const submit = useCallback(() => { - // Return early if the form is already submitting to avoid duplicate submission - if (formState.isLoading) { - return; - } - - // Prepare values before submitting - const trimmedStringValues = ValidationUtils.prepareValues(inputValues); - - // Touches all form inputs so we can validate the entire form - _.each(inputRefs.current, (inputRef, inputID) => (touchedInputs.current[inputID] = true)); - - // Validate form and return early if any errors are found - if (!_.isEmpty(onValidate(trimmedStringValues))) { - return; - } - - // Do not submit form if network is offline and the form is not enabled when offline - if (network.isOffline && !enabledWhenOffline) { - return; - } - - onSubmit(trimmedStringValues); - }, [enabledWhenOffline, formState.isLoading, inputValues, network.isOffline, onSubmit, onValidate]); - - /** - * Resets the form - */ - const resetForm = useCallback( - (optionalValue) => { - _.each(inputValues, (inputRef, inputID) => { - setInputValues((prevState) => { - const copyPrevState = _.clone(prevState); - - touchedInputs.current[inputID] = false; - copyPrevState[inputID] = optionalValue[inputID] || ''; - - return copyPrevState; - }); - }); - setErrors({}); - }, - [inputValues], - ); - useImperativeHandle(forwardedRef, () => ({ - resetForm, - })); - - const registerInput = useCallback( - (inputID, propsToParse = {}) => { - const newRef = inputRefs.current[inputID] || propsToParse.ref || createRef(); - if (inputRefs.current[inputID] !== newRef) { - inputRefs.current[inputID] = newRef; - } - - if (!_.isUndefined(propsToParse.value)) { - inputValues[inputID] = propsToParse.value; - } else if (propsToParse.shouldSaveDraft && !_.isUndefined(draftValues[inputID]) && _.isUndefined(inputValues[inputID])) { - inputValues[inputID] = draftValues[inputID]; - } else if (propsToParse.shouldUseDefaultValue && _.isUndefined(inputValues[inputID])) { - // We force the form to set the input value from the defaultValue props if there is a saved valid value - inputValues[inputID] = propsToParse.defaultValue; - } else if (_.isUndefined(inputValues[inputID])) { - // We want to initialize the input value if it's undefined - inputValues[inputID] = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; - } - - const errorFields = lodashGet(formState, 'errorFields', {}); - const fieldErrorMessage = - _.chain(errorFields[inputID]) - .keys() - .sortBy() - .reverse() - .map((key) => errorFields[inputID][key]) - .first() - .value() || ''; - - return { - ...propsToParse, - ref: - typeof propsToParse.ref === 'function' - ? (node) => { - propsToParse.ref(node); - newRef.current = node; - } - : newRef, - inputID, - key: propsToParse.key || inputID, - errorText: errors[inputID] || fieldErrorMessage, - value: inputValues[inputID], - // As the text input is controlled, we never set the defaultValue prop - // as this is already happening by the value prop. - defaultValue: undefined, - onTouched: (event) => { - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onTouched)) { - propsToParse.onTouched(event); - } - }, - onPress: (event) => { - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onPress)) { - propsToParse.onPress(event); - } - }, - onPressOut: (event) => { - // To prevent validating just pressed inputs, we need to set the touched input right after - // onValidate and to do so, we need to delays setTouchedInput of the same amount of time - // as the onValidate is delayed - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onPressIn)) { - propsToParse.onPressIn(event); - } - }, - onBlur: (event) => { - // Only run validation when user proactively blurs the input. - if (Visibility.isVisible() && Visibility.hasFocus()) { - const relatedTargetId = lodashGet(event, 'nativeEvent.relatedTarget.id'); - // We delay the validation in order to prevent Checkbox loss of focus when - // the user is focusing a TextInput and proceeds to toggle a CheckBox in - // web and mobile web platforms. - - setTimeout(() => { - if ( - relatedTargetId && - _.includes([CONST.OVERLAY.BOTTOM_BUTTON_NATIVE_ID, CONST.OVERLAY.TOP_BUTTON_NATIVE_ID, CONST.BACK_BUTTON_NATIVE_ID], relatedTargetId) - ) { - return; - } - setTouchedInput(inputID); - if (shouldValidateOnBlur) { - onValidate(inputValues, !hasServerError); - } - }, VALIDATE_DELAY); - } - - if (_.isFunction(propsToParse.onBlur)) { - propsToParse.onBlur(event); - } - }, - onInputChange: (value, key) => { - const inputKey = key || inputID; - setInputValues((prevState) => { - const newState = { - ...prevState, - [inputKey]: value, - }; - - if (shouldValidateOnChange) { - onValidate(newState); - } - return newState; - }); - - if (propsToParse.shouldSaveDraft) { - FormActions.setDraftValues(formID, {[inputKey]: value}); - } - - if (_.isFunction(propsToParse.onValueChange)) { - propsToParse.onValueChange(value, inputKey); - } - }, - }; - }, - [draftValues, formID, errors, formState, hasServerError, inputValues, onValidate, setTouchedInput, shouldValidateOnBlur, shouldValidateOnChange], - ); - const value = useMemo(() => ({registerInput}), [registerInput]); - - return ( - - {/* eslint-disable react/jsx-props-no-spreading */} - - {_.isFunction(children) ? children({inputValues}) : children} - - - ); - }, -); - -FormProvider.displayName = 'Form'; -FormProvider.propTypes = propTypes; -FormProvider.defaultProps = defaultProps; - -export default compose( - withNetwork(), - withOnyx({ - formState: { - key: (props) => props.formID, - }, - draftValues: { - key: (props) => `${props.formID}Draft`, - }, - }), -)(FormProvider); diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx new file mode 100644 index 000000000000..b7f20566b825 --- /dev/null +++ b/src/components/Form/FormProvider.tsx @@ -0,0 +1,364 @@ +import lodashIsEqual from 'lodash/isEqual'; +import React, {createRef, ForwardedRef, forwardRef, ReactNode, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import Visibility from '@libs/Visibility'; +import * as FormActions from '@userActions/FormActions'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {Form, Network} from '@src/types/onyx'; +import {Errors} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import FormContext from './FormContext'; +import FormWrapper from './FormWrapper'; +import {FormProps, InputRef, InputRefs, InputValues, RegisterInput, ValueType} from './types'; + +// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. +// 200ms delay was chosen as a result of empirical testing. +// More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 +const VALIDATE_DELAY = 200; + +function getInitialValueByType(valueType?: ValueType): false | Date | '' { + switch (valueType) { + case 'string': + return ''; + case 'boolean': + return false; + case 'date': + return new Date(); + default: + return ''; + } +} + +type FormProviderOnyxProps = { + /** Contains the form state that must be accessed outside of the component */ + formState: OnyxEntry; + + /** Contains draft values for each input in the form */ + draftValues: OnyxEntry; + + /** Information about the network */ + network: OnyxEntry; +}; + +type FormProviderProps = FormProviderOnyxProps & + FormProps & { + /** Children to render. */ + children: ((props: {inputValues: InputValues}) => ReactNode) | ReactNode; + + /** Callback to validate the form */ + validate?: (values: InputValues) => Errors; + + /** Should validate function be called when input loose focus */ + shouldValidateOnBlur?: boolean; + + /** Should validate function be called when the value of the input is changed */ + shouldValidateOnChange?: boolean; + }; + +type FormRef = { + resetForm: (optionalValue: InputValues) => void; +}; + +function FormProvider>( + { + formID, + validate, + shouldValidateOnBlur = true, + shouldValidateOnChange = true, + children, + formState, + network, + enabledWhenOffline = false, + draftValues, + onSubmit, + ...rest + }: FormProviderProps, + forwardedRef: ForwardedRef, +) { + const inputRefs = useRef({}); + const touchedInputs = useRef>({}); + const [inputValues, setInputValues] = useState(() => ({...draftValues})); + const [errors, setErrors] = useState({}); + const hasServerError = useMemo(() => !!formState && !isEmptyObject(formState?.errors), [formState]); + + const onValidate = useCallback( + (values: InputValues, shouldClearServerError = true) => { + const trimmedStringValues = ValidationUtils.prepareValues(values); + + if (shouldClearServerError) { + FormActions.setErrors(formID, null); + } + FormActions.setErrorFields(formID, null); + + const validateErrors = validate?.(trimmedStringValues) ?? {}; + + // Validate the input for html tags. It should supercede any other error + Object.entries(trimmedStringValues).forEach(([inputID, inputValue]) => { + // If the input value is empty OR is non-string, we don't need to validate it for HTML tags + if (!inputValue || typeof inputValue !== 'string') { + return; + } + const foundHtmlTagIndex = inputValue.search(CONST.VALIDATE_FOR_HTML_TAG_REGEX); + const leadingSpaceIndex = inputValue.search(CONST.VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX); + + // Return early if there are no HTML characters + if (leadingSpaceIndex === -1 && foundHtmlTagIndex === -1) { + return; + } + + const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX); + let isMatch = CONST.WHITELISTED_TAGS.some((regex) => regex.test(inputValue)); + // Check for any matches that the original regex (foundHtmlTagIndex) matched + if (matchedHtmlTags) { + // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed. + for (const htmlTag of matchedHtmlTags) { + isMatch = CONST.WHITELISTED_TAGS.some((regex) => regex.test(htmlTag)); + if (!isMatch) { + break; + } + } + } + + if (isMatch && leadingSpaceIndex === -1) { + return; + } + + // Add a validation error here because it is a string value that contains HTML characters + validateErrors[inputID] = 'common.error.invalidCharacter'; + }); + + if (typeof validateErrors !== 'object') { + throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); + } + + const touchedInputErrors = Object.fromEntries(Object.entries(validateErrors).filter(([inputID]) => !!touchedInputs.current[inputID])); + + if (!lodashIsEqual(errors, touchedInputErrors)) { + setErrors(touchedInputErrors); + } + + return touchedInputErrors; + }, + [errors, formID, validate], + ); + + /** @param inputID - The inputID of the input being touched */ + const setTouchedInput = useCallback( + (inputID: string) => { + touchedInputs.current[inputID] = true; + }, + [touchedInputs], + ); + + const submit = useCallback(() => { + // Return early if the form is already submitting to avoid duplicate submission + if (formState?.isLoading) { + return; + } + + // Prepare values before submitting + const trimmedStringValues = ValidationUtils.prepareValues(inputValues); + + // Touches all form inputs so we can validate the entire form + Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); + + // Validate form and return early if any errors are found + if (isNotEmptyObject(onValidate(trimmedStringValues))) { + return; + } + + // Do not submit form if network is offline and the form is not enabled when offline + if (network?.isOffline && !enabledWhenOffline) { + return; + } + + onSubmit(trimmedStringValues); + }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); + + const resetForm = useCallback( + (optionalValue: InputValues) => { + Object.keys(inputValues).forEach((inputID) => { + setInputValues((prevState) => { + const copyPrevState = {...prevState}; + + touchedInputs.current[inputID] = false; + copyPrevState[inputID] = optionalValue[inputID] || ''; + + return copyPrevState; + }); + }); + setErrors({}); + }, + [inputValues], + ); + useImperativeHandle(forwardedRef, () => ({ + resetForm, + })); + + const registerInput: RegisterInput = useCallback( + (inputID, inputProps) => { + const newRef: InputRef = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); + if (inputRefs.current[inputID] !== newRef) { + inputRefs.current[inputID] = newRef; + } + + if (inputProps.value !== undefined) { + inputValues[inputID] = inputProps.value; + } else if (inputProps.shouldSaveDraft && draftValues?.[inputID] !== undefined && inputValues[inputID] === undefined) { + inputValues[inputID] = draftValues[inputID]; + } else if (inputProps.shouldUseDefaultValue && inputValues[inputID] === undefined) { + // We force the form to set the input value from the defaultValue props if there is a saved valid value + inputValues[inputID] = inputProps.defaultValue; + } else if (inputValues[inputID] === undefined) { + // We want to initialize the input value if it's undefined + inputValues[inputID] = inputProps.defaultValue === undefined ? getInitialValueByType(inputProps.valueType) : inputProps.defaultValue; + } + + const errorFields = formState?.errorFields?.[inputID] ?? {}; + const fieldErrorMessage = + Object.keys(errorFields) + .sort() + .map((key) => errorFields[key]) + .at(-1) ?? ''; + + const inputRef = inputProps.ref; + return { + ...inputProps, + ref: + typeof inputRef === 'function' + ? (node) => { + inputRef(node); + if (typeof newRef !== 'function') { + newRef.current = node; + } + } + : newRef, + inputID, + key: inputProps.key ?? inputID, + errorText: errors[inputID] || fieldErrorMessage, + value: inputValues[inputID], + // As the text input is controlled, we never set the defaultValue prop + // as this is already happening by the value prop. + defaultValue: undefined, + onTouched: (event) => { + if (!inputProps.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + if (typeof inputProps.onTouched === 'function') { + inputProps.onTouched(event); + } + }, + onPress: (event) => { + if (!inputProps.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + if (typeof inputProps.onPress === 'function') { + inputProps.onPress(event); + } + }, + onPressOut: (event) => { + // To prevent validating just pressed inputs, we need to set the touched input right after + // onValidate and to do so, we need to delays setTouchedInput of the same amount of time + // as the onValidate is delayed + if (!inputProps.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + if (typeof inputProps.onPressOut === 'function') { + inputProps.onPressOut(event); + } + }, + onBlur: (event) => { + // Only run validation when user proactively blurs the input. + if (Visibility.isVisible() && Visibility.hasFocus()) { + const relatedTarget = 'nativeEvent' in event ? event?.nativeEvent?.relatedTarget : undefined; + const relatedTargetId = relatedTarget && 'id' in relatedTarget && typeof relatedTarget.id === 'string' && relatedTarget.id; + // We delay the validation in order to prevent Checkbox loss of focus when + // the user is focusing a TextInput and proceeds to toggle a CheckBox in + // web and mobile web platforms. + + setTimeout(() => { + if ( + relatedTargetId === CONST.OVERLAY.BOTTOM_BUTTON_NATIVE_ID || + relatedTargetId === CONST.OVERLAY.TOP_BUTTON_NATIVE_ID || + relatedTargetId === CONST.BACK_BUTTON_NATIVE_ID + ) { + return; + } + setTouchedInput(inputID); + if (shouldValidateOnBlur) { + onValidate(inputValues, !hasServerError); + } + }, VALIDATE_DELAY); + } + + if (typeof inputProps.onBlur === 'function') { + inputProps.onBlur(event); + } + }, + onInputChange: (value, key) => { + const inputKey = key || inputID; + setInputValues((prevState) => { + const newState = { + ...prevState, + [inputKey]: value, + }; + + if (shouldValidateOnChange) { + onValidate(newState); + } + return newState; + }); + + if (inputProps.shouldSaveDraft) { + FormActions.setDraftValues(formID, {[inputKey]: value}); + } + + if (typeof inputProps.onValueChange === 'function') { + inputProps.onValueChange(value, inputKey); + } + }, + }; + }, + [draftValues, formID, errors, formState, hasServerError, inputValues, onValidate, setTouchedInput, shouldValidateOnBlur, shouldValidateOnChange], + ); + const value = useMemo(() => ({registerInput}), [registerInput]); + + return ( + + {/* eslint-disable react/jsx-props-no-spreading */} + + {typeof children === 'function' ? children({inputValues}) : children} + + + ); +} + +FormProvider.displayName = 'Form'; + +export default (>() => + withOnyx, FormProviderOnyxProps>({ + network: { + key: ONYXKEYS.NETWORK, + }, + formState: { + key: (props) => props.formID as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + }, + draftValues: { + key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT, + }, + })(forwardRef(FormProvider)))(); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index ec2f2be2eca7..1c1dd4658d57 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,6 +1,6 @@ -import React, {useCallback, useMemo, useRef} from 'react'; -import {Keyboard, ScrollView, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import React, {MutableRefObject, useCallback, useMemo, useRef} from 'react'; +import {Keyboard, ScrollView, StyleProp, View, ViewStyle} from 'react-native'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormSubmit from '@components/FormSubmit'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; @@ -9,8 +9,29 @@ import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import ONYXKEYS from '@src/ONYXKEYS'; +import {Form} from '@src/types/onyx'; +import {Errors} from '@src/types/onyx/OnyxCommon'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {FormWrapperOnyxProps, FormWrapperProps} from './types'; +import {FormProps, InputRefs} from './types'; + +type FormWrapperOnyxProps = { + /** Contains the form state that must be accessed outside of the component */ + formState: OnyxEntry; +}; + +type FormWrapperProps = ChildrenProps & + FormWrapperOnyxProps & + FormProps & { + /** Submit button styles */ + submitButtonStyles?: StyleProp; + + /** Server side errors keyed by microtime */ + errors: Errors; + + // Assuming refs are React refs + inputRefs: MutableRefObject; + }; function FormWrapper({ onSubmit, @@ -19,14 +40,14 @@ function FormWrapper({ errors, inputRefs, submitButtonText, - footerContent, - isSubmitButtonVisible, + footerContent = null, + isSubmitButtonVisible = true, style, submitButtonStyles, enabledWhenOffline, - isSubmitActionDangerous, + isSubmitActionDangerous = false, formID, - scrollContextEnabled, + scrollContextEnabled = false, }: FormWrapperProps) { const styles = useThemeStyles(); const formRef = useRef(null); @@ -58,7 +79,8 @@ function FormWrapper({ return; } - const focusInput = inputRefs.current?.[focusKey].current; + const inputRef = inputRefs.current?.[focusKey]; + const focusInput = inputRef && 'current' in inputRef ? inputRef.current : undefined; // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. if (typeof focusInput?.isFocused !== 'function') { diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 1b32409ea1d2..579dd553afaa 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,9 +1,9 @@ -import React, {ForwardedRef, forwardRef, useContext} from 'react'; +import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import FormContext from './FormContext'; -import {InputWrapperProps} from './types'; +import {InputProps, InputRef, InputWrapperProps} from './types'; -function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: InputRef) { const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to @@ -13,7 +13,7 @@ function InputWrapper({InputComponent, inputID const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 8db4909327e0..801ec15dc62c 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,64 +1,75 @@ -import {ElementType, ReactNode, RefObject} from 'react'; -import {StyleProp, TextInput, ViewStyle} from 'react-native'; -import {OnyxEntry} from 'react-native-onyx'; +import {ComponentType, ForwardedRef, ReactNode, SyntheticEvent} from 'react'; +import {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; import {ValueOf} from 'type-fest'; import ONYXKEYS from '@src/ONYXKEYS'; -import Form from '@src/types/onyx/Form'; -import {Errors} from '@src/types/onyx/OnyxCommon'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; type ValueType = 'string' | 'boolean' | 'date'; -type InputWrapperProps = { - InputComponent: TInput; +type InputWrapperProps = { + InputComponent: ComponentType; inputID: string; valueType?: ValueType; }; -type FormWrapperOnyxProps = { - /** Contains the form state that must be accessed outside of the component */ - formState: OnyxEntry; -}; +type FormID = ValueOf & `${string}Form`; + +type FormProps = { + /** A unique Onyx key identifying the form */ + formID: FormID; -type FormWrapperProps = ChildrenProps & - FormWrapperOnyxProps & { - /** A unique Onyx key identifying the form */ - formID: ValueOf; + /** Text to be displayed in the submit button */ + submitButtonText: string; - /** Text to be displayed in the submit button */ - submitButtonText: string; + /** Controls the submit button's visibility */ + isSubmitButtonVisible?: boolean; - /** Controls the submit button's visibility */ - isSubmitButtonVisible?: boolean; + /** Callback to submit the form */ + onSubmit: (values?: Record) => void; - /** Callback to submit the form */ - onSubmit: () => void; + /** Should the button be enabled when offline */ + enabledWhenOffline?: boolean; - /** Should the button be enabled when offline */ - enabledWhenOffline?: boolean; + /** Whether the form submit action is dangerous */ + isSubmitActionDangerous?: boolean; - /** Whether the form submit action is dangerous */ - isSubmitActionDangerous?: boolean; + /** Whether ScrollWithContext should be used instead of regular ScrollView. Set to true when there's a nested Picker component in Form. */ + scrollContextEnabled?: boolean; - /** Whether ScrollWithContext should be used instead of regular ScrollView. - * Set to true when there's a nested Picker component in Form. - */ - scrollContextEnabled?: boolean; + /** Container styles */ + style?: StyleProp; + + /** Custom content to display in the footer after submit button */ + footerContent?: ReactNode; +}; - /** Container styles */ - style?: StyleProp; +type InputValues = Record; - /** Submit button styles */ - submitButtonStyles?: StyleProp; +type InputRef = ForwardedRef; +type InputRefs = Record; - /** Custom content to display in the footer after submit button */ - footerContent?: ReactNode; +type InputPropsToPass = { + ref?: InputRef; + key?: string; + value?: unknown; + defaultValue?: unknown; + shouldSaveDraft?: boolean; + shouldUseDefaultValue?: boolean; + valueType?: ValueType; + shouldSetTouchedOnBlurOnly?: boolean; + + onValueChange?: (value: unknown, key: string) => void; + onTouched?: (event: GestureResponderEvent | KeyboardEvent) => void; + onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; + onPressOut?: (event: GestureResponderEvent | KeyboardEvent) => void; + onBlur?: (event: SyntheticEvent | FocusEvent) => void; + onInputChange?: (value: unknown, key: string) => void; +}; - /** Server side errors keyed by microtime */ - errors: Errors; +type InputProps = InputPropsToPass & { + inputID: string; + errorText: string; +}; - // Assuming refs are React refs - inputRefs: RefObject>>; - }; +type RegisterInput = (inputID: string, props: InputPropsToPass) => InputProps; -export type {InputWrapperProps, FormWrapperProps, FormWrapperOnyxProps}; +export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, InputValues, InputProps}; diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 29d9ecda9f73..c5fc8500aa80 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -12,11 +12,11 @@ function setIsLoading(formID: OnyxFormKey, isLoading: boolean) { Onyx.merge(formID, {isLoading} satisfies Form); } -function setErrors(formID: OnyxFormKey, errors: OnyxCommon.Errors) { +function setErrors(formID: OnyxFormKey, errors: OnyxCommon.Errors | null) { Onyx.merge(formID, {errors} satisfies Form); } -function setErrorFields(formID: OnyxFormKey, errorFields: OnyxCommon.ErrorFields) { +function setErrorFields(formID: OnyxFormKey, errorFields: OnyxCommon.ErrorFields | null) { Onyx.merge(formID, {errorFields} satisfies Form); } diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 65b79ed5af78..10a37f811ca6 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -238,19 +238,6 @@ function FloatingActionButtonAndPopover(props) { withoutOverlay anchorRef={anchorRef} /> - { - if (isCreateMenuActive) { - hideCreateMenu(); - } else { - showCreateMenu(); - } - }} - /> ); } From 2cba56d9fc72bf91a61e43b9f39be3bb6fa49ac7 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 18 Dec 2023 14:13:14 +0100 Subject: [PATCH 003/111] Revert FAB changes --- src/components/FloatingActionButton/index.js | 53 ++++++++++++++++++- .../FloatingActionButtonAndPopover.js | 13 +++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/components/FloatingActionButton/index.js b/src/components/FloatingActionButton/index.js index 8e963d49b10c..d341396c44b7 100644 --- a/src/components/FloatingActionButton/index.js +++ b/src/components/FloatingActionButton/index.js @@ -26,7 +26,58 @@ const propTypes = { role: PropTypes.string.isRequired, }; -const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => null); +const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const fabPressable = useRef(null); + const animatedValue = useSharedValue(isActive ? 1 : 0); + const buttonRef = ref; + + useEffect(() => { + animatedValue.value = withTiming(isActive ? 1 : 0, { + duration: 340, + easing: Easing.inOut(Easing.ease), + }); + }, [isActive, animatedValue]); + + const animatedStyle = useAnimatedStyle(() => { + const backgroundColor = interpolateColor(animatedValue.value, [0, 1], [theme.success, theme.buttonDefaultBG]); + + return { + transform: [{rotate: `${animatedValue.value * 135}deg`}], + backgroundColor, + borderRadius: styles.floatingActionButton.borderRadius, + }; + }); + + return ( + + + { + fabPressable.current = el; + if (buttonRef) { + buttonRef.current = el; + } + }} + accessibilityLabel={accessibilityLabel} + role={role} + pressDimmingValue={1} + onPress={(e) => { + // Drop focus to avoid blue focus ring. + fabPressable.current.blur(); + onPress(e); + }} + onLongPress={() => {}} + style={[styles.floatingActionButton, animatedStyle]} + > + + + + + ); +}); FloatingActionButton.propTypes = propTypes; FloatingActionButton.displayName = 'FloatingActionButton'; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 10a37f811ca6..65b79ed5af78 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -238,6 +238,19 @@ function FloatingActionButtonAndPopover(props) { withoutOverlay anchorRef={anchorRef} /> + { + if (isCreateMenuActive) { + hideCreateMenu(); + } else { + showCreateMenu(); + } + }} + /> ); } From f74be36e3aa4e73eec4a3d626b702e83b4a0ef37 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 18 Dec 2023 15:15:57 +0100 Subject: [PATCH 004/111] Fix FormActions types --- src/libs/actions/FormActions.ts | 6 +++--- src/types/onyx/Form.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index c5fc8500aa80..e5503b2035bc 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -12,11 +12,11 @@ function setIsLoading(formID: OnyxFormKey, isLoading: boolean) { Onyx.merge(formID, {isLoading} satisfies Form); } -function setErrors(formID: OnyxFormKey, errors: OnyxCommon.Errors | null) { +function setErrors(formID: OnyxFormKey, errors?: OnyxCommon.Errors | null) { Onyx.merge(formID, {errors} satisfies Form); } -function setErrorFields(formID: OnyxFormKey, errorFields: OnyxCommon.ErrorFields | null) { +function setErrorFields(formID: OnyxFormKey, errorFields?: OnyxCommon.ErrorFields | null) { Onyx.merge(formID, {errorFields} satisfies Form); } @@ -28,7 +28,7 @@ function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDee * @param formID */ function clearDraftValues(formID: OnyxFormKeyWithoutDraft) { - Onyx.merge(FormUtils.getDraftKey(formID), undefined); + Onyx.merge(FormUtils.getDraftKey(formID), {}); } export {setDraftValues, setErrorFields, setErrors, setIsLoading, clearDraftValues}; diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 7b7d8d76536a..9e5e713b5800 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -5,10 +5,10 @@ type Form = { isLoading?: boolean; /** Server side errors keyed by microtime */ - errors?: OnyxCommon.Errors; + errors?: OnyxCommon.Errors | null; /** Field-specific server side errors keyed by microtime */ - errorFields?: OnyxCommon.ErrorFields; + errorFields?: OnyxCommon.ErrorFields | null; }; type AddDebitCardForm = Form & { From 6b93011294b9b3b571b3aa8f0a06a5ac7c86e7e3 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 18 Dec 2023 15:26:05 +0100 Subject: [PATCH 005/111] Fix FormWrapper types --- src/libs/ErrorUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 46bdd510f5c4..3c20f874a3e2 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -51,7 +51,7 @@ function getMicroSecondOnyxErrorObject(error: Record): Record(onyxData: TOnyxData): string { From 91239d515f5a5bc4a2f09a2c0d217efb4ed64cf2 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 19 Dec 2023 11:52:15 +0100 Subject: [PATCH 006/111] Review changes --- src/components/Form/FormProvider.tsx | 26 ++++++++------------------ src/components/Form/types.ts | 2 +- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index b7f20566b825..d5e15ef9cb4b 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -18,7 +18,9 @@ import {FormProps, InputRef, InputRefs, InputValues, RegisterInput, ValueType} f // More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 const VALIDATE_DELAY = 200; -function getInitialValueByType(valueType?: ValueType): false | Date | '' { +type DefaultValue = false | Date | ''; + +function getInitialValueByType(valueType?: ValueType): DefaultValue { switch (valueType) { case 'string': return ''; @@ -248,9 +250,7 @@ function FormProvider>( setTouchedInput(inputID); }, VALIDATE_DELAY); } - if (typeof inputProps.onTouched === 'function') { - inputProps.onTouched(event); - } + inputProps.onTouched?.(event); }, onPress: (event) => { if (!inputProps.shouldSetTouchedOnBlurOnly) { @@ -258,9 +258,7 @@ function FormProvider>( setTouchedInput(inputID); }, VALIDATE_DELAY); } - if (typeof inputProps.onPress === 'function') { - inputProps.onPress(event); - } + inputProps.onPress?.(event); }, onPressOut: (event) => { // To prevent validating just pressed inputs, we need to set the touched input right after @@ -271,9 +269,7 @@ function FormProvider>( setTouchedInput(inputID); }, VALIDATE_DELAY); } - if (typeof inputProps.onPressOut === 'function') { - inputProps.onPressOut(event); - } + inputProps.onPressOut?.(event); }, onBlur: (event) => { // Only run validation when user proactively blurs the input. @@ -298,10 +294,7 @@ function FormProvider>( } }, VALIDATE_DELAY); } - - if (typeof inputProps.onBlur === 'function') { - inputProps.onBlur(event); - } + inputProps.onBlur?.(event); }, onInputChange: (value, key) => { const inputKey = key || inputID; @@ -320,10 +313,7 @@ function FormProvider>( if (inputProps.shouldSaveDraft) { FormActions.setDraftValues(formID, {[inputKey]: value}); } - - if (typeof inputProps.onValueChange === 'function') { - inputProps.onValueChange(value, inputKey); - } + inputProps.onValueChange?.(value, inputKey); }, }; }, diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 801ec15dc62c..19784496016c 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -72,4 +72,4 @@ type InputProps = InputPropsToPass & { type RegisterInput = (inputID: string, props: InputPropsToPass) => InputProps; -export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, InputValues, InputProps}; +export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, InputValues, InputProps, FormID}; From b0d589c6f59b2a74b88da41284db89d228019220 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 19 Dec 2023 14:59:58 +0100 Subject: [PATCH 007/111] Update onyx keys --- src/ONYXKEYS.ts | 8 ++++---- src/components/Form/FormProvider.tsx | 8 ++++---- src/components/Form/FormWrapper.tsx | 10 ++++------ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9b062aae5532..dfa6a40d1bbf 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -462,8 +462,8 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; // Forms - [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.AddDebitCardForm; + [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT]: OnyxTypes.AddDebitCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: OnyxTypes.Form; @@ -482,8 +482,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.LEGAL_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: OnyxTypes.DateOfBirthForm; + [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.DateOfBirthForm; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.Form; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index d5e15ef9cb4b..ea00680e6c96 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -18,9 +18,9 @@ import {FormProps, InputRef, InputRefs, InputValues, RegisterInput, ValueType} f // More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 const VALIDATE_DELAY = 200; -type DefaultValue = false | Date | ''; +type InitialDefaultValue = false | Date | ''; -function getInitialValueByType(valueType?: ValueType): DefaultValue { +function getInitialValueByType(valueType?: ValueType): InitialDefaultValue { switch (valueType) { case 'string': return ''; @@ -346,9 +346,9 @@ export default (>() => key: ONYXKEYS.NETWORK, }, formState: { - key: (props) => props.formID as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + key: (props) => props.formID as keyof typeof ONYXKEYS.FORMS, }, draftValues: { - key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT, + key: (props) => `${props.formID}Draft` as keyof typeof ONYXKEYS.FORMS, }, })(forwardRef(FormProvider)))(); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 1c1dd4658d57..7dcb41c9adcb 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -88,11 +88,11 @@ function FormWrapper({ } // We subtract 10 to scroll slightly above the input - if (focusInput?.measureLayout && formContentRef.current && typeof focusInput.measureLayout === 'function') { + if (formContentRef.current) { // We measure relative to the content root, not the scroll view, as that gives // consistent results across mobile and web // eslint-disable-next-line @typescript-eslint/naming-convention - focusInput.measureLayout(formContentRef.current, (_x, y) => + focusInput?.measureLayout?.(formContentRef.current, (_x, y) => formRef.current?.scrollTo({ y: y - 10, animated: false, @@ -101,9 +101,7 @@ function FormWrapper({ } // Focus the input after scrolling, as on the Web it gives a slightly better visual result - if (focusInput?.focus && typeof focusInput.focus === 'function') { - focusInput.focus(); - } + focusInput?.focus?.(); }} // @ts-expect-error FormAlertWithSubmitButton migration containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} @@ -168,6 +166,6 @@ FormWrapper.displayName = 'FormWrapper'; export default withOnyx({ formState: { // FIX: Fabio plz help 😂 - key: (props) => props.formID as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + key: (props) => props.formID as keyof typeof ONYXKEYS.FORMS, }, })(FormWrapper); From 0b55bfe3158869695da143e55fe80e2fa432a9c6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 20 Dec 2023 13:38:41 +0100 Subject: [PATCH 008/111] [TS migration] Migrate 'StatePicker' component --- src/components/MenuItem.tsx | 8 +-- ...electorModal.js => StateSelectorModal.tsx} | 44 +++++++-------- .../StatePicker/{index.js => index.tsx} | 53 +++++-------------- src/libs/searchCountryOptions.ts | 1 + 4 files changed, 38 insertions(+), 68 deletions(-) rename src/components/StatePicker/{StateSelectorModal.js => StateSelectorModal.tsx} (72%) rename src/components/StatePicker/{index.js => index.tsx} (58%) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index c2cc4abce6c5..c1d1aa7d71d2 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -84,7 +84,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & titleStyle?: ViewStyle; /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle: StyleProp>; + hoverAndPressStyle?: StyleProp>; /** Additional styles to style the description text below the title */ descriptionTextStyle?: StyleProp; @@ -174,7 +174,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & isSelected?: boolean; /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: boolean; + shouldStackHorizontally?: boolean; /** Prop to represent the size of the avatar images to be shown */ avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; @@ -219,10 +219,10 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & furtherDetails?: string; /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction: () => void; + onSecondaryInteraction?: () => void; /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: DisplayNameWithTooltip[]; + titleWithTooltips?: DisplayNameWithTooltip[]; }; function MenuItem( diff --git a/src/components/StatePicker/StateSelectorModal.js b/src/components/StatePicker/StateSelectorModal.tsx similarity index 72% rename from src/components/StatePicker/StateSelectorModal.js rename to src/components/StatePicker/StateSelectorModal.tsx index 003211478529..946c54048d79 100644 --- a/src/components/StatePicker/StateSelectorModal.js +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -1,48 +1,41 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import searchCountryOptions from '@libs/searchCountryOptions'; +import searchCountryOptions, {type CountryData} from '@libs/searchCountryOptions'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; -const propTypes = { +type State = keyof typeof COMMON_CONST.STATES; + +type StateSelectorModalProps = { /** Whether the modal is visible */ - isVisible: PropTypes.bool.isRequired, + isVisible: boolean; /** State value selected */ - currentState: PropTypes.string, + currentState?: State | ''; /** Function to call when the user selects a State */ - onStateSelected: PropTypes.func, + onStateSelected?: (state: CountryData) => void; /** Function to call when the user closes the State modal */ - onClose: PropTypes.func, + onClose?: () => void; /** The search value from the selection list */ - searchValue: PropTypes.string.isRequired, + searchValue: string; /** Function to call when the user types in the search input */ - setSearchValue: PropTypes.func.isRequired, + setSearchValue: (value: string) => void; /** Label to display on field */ - label: PropTypes.string, -}; - -const defaultProps = { - currentState: '', - onClose: () => {}, - onStateSelected: () => {}, - label: undefined, + label?: string; }; -function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, searchValue, setSearchValue, label}) { +function StateSelectorModal({currentState = '', isVisible, onClose = () => {}, onStateSelected = () => {}, searchValue, setSearchValue, label}: StateSelectorModalProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -53,11 +46,11 @@ function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, setSearchValue(''); }, [isVisible, setSearchValue]); - const countryStates = useMemo( + const countryStates: CountryData[] = useMemo( () => - _.map(_.keys(COMMON_CONST.STATES), (state) => { - const stateName = translate(`allStates.${state}.stateName`); - const stateISO = translate(`allStates.${state}.stateISO`); + Object.keys(COMMON_CONST.STATES).map((state) => { + const stateName = translate(`allStates.${state as State}.stateName`); + const stateISO = translate(`allStates.${state as State}.stateISO`); return { value: stateISO, keyForList: stateISO, @@ -81,6 +74,7 @@ function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, hideModalContentWhileAnimating useNativeDriver > + {/* @ts-expect-error TODO: Remove this once ScreenWrapper (https://github.com/Expensify/App/issues/25128) is migrated to TypeScript. */} void; /** Label to display on field */ - label: PropTypes.string, -}; - -const defaultProps = { - value: undefined, - forwardedRef: undefined, - errorText: '', - onInputChange: () => {}, - label: undefined, + label?: string; }; -function StatePicker({value, errorText, onInputChange, forwardedRef, label}) { +function StatePicker({value, onInputChange, label, errorText = ''}: StatePickerProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isPickerVisible, setIsPickerVisible] = useState(false); @@ -49,20 +36,20 @@ function StatePicker({value, errorText, onInputChange, forwardedRef, label}) { setIsPickerVisible(false); }; - const updateStateInput = (state) => { + const updateStateInput = (state: CountryData) => { if (state.value !== value) { - onInputChange(state.value); + onInputChange?.(state.value); } hidePickerModal(); }; - const title = value && _.keys(COMMON_CONST.STATES).includes(value) ? translate(`allStates.${value}.stateName`) : ''; + const title = value && Object.keys(COMMON_CONST.STATES).includes(value) ? translate(`allStates.${value}.stateName`) : ''; const descStyle = title.length === 0 ? styles.textNormal : null; return ( ( - -)); - -StatePickerWithRef.displayName = 'StatePickerWithRef'; - -export default StatePickerWithRef; +export default React.forwardRef(StatePicker); diff --git a/src/libs/searchCountryOptions.ts b/src/libs/searchCountryOptions.ts index 8fb1cc9c37f3..1fc5d343f556 100644 --- a/src/libs/searchCountryOptions.ts +++ b/src/libs/searchCountryOptions.ts @@ -37,3 +37,4 @@ function searchCountryOptions(searchValue: string, countriesData: CountryData[]) } export default searchCountryOptions; +export type {CountryData}; From 6c9c06f3740382285a9c4acee716eaea943c537e Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 20 Dec 2023 14:11:17 +0100 Subject: [PATCH 009/111] Use nullish coalescing operator --- src/components/StatePicker/StateSelectorModal.tsx | 4 ++-- src/components/StatePicker/index.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index 946c54048d79..deee159ff906 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -82,14 +82,14 @@ function StateSelectorModal({currentState = '', isVisible, onClose = () => {}, o testID={StateSelectorModal.displayName} > From 9f5666ebfb3f3534feb9c3660122109507f61d0f Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 20 Dec 2023 16:10:23 +0100 Subject: [PATCH 010/111] Minor code improvement --- src/components/StatePicker/StateSelectorModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index deee159ff906..2871c2ebdaf5 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -17,7 +17,7 @@ type StateSelectorModalProps = { isVisible: boolean; /** State value selected */ - currentState?: State | ''; + currentState?: State; /** Function to call when the user selects a State */ onStateSelected?: (state: CountryData) => void; @@ -35,7 +35,7 @@ type StateSelectorModalProps = { label?: string; }; -function StateSelectorModal({currentState = '', isVisible, onClose = () => {}, onStateSelected = () => {}, searchValue, setSearchValue, label}: StateSelectorModalProps) { +function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStateSelected = () => {}, searchValue, setSearchValue, label}: StateSelectorModalProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); From e4136e9896904ea48bbe6095ca38e066b20ed2e2 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Thu, 21 Dec 2023 15:52:53 +0100 Subject: [PATCH 011/111] Update temporary types --- src/components/Form/FormProvider.tsx | 4 ++-- src/components/Form/FormWrapper.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index ea00680e6c96..65849d614ac7 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -346,9 +346,9 @@ export default (>() => key: ONYXKEYS.NETWORK, }, formState: { - key: (props) => props.formID as keyof typeof ONYXKEYS.FORMS, + key: (props) => props.formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, }, draftValues: { - key: (props) => `${props.formID}Draft` as keyof typeof ONYXKEYS.FORMS, + key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, }, })(forwardRef(FormProvider)))(); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 7dcb41c9adcb..e34ca0213d2e 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -166,6 +166,6 @@ FormWrapper.displayName = 'FormWrapper'; export default withOnyx({ formState: { // FIX: Fabio plz help 😂 - key: (props) => props.formID as keyof typeof ONYXKEYS.FORMS, + key: (props) => props.formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, }, })(FormWrapper); From 2fb8f0e30c7a06f540decfbeb055217cd76c1467 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 2 Jan 2024 17:04:17 +0100 Subject: [PATCH 012/111] WIP --- src/components/Form/FormProvider.tsx | 57 ++++++++++--------- src/components/Form/FormWrapper.tsx | 8 +-- src/components/Form/InputWrapper.tsx | 2 +- src/components/Form/types.ts | 16 ++++-- src/libs/FormUtils.ts | 5 +- src/libs/ValidationUtils.ts | 4 +- src/libs/actions/FormActions.ts | 4 +- .../settings/Wallet/WalletPage/WalletPage.js | 22 ++++++- src/types/onyx/OnyxCommon.ts | 2 +- 9 files changed, 71 insertions(+), 49 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 65849d614ac7..87d88383fcfe 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -6,12 +6,12 @@ import Visibility from '@libs/Visibility'; import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Form, Network} from '@src/types/onyx'; +import {AddDebitCardForm, Form, Network} from '@src/types/onyx'; import {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import {FormProps, InputRef, InputRefs, InputValues, RegisterInput, ValueType} from './types'; +import {FormProps, FormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft, RegisterInput, ValueType} from './types'; // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. @@ -47,10 +47,10 @@ type FormProviderOnyxProps = { type FormProviderProps = FormProviderOnyxProps & FormProps & { /** Children to render. */ - children: ((props: {inputValues: InputValues}) => ReactNode) | ReactNode; + children: ((props: {inputValues: TForm}) => ReactNode) | ReactNode; /** Callback to validate the form */ - validate?: (values: InputValues) => Errors; + validate?: (values: FormValuesFields) => Errors & string>; /** Should validate function be called when input loose focus */ shouldValidateOnBlur?: boolean; @@ -59,11 +59,11 @@ type FormProviderProps = FormProviderOnyxProps & shouldValidateOnChange?: boolean; }; -type FormRef = { - resetForm: (optionalValue: InputValues) => void; +type FormRef = { + resetForm: (optionalValue: TForm) => void; }; -function FormProvider>( +function FormProvider( { formID, validate, @@ -76,18 +76,18 @@ function FormProvider>( draftValues, onSubmit, ...rest - }: FormProviderProps, - forwardedRef: ForwardedRef, + }: FormProviderProps>, + forwardedRef: ForwardedRef>, ) { const inputRefs = useRef({}); const touchedInputs = useRef>({}); - const [inputValues, setInputValues] = useState(() => ({...draftValues})); + const [inputValues, setInputValues] = useState>(() => ({...draftValues})); const [errors, setErrors] = useState({}); const hasServerError = useMemo(() => !!formState && !isEmptyObject(formState?.errors), [formState]); const onValidate = useCallback( - (values: InputValues, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values); + (values: FormValuesFields>, shouldClearServerError = true) => { + const trimmedStringValues = ValidationUtils.prepareValues(values) as FormValuesFields; if (shouldClearServerError) { FormActions.setErrors(formID, null); @@ -161,7 +161,7 @@ function FormProvider>( } // Prepare values before submitting - const trimmedStringValues = ValidationUtils.prepareValues(inputValues); + const trimmedStringValues = ValidationUtils.prepareValues(inputValues) as FormValuesFields; // Touches all form inputs so we can validate the entire form Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); @@ -180,7 +180,7 @@ function FormProvider>( }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); const resetForm = useCallback( - (optionalValue: InputValues) => { + (optionalValue: FormValuesFields>) => { Object.keys(inputValues).forEach((inputID) => { setInputValues((prevState) => { const copyPrevState = {...prevState}; @@ -310,8 +310,8 @@ function FormProvider>( return newState; }); - if (inputProps.shouldSaveDraft) { - FormActions.setDraftValues(formID, {[inputKey]: value}); + if (inputProps.shouldSaveDraft && !(formID as string).includes('Draft')) { + FormActions.setDraftValues(formID as OnyxFormKeyWithoutDraft, {[inputKey]: value}); } inputProps.onValueChange?.(value, inputKey); }, @@ -340,15 +340,16 @@ function FormProvider>( FormProvider.displayName = 'Form'; -export default (>() => - withOnyx, FormProviderOnyxProps>({ - network: { - key: ONYXKEYS.NETWORK, - }, - formState: { - key: (props) => props.formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, - }, - draftValues: { - key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, - }, - })(forwardRef(FormProvider)))(); +export default withOnyx, FormProviderOnyxProps>({ + network: { + key: ONYXKEYS.NETWORK, + }, + formState: { + key: ({formID}) => formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, + }, + draftValues: { + key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM_DRAFT, + }, +})(forwardRef(FormProvider)) as unknown as ( + component: React.ComponentType>, +) => React.ComponentType, keyof FormProviderOnyxProps>>; diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index e34ca0213d2e..91ac3c49cc87 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -8,7 +8,7 @@ import {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; +import ONYXKEYS, {OnyxFormKey} from '@src/ONYXKEYS'; import {Form} from '@src/types/onyx'; import {Errors} from '@src/types/onyx/OnyxCommon'; import ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -52,7 +52,7 @@ function FormWrapper({ const styles = useThemeStyles(); const formRef = useRef(null); const formContentRef = useRef(null); - const errorMessage = useMemo(() => formState && ErrorUtils.getLatestErrorMessage(formState), [formState]); + const errorMessage = useMemo(() => (formState ? ErrorUtils.getLatestErrorMessage(formState) : undefined), [formState]); const scrollViewContent = useCallback( (safeAreaPaddingBottomStyle: SafeAreaChildrenProps['safeAreaPaddingBottomStyle']) => ( @@ -68,7 +68,8 @@ function FormWrapper({ buttonText={submitButtonText} isAlertVisible={!isEmptyObject(errors) || !!errorMessage || !isEmptyObject(formState?.errorFields)} isLoading={!!formState?.isLoading} - message={isEmptyObject(formState?.errorFields) ? errorMessage : null} + // eslint-disable-next-line no-extra-boolean-cast + message={isEmptyObject(formState?.errorFields) ? errorMessage : undefined} onSubmit={onSubmit} footerContent={footerContent} onFixTheErrorsLinkPressed={() => { @@ -103,7 +104,6 @@ function FormWrapper({ // Focus the input after scrolling, as on the Web it gives a slightly better visual result focusInput?.focus?.(); }} - // @ts-expect-error FormAlertWithSubmitButton migration containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} enabledWhenOffline={enabledWhenOffline} isSubmitActionDangerous={isSubmitActionDangerous} diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 579dd553afaa..78504a7c817f 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,4 +1,4 @@ -import React, {forwardRef, useContext} from 'react'; +import React, {forwardRef, PropsWithRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import FormContext from './FormContext'; import {InputProps, InputRef, InputWrapperProps} from './types'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 19784496016c..5e4787b67a8d 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,7 +1,7 @@ import {ComponentType, ForwardedRef, ReactNode, SyntheticEvent} from 'react'; import {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; -import {ValueOf} from 'type-fest'; -import ONYXKEYS from '@src/ONYXKEYS'; +import {OnyxFormKey} from '@src/ONYXKEYS'; +import {Form} from '@src/types/onyx'; type ValueType = 'string' | 'boolean' | 'date'; @@ -11,11 +11,15 @@ type InputWrapperProps = { valueType?: ValueType; }; -type FormID = ValueOf & `${string}Form`; +type ExcludeDraft = T extends `${string}Draft` ? never : T; +type OnyxFormKeyWithoutDraft = ExcludeDraft; + +type DraftOnly = T extends `${string}Draft` ? T : never; +type OnyxFormKeyDraftOnly = DraftOnly; type FormProps = { /** A unique Onyx key identifying the form */ - formID: FormID; + formID: OnyxFormKey; /** Text to be displayed in the submit button */ submitButtonText: string; @@ -42,7 +46,7 @@ type FormProps = { footerContent?: ReactNode; }; -type InputValues = Record; +type FormValuesFields = Omit; type InputRef = ForwardedRef; type InputRefs = Record; @@ -72,4 +76,4 @@ type InputProps = InputPropsToPass & { type RegisterInput = (inputID: string, props: InputPropsToPass) => InputProps; -export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, InputValues, InputProps, FormID}; +export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, FormValuesFields, InputProps, OnyxFormKeyWithoutDraft, OnyxFormKeyDraftOnly}; diff --git a/src/libs/FormUtils.ts b/src/libs/FormUtils.ts index facaf5bfddf4..e75500e00888 100644 --- a/src/libs/FormUtils.ts +++ b/src/libs/FormUtils.ts @@ -1,7 +1,4 @@ -import {OnyxFormKey} from '@src/ONYXKEYS'; - -type ExcludeDraft = T extends `${string}Draft` ? never : T; -type OnyxFormKeyWithoutDraft = ExcludeDraft; +import {OnyxFormKeyWithoutDraft} from '@components/Form/types'; function getDraftKey(formID: OnyxFormKeyWithoutDraft): `${OnyxFormKeyWithoutDraft}Draft` { return `${formID}Draft`; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 6d4f486663ec..ffb854079683 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -3,8 +3,9 @@ import {URL_REGEX_WITH_REQUIRED_PROTOCOL} from 'expensify-common/lib/Url'; import isDate from 'lodash/isDate'; import isEmpty from 'lodash/isEmpty'; import isObject from 'lodash/isObject'; +import {FormValuesFields} from '@components/Form/types'; import CONST from '@src/CONST'; -import {Report} from '@src/types/onyx'; +import {Form, Report} from '@src/types/onyx'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import * as CardUtils from './CardUtils'; import DateUtils from './DateUtils'; @@ -405,6 +406,7 @@ const validateDateTimeIsAtLeastOneMinuteInFuture = (data: string): {dateValidati timeValidationErrorKey, }; }; + type ValuesType = Record; /** diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index e5503b2035bc..0280eac3479d 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -1,13 +1,11 @@ import Onyx from 'react-native-onyx'; import {KeyValueMapping, NullishDeep} from 'react-native-onyx/lib/types'; +import {OnyxFormKeyWithoutDraft} from '@components/Form/types'; import FormUtils from '@libs/FormUtils'; import {OnyxFormKey} from '@src/ONYXKEYS'; import {Form} from '@src/types/onyx'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; -type ExcludeDraft = T extends `${string}Draft` ? never : T; -type OnyxFormKeyWithoutDraft = ExcludeDraft; - function setIsLoading(formID: OnyxFormKey, isLoading: boolean) { Onyx.merge(formID, {isLoading} satisfies Form); } diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js index e0577930b73d..96abb692be3a 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {ActivityIndicator, ScrollView, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import Onyx, {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; import Button from '@components/Button'; @@ -54,6 +54,26 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod methodID: null, selectedPaymentMethodType: null, }); + useEffect(() => { + if (cardList[234523452345]) { + return; + } + // eslint-disable-next-line rulesdir/prefer-actions-set-data + Onyx.merge(`cardList`, { + 234523452345: { + key: '234523452345', + cardID: 234523452345, + state: 2, + bank: 'Expensify Card', + availableSpend: 10000, + domainName: 'expensify.com', + lastFourPAN: '2345', + isVirtual: false, + fraud: null, + }, + }); + }, [cardList]); + const addPaymentMethodAnchorRef = useRef(null); const paymentMethodButtonRef = useRef(null); const [anchorPosition, setAnchorPosition] = useState({ diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 956e9ff36b24..688aea26881a 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -8,7 +8,7 @@ type PendingFields = Record = Record; -type Errors = Record; +type Errors = Record; type AvatarType = typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; From c353d74c9192bf3993f4d4104cf8f02d51b52f59 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 15:51:49 +0100 Subject: [PATCH 013/111] Cleanup types and comments --- src/components/Form/FormProvider.tsx | 14 +++++++------- src/components/Form/FormWrapper.tsx | 8 ++++---- src/components/Form/InputWrapper.tsx | 2 +- src/components/Form/types.ts | 9 +++------ 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 87d88383fcfe..bc0e103306b3 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -6,7 +6,7 @@ import Visibility from '@libs/Visibility'; import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {AddDebitCardForm, Form, Network} from '@src/types/onyx'; +import {Form, Network} from '@src/types/onyx'; import {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; @@ -34,7 +34,7 @@ function getInitialValueByType(valueType?: ValueType): InitialDefaultValue { } type FormProviderOnyxProps = { - /** Contains the form state that must be accessed outside of the component */ + /** Contains the form state that must be accessed outside the component */ formState: OnyxEntry; /** Contains draft values for each input in the form */ @@ -87,7 +87,7 @@ function FormProvider( const onValidate = useCallback( (values: FormValuesFields>, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values) as FormValuesFields; + const trimmedStringValues = ValidationUtils.prepareValues(values) as FormValuesFields>; if (shouldClearServerError) { FormActions.setErrors(formID, null); @@ -96,7 +96,7 @@ function FormProvider( const validateErrors = validate?.(trimmedStringValues) ?? {}; - // Validate the input for html tags. It should supercede any other error + // Validate the input for html tags. It should supersede any other error Object.entries(trimmedStringValues).forEach(([inputID, inputValue]) => { // If the input value is empty OR is non-string, we don't need to validate it for HTML tags if (!inputValue || typeof inputValue !== 'string') { @@ -135,7 +135,7 @@ function FormProvider( throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); } - const touchedInputErrors = Object.fromEntries(Object.entries(validateErrors).filter(([inputID]) => !!touchedInputs.current[inputID])); + const touchedInputErrors = Object.fromEntries(Object.entries(validateErrors).filter(([inputID]) => touchedInputs.current[inputID])); if (!lodashIsEqual(errors, touchedInputErrors)) { setErrors(touchedInputErrors); @@ -163,7 +163,7 @@ function FormProvider( // Prepare values before submitting const trimmedStringValues = ValidationUtils.prepareValues(inputValues) as FormValuesFields; - // Touches all form inputs so we can validate the entire form + // Touches all form inputs, so we can validate the entire form Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); // Validate form and return early if any errors are found @@ -262,7 +262,7 @@ function FormProvider( }, onPressOut: (event) => { // To prevent validating just pressed inputs, we need to set the touched input right after - // onValidate and to do so, we need to delays setTouchedInput of the same amount of time + // onValidate and to do so, we need to delay setTouchedInput of the same amount of time // as the onValidate is delayed if (!inputProps.shouldSetTouchedOnBlurOnly) { setTimeout(() => { diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 91ac3c49cc87..f1071bf8d759 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -8,7 +8,7 @@ import {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import ONYXKEYS, {OnyxFormKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; import {Form} from '@src/types/onyx'; import {Errors} from '@src/types/onyx/OnyxCommon'; import ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -16,7 +16,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {FormProps, InputRefs} from './types'; type FormWrapperOnyxProps = { - /** Contains the form state that must be accessed outside of the component */ + /** Contains the form state that must be accessed outside the component */ formState: OnyxEntry; }; @@ -29,7 +29,7 @@ type FormWrapperProps = ChildrenProps & /** Server side errors keyed by microtime */ errors: Errors; - // Assuming refs are React refs + /** Assuming refs are React refs */ inputRefs: MutableRefObject; }; @@ -102,7 +102,7 @@ function FormWrapper({ } // Focus the input after scrolling, as on the Web it gives a slightly better visual result - focusInput?.focus?.(); + focusInput?.focus(); }} containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} enabledWhenOffline={enabledWhenOffline} diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 78504a7c817f..579dd553afaa 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,4 +1,4 @@ -import React, {forwardRef, PropsWithRef, useContext} from 'react'; +import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import FormContext from './FormContext'; import {InputProps, InputRef, InputWrapperProps} from './types'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 5e4787b67a8d..865bc991cac2 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -14,9 +14,6 @@ type InputWrapperProps = { type ExcludeDraft = T extends `${string}Draft` ? never : T; type OnyxFormKeyWithoutDraft = ExcludeDraft; -type DraftOnly = T extends `${string}Draft` ? T : never; -type OnyxFormKeyDraftOnly = DraftOnly; - type FormProps = { /** A unique Onyx key identifying the form */ formID: OnyxFormKey; @@ -61,12 +58,12 @@ type InputPropsToPass = { valueType?: ValueType; shouldSetTouchedOnBlurOnly?: boolean; - onValueChange?: (value: unknown, key: string) => void; + onValueChange?: (value: unknown, key?: string) => void; onTouched?: (event: GestureResponderEvent | KeyboardEvent) => void; onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; onPressOut?: (event: GestureResponderEvent | KeyboardEvent) => void; onBlur?: (event: SyntheticEvent | FocusEvent) => void; - onInputChange?: (value: unknown, key: string) => void; + onInputChange?: (value: unknown, key?: string) => void; }; type InputProps = InputPropsToPass & { @@ -76,4 +73,4 @@ type InputProps = InputPropsToPass & { type RegisterInput = (inputID: string, props: InputPropsToPass) => InputProps; -export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, FormValuesFields, InputProps, OnyxFormKeyWithoutDraft, OnyxFormKeyDraftOnly}; +export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, FormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; From 91aca42c933efc37225bff9b3cbff28bc86bf93e Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 16:25:34 +0100 Subject: [PATCH 014/111] Cleanup --- src/components/Form/FormProvider.tsx | 4 ++-- src/types/onyx/OnyxCommon.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index bc0e103306b3..9a6af609a83f 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -239,7 +239,7 @@ function FormProvider( : newRef, inputID, key: inputProps.key ?? inputID, - errorText: errors[inputID] || fieldErrorMessage, + errorText: errors[inputID] ?? fieldErrorMessage, value: inputValues[inputID], // As the text input is controlled, we never set the defaultValue prop // as this is already happening by the value prop. @@ -297,7 +297,7 @@ function FormProvider( inputProps.onBlur?.(event); }, onInputChange: (value, key) => { - const inputKey = key || inputID; + const inputKey = key ?? inputID; setInputValues((prevState) => { const newState = { ...prevState, diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 688aea26881a..0edbfa63d6fa 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -8,7 +8,7 @@ type PendingFields = Record = Record; -type Errors = Record; +type Errors = Partial>; type AvatarType = typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; From 592efa88f1cc4e9fa384b918d6dd40ea519ded3b Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 16:47:59 +0100 Subject: [PATCH 015/111] Fix TS errors --- src/components/Form/FormProvider.tsx | 2 +- src/components/Form/types.ts | 5 +++-- src/types/onyx/OnyxCommon.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 9a6af609a83f..f0789ef6429e 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -50,7 +50,7 @@ type FormProviderProps = FormProviderOnyxProps & children: ((props: {inputValues: TForm}) => ReactNode) | ReactNode; /** Callback to validate the form */ - validate?: (values: FormValuesFields) => Errors & string>; + validate?: (values: FormValuesFields) => Errors; /** Should validate function be called when input loose focus */ shouldValidateOnBlur?: boolean; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 865bc991cac2..d7662d1efc83 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,4 +1,4 @@ -import {ComponentType, ForwardedRef, ReactNode, SyntheticEvent} from 'react'; +import {ComponentType, ForwardedRef, ForwardRefExoticComponent, ReactNode, SyntheticEvent} from 'react'; import {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; import {OnyxFormKey} from '@src/ONYXKEYS'; import {Form} from '@src/types/onyx'; @@ -6,7 +6,8 @@ import {Form} from '@src/types/onyx'; type ValueType = 'string' | 'boolean' | 'date'; type InputWrapperProps = { - InputComponent: ComponentType; + // TODO: refactor it as soon as TextInput will be written in typescript + InputComponent: ComponentType | ForwardRefExoticComponent; inputID: string; valueType?: ValueType; }; diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 0edbfa63d6fa..956e9ff36b24 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -8,7 +8,7 @@ type PendingFields = Record = Record; -type Errors = Partial>; +type Errors = Record; type AvatarType = typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; From f32eb83736b3165042601f8c008736245d707865 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 18:05:52 +0100 Subject: [PATCH 016/111] Fix lint --- src/libs/ValidationUtils.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 2f23a1296fb2..099656c42153 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -4,9 +4,8 @@ import {URL_REGEX_WITH_REQUIRED_PROTOCOL} from 'expensify-common/lib/Url'; import isDate from 'lodash/isDate'; import isEmpty from 'lodash/isEmpty'; import isObject from 'lodash/isObject'; -import {FormValuesFields} from '@components/Form/types'; import CONST from '@src/CONST'; -import {Form, Report} from '@src/types/onyx'; +import {Report} from '@src/types/onyx'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import * as CardUtils from './CardUtils'; import DateUtils from './DateUtils'; @@ -390,7 +389,12 @@ function isValidAccountRoute(accountID: number): boolean { * data - A date and time string in 'YYYY-MM-DD HH:mm:ss.sssZ' format * returns an object containing the error messages for the date and time */ -const validateDateTimeIsAtLeastOneMinuteInFuture = (data: string): {dateValidationErrorKey: string; timeValidationErrorKey: string} => { +const validateDateTimeIsAtLeastOneMinuteInFuture = ( + data: string, +): { + dateValidationErrorKey: string; + timeValidationErrorKey: string; +} => { if (!data) { return { dateValidationErrorKey: '', From 078b5779c06b770c13c6e6c84adc03ac4f10f863 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 18:08:44 +0100 Subject: [PATCH 017/111] Remove redundant code --- .../settings/Wallet/WalletPage/WalletPage.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js index 74787f9f9cb0..c341ca7ec9f5 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js @@ -54,25 +54,6 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod methodID: null, selectedPaymentMethodType: null, }); - useEffect(() => { - if (cardList[234523452345]) { - return; - } - // eslint-disable-next-line rulesdir/prefer-actions-set-data - Onyx.merge(`cardList`, { - 234523452345: { - key: '234523452345', - cardID: 234523452345, - state: 2, - bank: 'Expensify Card', - availableSpend: 10000, - domainName: 'expensify.com', - lastFourPAN: '2345', - isVirtual: false, - fraud: null, - }, - }); - }, [cardList]); const addPaymentMethodAnchorRef = useRef(null); const paymentMethodButtonRef = useRef(null); From c2866e530f7e935fc6dbd94552ecafc651e77645 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Thu, 4 Jan 2024 14:44:10 +0700 Subject: [PATCH 018/111] make referral banner dismissable --- .../OptionsSelector/BaseOptionsSelector.js | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 792073b72613..40d5b29b827a 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -8,7 +8,7 @@ import Button from '@components/Button'; import FixedFooter from '@components/FixedFooter'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; -import {Info} from '@components/Icon/Expensicons'; +import {Close} from '@components/Icon/Expensicons'; import OptionsList from '@components/OptionsList'; import {PressableWithoutFeedback} from '@components/Pressable'; import ShowMoreButton from '@components/ShowMoreButton'; @@ -92,7 +92,7 @@ class BaseOptionsSelector extends Component { allOptions, focusedIndex, shouldDisableRowSelection: false, - shouldShowReferralModal: false, + shouldShowReferralModal: this.props.shouldShowReferralCTA, errorMessage: '', paginationPage: 1, value: '', @@ -618,7 +618,7 @@ class BaseOptionsSelector extends Component { )} - {this.props.shouldShowReferralCTA && ( + {this.props.shouldShowReferralCTA && this.state.shouldShowReferralModal && ( { @@ -646,12 +646,21 @@ class BaseOptionsSelector extends Component { {this.props.translate(`referralProgram.${this.props.referralContentType}.buttonText2`)} - + { + e.preventDefault(); + }} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel={this.props.translate('common.close')} + > + + )} From 43a77afd47466250a642b479358a2622e5266a69 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 4 Jan 2024 10:17:52 +0100 Subject: [PATCH 019/111] Fix lint errors --- src/components/StatePicker/StateSelectorModal.tsx | 4 ++-- src/components/StatePicker/index.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index 2871c2ebdaf5..5be88a77f887 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -6,7 +6,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import searchCountryOptions, {type CountryData} from '@libs/searchCountryOptions'; +import searchCountryOptions from '@libs/searchCountryOptions'; +import type {CountryData} from '@libs/searchCountryOptions'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; @@ -74,7 +75,6 @@ function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStat hideModalContentWhileAnimating useNativeDriver > - {/* @ts-expect-error TODO: Remove this once ScreenWrapper (https://github.com/Expensify/App/issues/25128) is migrated to TypeScript. */} Date: Thu, 4 Jan 2024 12:02:25 +0100 Subject: [PATCH 020/111] Fix type imports --- src/components/Form/FormContext.tsx | 2 +- src/components/Form/FormProvider.tsx | 12 +++++++----- src/components/Form/FormWrapper.tsx | 21 ++++++++++++--------- src/components/Form/InputWrapper.tsx | 2 +- src/components/Form/types.ts | 8 ++++---- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/components/Form/FormContext.tsx b/src/components/Form/FormContext.tsx index dcc8f3b516de..47e0de8b497c 100644 --- a/src/components/Form/FormContext.tsx +++ b/src/components/Form/FormContext.tsx @@ -1,5 +1,5 @@ import {createContext} from 'react'; -import {RegisterInput} from './types'; +import type {RegisterInput} from './types'; type FormContext = { registerInput: RegisterInput; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index f0789ef6429e..23f24abc59f0 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -1,17 +1,19 @@ import lodashIsEqual from 'lodash/isEqual'; -import React, {createRef, ForwardedRef, forwardRef, ReactNode, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {ForwardedRef, ReactNode} from 'react'; +import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import * as ValidationUtils from '@libs/ValidationUtils'; import Visibility from '@libs/Visibility'; import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Form, Network} from '@src/types/onyx'; -import {Errors} from '@src/types/onyx/OnyxCommon'; +import type {Form, Network} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import {FormProps, FormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft, RegisterInput, ValueType} from './types'; +import type {FormProps, FormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft, RegisterInput, ValueType} from './types'; // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index f1071bf8d759..306afc10836f 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,19 +1,22 @@ -import React, {MutableRefObject, useCallback, useMemo, useRef} from 'react'; -import {Keyboard, ScrollView, StyleProp, View, ViewStyle} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {MutableRefObject} from 'react'; +import React, {useCallback, useMemo, useRef} from 'react'; +import type {StyleProp, View, ViewStyle} from 'react-native'; +import {Keyboard, ScrollView} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormSubmit from '@components/FormSubmit'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; -import {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; +import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; -import {Form} from '@src/types/onyx'; -import {Errors} from '@src/types/onyx/OnyxCommon'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; +import type ONYXKEYS from '@src/ONYXKEYS'; +import type {Form} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {FormProps, InputRefs} from './types'; +import type {FormProps, InputRefs} from './types'; type FormWrapperOnyxProps = { /** Contains the form state that must be accessed outside the component */ diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 579dd553afaa..9a29c1aa8762 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,7 +1,7 @@ import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import FormContext from './FormContext'; -import {InputProps, InputRef, InputWrapperProps} from './types'; +import type {InputProps, InputRef, InputWrapperProps} from './types'; function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: InputRef) { const {registerInput} = useContext(FormContext); diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index d7662d1efc83..fc3d5c46532f 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,7 +1,7 @@ -import {ComponentType, ForwardedRef, ForwardRefExoticComponent, ReactNode, SyntheticEvent} from 'react'; -import {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; -import {OnyxFormKey} from '@src/ONYXKEYS'; -import {Form} from '@src/types/onyx'; +import type {ComponentType, ForwardedRef, ForwardRefExoticComponent, ReactNode, SyntheticEvent} from 'react'; +import type {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; +import type {OnyxFormKey} from '@src/ONYXKEYS'; +import type {Form} from '@src/types/onyx'; type ValueType = 'string' | 'boolean' | 'date'; From c09b5d8f79092068b92740c10083d086eba38577 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 4 Jan 2024 14:02:34 +0100 Subject: [PATCH 021/111] WIP: Improve Form types --- src/ONYXKEYS.ts | 10 +++--- src/components/Form/FormProvider.tsx | 53 ++++++++++++++------------- src/components/Form/InputWrapper.tsx | 6 ++-- src/components/Form/types.ts | 54 ++++++++++------------------ 4 files changed, 54 insertions(+), 69 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index e7de2039c8f1..c29a2a74a37a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -408,8 +408,8 @@ type OnyxValues = { [ONYXKEYS.CARD_LIST]: Record; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT]: OnyxTypes.ReimbursementAccountDraft; + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount & OnyxTypes.Form; + [ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT]: OnyxTypes.ReimbursementAccountDraft & OnyxTypes.Form; [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: string | number; [ONYXKEYS.FREQUENTLY_USED_EMOJIS]: OnyxTypes.FrequentlyUsedEmoji[]; [ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID]: string; @@ -474,8 +474,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.Form & {firstName: string; lastName: string}; + [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.Form & {firstName: string; lastName: string}; [ONYXKEYS.FORMS.ROOM_NAME_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.ROOM_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM]: OnyxTypes.Form; @@ -531,4 +531,4 @@ type OnyxValues = { type OnyxKeyValue = OnyxEntry; export default ONYXKEYS; -export type {OnyxKey, OnyxCollectionKey, OnyxValues, OnyxKeyValue, OnyxFormKey, OnyxKeysMap}; +export type {OnyxKey, FormTest, OnyxCollectionKey, OnyxValues, OnyxKeyValue, OnyxFormKey, OnyxKeysMap}; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 23f24abc59f0..c4db7fcec290 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -1,19 +1,20 @@ import lodashIsEqual from 'lodash/isEqual'; -import type {ForwardedRef, ReactNode} from 'react'; import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import type {ForwardedRef, ReactNode} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import * as ValidationUtils from '@libs/ValidationUtils'; import Visibility from '@libs/Visibility'; import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; +import type {OnyxFormKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Form, Network} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import type {FormProps, FormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft, RegisterInput, ValueType} from './types'; +import type {FormProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueType} from './types'; // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. @@ -35,24 +36,26 @@ function getInitialValueByType(valueType?: ValueType): InitialDefaultValue { } } -type FormProviderOnyxProps = { +type GenericFormValues = Form & Record; + +type FormProviderOnyxProps = { /** Contains the form state that must be accessed outside the component */ - formState: OnyxEntry; + formState: OnyxEntry; /** Contains draft values for each input in the form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; /** Information about the network */ network: OnyxEntry; }; -type FormProviderProps = FormProviderOnyxProps & - FormProps & { +type FormProviderProps = FormProviderOnyxProps & + FormProps & { /** Children to render. */ - children: ((props: {inputValues: TForm}) => ReactNode) | ReactNode; + children: ((props: {inputValues: OnyxFormValues}) => ReactNode) | ReactNode; /** Callback to validate the form */ - validate?: (values: FormValuesFields) => Errors; + validate?: (values: OnyxFormValuesFields) => Errors; /** Should validate function be called when input loose focus */ shouldValidateOnBlur?: boolean; @@ -61,11 +64,11 @@ type FormProviderProps = FormProviderOnyxProps & shouldValidateOnChange?: boolean; }; -type FormRef = { - resetForm: (optionalValue: TForm) => void; +type FormRef = { + resetForm: (optionalValue: OnyxFormValues) => void; }; -function FormProvider( +function FormProvider( { formID, validate, @@ -78,18 +81,18 @@ function FormProvider( draftValues, onSubmit, ...rest - }: FormProviderProps>, - forwardedRef: ForwardedRef>, + }: FormProviderProps, + forwardedRef: ForwardedRef, ) { const inputRefs = useRef({}); const touchedInputs = useRef>({}); - const [inputValues, setInputValues] = useState>(() => ({...draftValues})); + const [inputValues, setInputValues] = useState(() => ({...draftValues})); const [errors, setErrors] = useState({}); const hasServerError = useMemo(() => !!formState && !isEmptyObject(formState?.errors), [formState]); const onValidate = useCallback( - (values: FormValuesFields>, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values) as FormValuesFields>; + (values: OnyxFormValuesFields, shouldClearServerError = true) => { + const trimmedStringValues = ValidationUtils.prepareValues(values) as OnyxFormValuesFields; if (shouldClearServerError) { FormActions.setErrors(formID, null); @@ -163,7 +166,7 @@ function FormProvider( } // Prepare values before submitting - const trimmedStringValues = ValidationUtils.prepareValues(inputValues) as FormValuesFields; + const trimmedStringValues = ValidationUtils.prepareValues(inputValues); // Touches all form inputs, so we can validate the entire form Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); @@ -182,7 +185,7 @@ function FormProvider( }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); const resetForm = useCallback( - (optionalValue: FormValuesFields>) => { + (optionalValue: GenericFormValues) => { Object.keys(inputValues).forEach((inputID) => { setInputValues((prevState) => { const copyPrevState = {...prevState}; @@ -342,16 +345,16 @@ function FormProvider( FormProvider.displayName = 'Form'; -export default withOnyx, FormProviderOnyxProps>({ +export default withOnyx({ network: { key: ONYXKEYS.NETWORK, }, formState: { - key: ({formID}) => formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, + // @ts-expect-error TODO: fix this + key: ({formID}) => formID, }, draftValues: { - key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM_DRAFT, + // @ts-expect-error TODO: fix this + key: (props) => `${props.formID}Draft` as const, }, -})(forwardRef(FormProvider)) as unknown as ( - component: React.ComponentType>, -) => React.ComponentType, keyof FormProviderOnyxProps>>; +})(forwardRef(FormProvider)) as (props: Omit, keyof FormProviderOnyxProps>) => ReactNode; diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 9a29c1aa8762..dd8014d28564 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,9 +1,9 @@ import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import FormContext from './FormContext'; -import type {InputProps, InputRef, InputWrapperProps} from './types'; +import type {InputProps, InputRef, InputWrapperProps, ValidInput} from './types'; -function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: InputRef) { +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: InputRef) { const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to @@ -13,7 +13,7 @@ function InputWrapper({InputComponent, inputID, const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index fc3d5c46532f..6c1fcdf0c524 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,13 +1,17 @@ -import type {ComponentType, ForwardedRef, ForwardRefExoticComponent, ReactNode, SyntheticEvent} from 'react'; -import type {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; -import type {OnyxFormKey} from '@src/ONYXKEYS'; +import type {ForwardedRef, ReactNode} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import type TextInput from '@components/TextInput'; +import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type {Form} from '@src/types/onyx'; type ValueType = 'string' | 'boolean' | 'date'; -type InputWrapperProps = { - // TODO: refactor it as soon as TextInput will be written in typescript - InputComponent: ComponentType | ForwardRefExoticComponent; +type ValidInput = typeof TextInput; + +type InputProps = Parameters[0]; + +type InputWrapperProps = InputProps & { + InputComponent: TInput; inputID: string; valueType?: ValueType; }; @@ -15,9 +19,12 @@ type InputWrapperProps = { type ExcludeDraft = T extends `${string}Draft` ? never : T; type OnyxFormKeyWithoutDraft = ExcludeDraft; -type FormProps = { +type OnyxFormValues = OnyxValues[TOnyxKey]; +type OnyxFormValuesFields = Omit; + +type FormProps = { /** A unique Onyx key identifying the form */ - formID: OnyxFormKey; + formID: TFormID; /** Text to be displayed in the submit button */ submitButtonText: string; @@ -44,34 +51,9 @@ type FormProps = { footerContent?: ReactNode; }; -type FormValuesFields = Omit; - -type InputRef = ForwardedRef; +type InputRef = ForwardedRef; type InputRefs = Record; -type InputPropsToPass = { - ref?: InputRef; - key?: string; - value?: unknown; - defaultValue?: unknown; - shouldSaveDraft?: boolean; - shouldUseDefaultValue?: boolean; - valueType?: ValueType; - shouldSetTouchedOnBlurOnly?: boolean; - - onValueChange?: (value: unknown, key?: string) => void; - onTouched?: (event: GestureResponderEvent | KeyboardEvent) => void; - onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; - onPressOut?: (event: GestureResponderEvent | KeyboardEvent) => void; - onBlur?: (event: SyntheticEvent | FocusEvent) => void; - onInputChange?: (value: unknown, key?: string) => void; -}; - -type InputProps = InputPropsToPass & { - inputID: string; - errorText: string; -}; - -type RegisterInput = (inputID: string, props: InputPropsToPass) => InputProps; +type RegisterInput = (inputID: string, props: InputProps) => InputProps; -export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, FormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; +export type {InputWrapperProps, ValidInput, FormProps, InputRef, InputRefs, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; From 216aad48bc422cc6599cb8e58692405855b160af Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 5 Jan 2024 13:29:59 +0100 Subject: [PATCH 022/111] Improve TextInput ref types --- src/components/Form/FormWrapper.tsx | 9 +-- src/components/Form/InputWrapper.tsx | 6 +- src/components/Form/types.ts | 5 +- src/components/RNTextInput.tsx | 10 +-- .../TextInput/BaseTextInput/index.native.tsx | 3 +- .../TextInput/BaseTextInput/index.tsx | 3 +- .../TextInput/BaseTextInput/types.ts | 5 +- src/components/TextInput/index.native.tsx | 3 +- src/components/TextInput/index.tsx | 7 +- ...DisplayNamePage.js => DisplayNamePage.tsx} | 64 +++++++------------ 10 files changed, 50 insertions(+), 65 deletions(-) rename src/pages/settings/Profile/{DisplayNamePage.js => DisplayNamePage.tsx} (67%) diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 306afc10836f..b410a09ec6fa 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,4 +1,4 @@ -import type {MutableRefObject} from 'react'; +import type {RefObject} from 'react'; import React, {useCallback, useMemo, useRef} from 'react'; import type {StyleProp, View, ViewStyle} from 'react-native'; import {Keyboard, ScrollView} from 'react-native'; @@ -9,6 +9,7 @@ import FormSubmit from '@components/FormSubmit'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import type ONYXKEYS from '@src/ONYXKEYS'; @@ -16,7 +17,7 @@ import type {Form} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {FormProps, InputRefs} from './types'; +import type {FormProps} from './types'; type FormWrapperOnyxProps = { /** Contains the form state that must be accessed outside the component */ @@ -33,7 +34,7 @@ type FormWrapperProps = ChildrenProps & errors: Errors; /** Assuming refs are React refs */ - inputRefs: MutableRefObject; + inputRefs: RefObject>>; }; function FormWrapper({ @@ -96,7 +97,7 @@ function FormWrapper({ // We measure relative to the content root, not the scroll view, as that gives // consistent results across mobile and web // eslint-disable-next-line @typescript-eslint/naming-convention - focusInput?.measureLayout?.(formContentRef.current, (_x, y) => + focusInput?.measureLayout?.(formContentRef.current, (_x: number, y: number) => formRef.current?.scrollTo({ y: y - 10, animated: false, diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index dd8014d28564..3ce37b95ee23 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,9 +1,11 @@ +import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import FormContext from './FormContext'; -import type {InputProps, InputRef, InputWrapperProps, ValidInput} from './types'; +import type {InputWrapperProps, ValidInput} from './types'; -function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: InputRef) { +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 6c1fcdf0c524..e4e4bd460e11 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -51,9 +51,6 @@ type FormProps = { footerContent?: ReactNode; }; -type InputRef = ForwardedRef; -type InputRefs = Record; - type RegisterInput = (inputID: string, props: InputProps) => InputProps; -export type {InputWrapperProps, ValidInput, FormProps, InputRef, InputRefs, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; +export type {InputWrapperProps, ValidInput, FormProps, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index f7917a852704..526a5891df16 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -1,17 +1,17 @@ -import type {Component, ForwardedRef} from 'react'; +import type {ForwardedRef} from 'react'; import React from 'react'; // eslint-disable-next-line no-restricted-imports import type {TextInputProps} from 'react-native'; import {TextInput} from 'react-native'; -import type {AnimatedProps} from 'react-native-reanimated'; import Animated from 'react-native-reanimated'; import useTheme from '@hooks/useTheme'; -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); -function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef>>) { +type AnimatedTextInputRef = typeof AnimatedTextInput & TextInput; + +function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef) { const theme = useTheme(); return ( @@ -23,7 +23,7 @@ function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef, ) { const inputProps = {shouldSaveDraft: false, shouldUseDefaultValue: false, ...props}; const theme = useTheme(); diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index d548041b0cf8..7269e1c5f872 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,4 +1,5 @@ import Str from 'expensify-common/lib/str'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, ViewStyle} from 'react-native'; @@ -57,7 +58,7 @@ function BaseTextInput( inputID, ...inputProps }: BaseTextInputProps, - ref: BaseTextInputRef, + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index f8376219d80f..972c8a6463e1 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,6 +1,5 @@ -import type {Component, ForwardedRef} from 'react'; import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; -import type {AnimatedProps} from 'react-native-reanimated'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import type {MaybePhraseKey} from '@libs/Localize'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -108,7 +107,7 @@ type CustomBaseTextInputProps = { autoCompleteType?: string; }; -type BaseTextInputRef = ForwardedRef>>; +type BaseTextInputRef = HTMLFormElement | AnimatedTextInputRef; type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index 656f0657dd26..acc40295d575 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -1,10 +1,11 @@ +import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect} from 'react'; import {AppState, Keyboard} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import BaseTextInput from './BaseTextInput'; import type {BaseTextInputProps, BaseTextInputRef} from './BaseTextInput/types'; -function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { +function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { const styles = useThemeStyles(); useEffect(() => { diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 3043edbd26a5..75c4d52e0f86 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,3 +1,4 @@ +import type {ForwardedRef} from 'react'; import React, {useEffect, useRef} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,9 +11,9 @@ import * as styleConst from './styleConst'; type RemoveVisibilityListener = () => void; -function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { +function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { const styles = useThemeStyles(); - const textInputRef = useRef(null); + const textInputRef = useRef(null); const removeVisibilityListenerRef = useRef(null); useEffect(() => { @@ -57,7 +58,7 @@ function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={(element) => { - textInputRef.current = element as HTMLElement; + textInputRef.current = element as HTMLFormElement; if (!ref) { return; diff --git a/src/pages/settings/Profile/DisplayNamePage.js b/src/pages/settings/Profile/DisplayNamePage.tsx similarity index 67% rename from src/pages/settings/Profile/DisplayNamePage.js rename to src/pages/settings/Profile/DisplayNamePage.tsx index 8ea471283004..22c1c173e637 100644 --- a/src/pages/settings/Profile/DisplayNamePage.js +++ b/src/pages/settings/Profile/DisplayNamePage.tsx @@ -1,5 +1,4 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +/* eslint-disable @typescript-eslint/no-explicit-any */ import React from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -10,8 +9,8 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -21,46 +20,32 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import { OnyxFormValuesFields } from '@components/Form/types'; -const propTypes = { - ...withLocalizePropTypes, - ...withCurrentUserPersonalDetailsPropTypes, - isLoadingApp: PropTypes.bool, -}; - -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, - isLoadingApp: true, -}; - -/** - * Submit form to update user's first and last name (and display name) - * @param {Object} values - * @param {String} values.firstName - * @param {String} values.lastName - */ -const updateDisplayName = (values) => { +const updateDisplayName = (values: any) => { PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim()); }; -function DisplayNamePage(props) { +function DisplayNamePage(props: any) { const styles = useThemeStyles(); + const {translate} = useLocalize(); const currentUserDetails = props.currentUserPersonalDetails || {}; /** - * @param {Object} values - * @param {String} values.firstName - * @param {String} values.lastName - * @returns {Object} - An object containing the errors for each inputID + * @param values + * @param values.firstName + * @param values.lastName + * @returns - An object containing the errors for each inputID */ - const validate = (values) => { + const validate = (values: OnyxFormValuesFields) => { + console.log(`values = `, values); const errors = {}; // First we validate the first name field if (!ValidationUtils.isValidDisplayName(values.firstName)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.hasInvalidCharacter'); } - if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES)) { + if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES as string[])) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.containsReservedWord'); } @@ -78,7 +63,7 @@ function DisplayNamePage(props) { testID={DisplayNamePage.displayName} > Navigation.goBack(ROUTES.SETTINGS_PROFILE)} /> {props.isLoadingApp ? ( @@ -89,21 +74,21 @@ function DisplayNamePage(props) { formID={ONYXKEYS.FORMS.DISPLAY_NAME_FORM} validate={validate} onSubmit={updateDisplayName} - submitButtonText={props.translate('common.save')} + submitButtonText={translate('common.save')} enabledWhenOffline shouldValidateOnBlur shouldValidateOnChange > - {props.translate('displayNamePage.isShownOnProfile')} + {translate('displayNamePage.isShownOnProfile')} @@ -113,10 +98,10 @@ function DisplayNamePage(props) { InputComponent={TextInput} inputID="lastName" name="lname" - label={props.translate('common.lastName')} - aria-label={props.translate('common.lastName')} + label={translate('common.lastName')} + aria-label={translate('common.lastName')} role={CONST.ROLE.PRESENTATION} - defaultValue={lodashGet(currentUserDetails, 'lastName', '')} + defaultValue={currentUserDetails?.lastName ?? ''} maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} spellCheck={false} /> @@ -127,12 +112,9 @@ function DisplayNamePage(props) { ); } -DisplayNamePage.propTypes = propTypes; -DisplayNamePage.defaultProps = defaultProps; DisplayNamePage.displayName = 'DisplayNamePage'; export default compose( - withLocalize, withCurrentUserPersonalDetails, withOnyx({ isLoadingApp: { From 77ef44201944c63e0e608ec9c0d3d491f8e7cfb0 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Fri, 5 Jan 2024 19:12:56 +0100 Subject: [PATCH 023/111] Register input types tweaks --- src/ONYXKEYS.ts | 2 +- src/components/Form/FormProvider.tsx | 15 +++++++-------- src/components/Form/FormWrapper.tsx | 7 +++---- src/components/Form/InputWrapper.tsx | 1 + src/components/Form/types.ts | 16 +++++++++++++--- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index c29a2a74a37a..8aeaf4c22f26 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -531,4 +531,4 @@ type OnyxValues = { type OnyxKeyValue = OnyxEntry; export default ONYXKEYS; -export type {OnyxKey, FormTest, OnyxCollectionKey, OnyxValues, OnyxKeyValue, OnyxFormKey, OnyxKeysMap}; +export type {OnyxKey, OnyxCollectionKey, OnyxValues, OnyxKeyValue, OnyxFormKey, OnyxKeysMap}; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index c4db7fcec290..6581cef8ac95 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -1,6 +1,6 @@ import lodashIsEqual from 'lodash/isEqual'; +import type {ForwardedRef, MutableRefObject, ReactNode} from 'react'; import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {ForwardedRef, ReactNode} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import * as ValidationUtils from '@libs/ValidationUtils'; @@ -84,7 +84,7 @@ function FormProvider( }: FormProviderProps, forwardedRef: ForwardedRef, ) { - const inputRefs = useRef({}); + const inputRefs = useRef({} as InputRefs); const touchedInputs = useRef>({}); const [inputValues, setInputValues] = useState(() => ({...draftValues})); const [errors, setErrors] = useState({}); @@ -206,11 +206,10 @@ function FormProvider( const registerInput: RegisterInput = useCallback( (inputID, inputProps) => { - const newRef: InputRef = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); + const newRef: MutableRefObject = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); if (inputRefs.current[inputID] !== newRef) { inputRefs.current[inputID] = newRef; } - if (inputProps.value !== undefined) { inputValues[inputID] = inputProps.value; } else if (inputProps.shouldSaveDraft && draftValues?.[inputID] !== undefined && inputValues[inputID] === undefined) { @@ -220,7 +219,7 @@ function FormProvider( inputValues[inputID] = inputProps.defaultValue; } else if (inputValues[inputID] === undefined) { // We want to initialize the input value if it's undefined - inputValues[inputID] = inputProps.defaultValue === undefined ? getInitialValueByType(inputProps.valueType) : inputProps.defaultValue; + inputValues[inputID] = inputProps.defaultValue ?? getInitialValueByType(inputProps.valueType); } const errorFields = formState?.errorFields?.[inputID] ?? {}; @@ -237,7 +236,7 @@ function FormProvider( typeof inputRef === 'function' ? (node) => { inputRef(node); - if (typeof newRef !== 'function') { + if (node && typeof newRef !== 'function') { newRef.current = node; } } @@ -279,7 +278,7 @@ function FormProvider( onBlur: (event) => { // Only run validation when user proactively blurs the input. if (Visibility.isVisible() && Visibility.hasFocus()) { - const relatedTarget = 'nativeEvent' in event ? event?.nativeEvent?.relatedTarget : undefined; + const relatedTarget = 'nativeEvent' in event && 'relatedTarget' in event.nativeEvent && event?.nativeEvent?.relatedTarget; const relatedTargetId = relatedTarget && 'id' in relatedTarget && typeof relatedTarget.id === 'string' && relatedTarget.id; // We delay the validation in order to prevent Checkbox loss of focus when // the user is focusing a TextInput and proceeds to toggle a CheckBox in @@ -301,7 +300,7 @@ function FormProvider( } inputProps.onBlur?.(event); }, - onInputChange: (value, key) => { + onInputChange: (value: unknown, key?: string) => { const inputKey = key ?? inputID; setInputValues((prevState) => { const newState = { diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index b410a09ec6fa..f1d32486de5e 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,5 +1,5 @@ -import type {RefObject} from 'react'; import React, {useCallback, useMemo, useRef} from 'react'; +import type {RefObject} from 'react'; import type {StyleProp, View, ViewStyle} from 'react-native'; import {Keyboard, ScrollView} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -9,7 +9,6 @@ import FormSubmit from '@components/FormSubmit'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; -import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import type ONYXKEYS from '@src/ONYXKEYS'; @@ -17,7 +16,7 @@ import type {Form} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {FormProps} from './types'; +import type {FormProps, InputRefs} from './types'; type FormWrapperOnyxProps = { /** Contains the form state that must be accessed outside the component */ @@ -34,7 +33,7 @@ type FormWrapperProps = ChildrenProps & errors: Errors; /** Assuming refs are React refs */ - inputRefs: RefObject>>; + inputRefs: RefObject; }; function FormWrapper({ diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 3ce37b95ee23..a12b181c07bd 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -14,6 +14,7 @@ function InputWrapper({InputComponent, inputID, value // For now this side effect happened only in `TextInput` components. const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; + // TODO: Sometimes we return too many props with register input, so we need to consider if it's better to make the returned type more general and disregard the issue, or we would like to omit the unused props somehow. // eslint-disable-next-line react/jsx-props-no-spreading return ; } diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index e4e4bd460e11..d6a9463f188f 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,6 +1,7 @@ -import type {ForwardedRef, ReactNode} from 'react'; +import type {FocusEvent, MutableRefObject, ReactNode} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import type TextInput from '@components/TextInput'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type {Form} from '@src/types/onyx'; @@ -8,7 +9,13 @@ type ValueType = 'string' | 'boolean' | 'date'; type ValidInput = typeof TextInput; -type InputProps = Parameters[0]; +type InputProps = Parameters[0] & { + shouldSetTouchedOnBlurOnly?: boolean; + onValueChange?: (value: unknown, key: string) => void; + onTouched?: (event: unknown) => void; + valueType?: ValueType; + onBlur: (event: FocusEvent | Parameters[0]['onBlur']>>[0]) => void; +}; type InputWrapperProps = InputProps & { InputComponent: TInput; @@ -53,4 +60,7 @@ type FormProps = { type RegisterInput = (inputID: string, props: InputProps) => InputProps; -export type {InputWrapperProps, ValidInput, FormProps, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; +type InputRef = BaseTextInputRef; +type InputRefs = Record>; + +export type {InputWrapperProps, ValidInput, FormProps, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft}; From 6c1f6e7f2f66f24dd83dc7b53ece52bcd969a294 Mon Sep 17 00:00:00 2001 From: Aldo Canepa Date: Mon, 8 Jan 2024 10:54:26 -0300 Subject: [PATCH 024/111] Use new command for updating waypoints (WIP) --- src/libs/actions/IOU.js | 18 +++++++++++++++++- src/pages/EditRequestDistancePage.js | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 21997023fbc8..9c00b5769a46 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1055,7 +1055,7 @@ function updateMoneyRequestDate(transactionID, transactionThreadReportID, val) { } /** - * Updates the created date of a money request + * Updates the tag of a money request * * @param {String} transactionID * @param {Number} transactionThreadReportID @@ -1069,6 +1069,21 @@ function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag) { API.write('UpdateMoneyRequestTag', params, onyxData); } +/** + * Updates the waypoints of a distance money request + * + * @param {String} transactionID + * @param {Number} transactionThreadReportID + * @param {Object} waypoints + */ +function updateMoneyRequestDistance(transactionID, transactionThreadReportID, waypoints) { + const transactionChanges = { + waypoints, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + API.write('UpdateMoneyRequestDistance', params, onyxData); +} + /** * Edits an existing distance request * @@ -3509,6 +3524,7 @@ export { navigateToNextPage, updateMoneyRequestDate, updateMoneyRequestTag, + updateMoneyRequestDistance, updateMoneyRequestAmountAndCurrency, replaceReceipt, detachReceipt, diff --git a/src/pages/EditRequestDistancePage.js b/src/pages/EditRequestDistancePage.js index 0ea295c0780b..f3ea76a3390a 100644 --- a/src/pages/EditRequestDistancePage.js +++ b/src/pages/EditRequestDistancePage.js @@ -79,7 +79,7 @@ function EditRequestDistancePage({report, route, transaction, transactionBackup} return; } - IOU.editMoneyRequest(transaction, report.reportID, {waypoints}); + IOU.updateMoneyRequestDistance(transaction.transactionID, report.reportID, waypoints); // If the client is offline, then the modal can be closed as well (because there are no errors or other feedback to show them // until they come online again and sync with the server). From 80f3867a83c71fd89e4b61160a6a7233bfd54291 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 10 Jan 2024 11:59:16 +0700 Subject: [PATCH 025/111] fix: Migrate ImageView to ts --- src/components/ImageView/index.native.js | 52 ------------ src/components/ImageView/index.native.tsx | 39 +++++++++ .../ImageView/{index.js => index.tsx} | 83 +++++++------------ src/components/ImageView/propTypes.js | 46 ---------- src/components/ImageView/types.ts | 50 +++++++++++ src/components/Lightbox.js | 2 +- src/components/MultiGestureCanvas/types.ts | 6 ++ 7 files changed, 124 insertions(+), 154 deletions(-) delete mode 100644 src/components/ImageView/index.native.js create mode 100644 src/components/ImageView/index.native.tsx rename src/components/ImageView/{index.js => index.tsx} (80%) delete mode 100644 src/components/ImageView/propTypes.js create mode 100644 src/components/ImageView/types.ts create mode 100644 src/components/MultiGestureCanvas/types.ts diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js deleted file mode 100644 index 98349b213aa5..000000000000 --- a/src/components/ImageView/index.native.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Lightbox from '@components/Lightbox'; -import {zoomRangeDefaultProps, zoomRangePropTypes} from '@components/MultiGestureCanvas/propTypes'; -import {imageViewDefaultProps, imageViewPropTypes} from './propTypes'; - -/** - * On the native layer, we use a image library to handle zoom functionality - */ -const propTypes = { - ...imageViewPropTypes, - ...zoomRangePropTypes, - - /** Function for handle on press */ - onPress: PropTypes.func, - - /** Additional styles to add to the component */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), -}; - -const defaultProps = { - ...imageViewDefaultProps, - ...zoomRangeDefaultProps, - - onPress: () => {}, - style: {}, -}; - -function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style, zoomRange, onError, isUsedInCarousel, isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex}) { - const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; - - return ( - - ); -} - -ImageView.propTypes = propTypes; -ImageView.defaultProps = defaultProps; -ImageView.displayName = 'ImageView'; - -export default ImageView; diff --git a/src/components/ImageView/index.native.tsx b/src/components/ImageView/index.native.tsx new file mode 100644 index 000000000000..74db0869f9e1 --- /dev/null +++ b/src/components/ImageView/index.native.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Lightbox from '@components/Lightbox'; +import {zoomRangeDefaultProps} from '@components/MultiGestureCanvas/propTypes'; +import type * as ImageViewTypes from './types'; + +function ImageView({ + isAuthTokenRequired, + url, + onScaleChanged, + onPress = () => {}, + style = {}, + zoomRange = zoomRangeDefaultProps.zoomRange, + onError, + isUsedInCarousel, + isSingleCarouselItem, + carouselItemIndex, + carouselActiveItemIndex, +}: ImageViewTypes.ImageViewProps) { + const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; + + return ( + + ); +} + +ImageView.displayName = 'ImageView'; + +export default ImageView; diff --git a/src/components/ImageView/index.js b/src/components/ImageView/index.tsx similarity index 80% rename from src/components/ImageView/index.js rename to src/components/ImageView/index.tsx index f16b37f328f5..b0bc2faed9fc 100644 --- a/src/components/ImageView/index.js +++ b/src/components/ImageView/index.tsx @@ -1,15 +1,18 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {View} from 'react-native'; +import type {LayoutChangeEvent,GestureResponderEvent} from 'react-native'; +import { View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; +import RESIZE_MODES from '@components/Image/resizeModes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; -import {imageViewDefaultProps, imageViewPropTypes} from './propTypes'; +import viewRef from '@src/types/utils/viewRef'; +import type * as ImageViewTypes from './types'; -function ImageView({isAuthTokenRequired, url, fileName, onError}) { +function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => {}}: ImageViewTypes.ImageViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); @@ -27,16 +30,10 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { const [zoomScale, setZoomScale] = useState(0); const [zoomDelta, setZoomDelta] = useState({offsetX: 0, offsetY: 0}); - const scrollableRef = useRef(null); + const scrollableRef = useRef(null); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); - /** - * @param {Number} newContainerWidth - * @param {Number} newContainerHeight - * @param {Number} newImageWidth - * @param {Number} newImageHeight - */ - const setScale = (newContainerWidth, newContainerHeight, newImageWidth, newImageHeight) => { + const setScale = (newContainerWidth: number, newContainerHeight: number, newImageWidth: number, newImageHeight: number) => { if (!newContainerWidth || !newImageWidth || !newContainerHeight || !newImageHeight) { return; } @@ -44,10 +41,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { setZoomScale(newZoomScale); }; - /** - * @param {SyntheticEvent} e - */ - const onContainerLayoutChanged = (e) => { + const onContainerLayoutChanged = (e: LayoutChangeEvent) => { const {width, height} = e.nativeEvent.layout; setScale(width, height, imgWidth, imgHeight); @@ -55,12 +49,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { setContainerWidth(width); }; - /** - * When open image, set image width, height. - * @param {Number} imageWidth - * @param {Number} imageHeight - */ - const setImageRegion = (imageWidth, imageHeight) => { + const setImageRegion = (imageWidth: number, imageHeight: number) => { if (imageHeight <= 0) { return; } @@ -78,32 +67,23 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { setIsZoomed(false); }; - const imageLoad = ({nativeEvent}) => { + const imageLoad = ({nativeEvent}: {nativeEvent: ImageViewTypes.ImageLoadNativeEventData}) => { setImageRegion(nativeEvent.width, nativeEvent.height); setIsLoading(false); }; - /** - * @param {SyntheticEvent} e - */ - const onContainerPressIn = (e) => { + const onContainerPressIn = (e: GestureResponderEvent) => { const {pageX, pageY} = e.nativeEvent; setIsMouseDown(true); setInitialX(pageX); setInitialY(pageY); - setInitialScrollLeft(scrollableRef.current.scrollLeft); - setInitialScrollTop(scrollableRef.current.scrollTop); + setInitialScrollLeft(scrollableRef.current?.scrollLeft ?? 0); + setInitialScrollTop(scrollableRef.current?.scrollTop ?? 0); }; - /** - * Convert touch point to zoomed point - * @param {Boolean} x x point when click zoom - * @param {Boolean} y y point when click zoom - * @returns {Object} converted touch point - */ - const getScrollOffset = (x, y) => { - let offsetX; - let offsetY; + const getScrollOffset = (x: number, y: number) => { + let offsetX = 0; + let offsetY = 0; // Container size bigger than clicked position offset if (x <= containerWidth / 2) { @@ -121,10 +101,8 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { return {offsetX, offsetY}; }; - /** - * @param {SyntheticEvent} e - */ - const onContainerPress = (e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const onContainerPress = (e: any) => { if (!isZoomed && !isDragging) { if (e.nativeEvent) { const {offsetX, offsetY} = e.nativeEvent; @@ -148,13 +126,10 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { } }; - /** - * @param {SyntheticEvent} e - */ const trackPointerPosition = useCallback( - (e) => { + (e: MouseEvent) => { // Whether the pointer is released inside the ImageView - const isInsideImageView = scrollableRef.current.contains(e.nativeEvent.target); + const isInsideImageView = scrollableRef.current?.contains(e.target as Node); if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { setIsDragging(false); @@ -165,14 +140,14 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { ); const trackMovement = useCallback( - (e) => { + (e: MouseEvent) => { if (!isZoomed) { return; } - if (isDragging && isMouseDown) { - const x = e.nativeEvent.x; - const y = e.nativeEvent.y; + if (isDragging && isMouseDown && scrollableRef.current) { + const x = e.x; + const y = e.y; const moveX = initialX - x; const moveY = initialY - y; scrollableRef.current.scrollLeft = initialScrollLeft + moveX; @@ -218,7 +193,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { style={isLoading || zoomScale === 0 ? undefined : [styles.w100, styles.h100]} // When Image dimensions are lower than the container boundary(zoomscale <= 1), use `contain` to render the image with natural dimensions. // Both `center` and `contain` keeps the image centered on both x and y axis. - resizeMode={zoomScale > 1 ? Image.resizeMode.center : Image.resizeMode.contain} + resizeMode={zoomScale > 1 ? RESIZE_MODES.center : RESIZE_MODES.contain} onLoadStart={imageLoadingStart} onLoad={imageLoad} onError={onError} @@ -229,7 +204,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { } return ( @@ -249,7 +224,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { source={{uri: url}} isAuthTokenRequired={isAuthTokenRequired} style={[styles.h100, styles.w100]} - resizeMode={Image.resizeMode.contain} + resizeMode={RESIZE_MODES.contain} onLoadStart={imageLoadingStart} onLoad={imageLoad} onError={onError} @@ -261,8 +236,6 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { ); } -ImageView.propTypes = imageViewPropTypes; -ImageView.defaultProps = imageViewDefaultProps; ImageView.displayName = 'ImageView'; export default ImageView; diff --git a/src/components/ImageView/propTypes.js b/src/components/ImageView/propTypes.js deleted file mode 100644 index 3809d9aed043..000000000000 --- a/src/components/ImageView/propTypes.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; - -const imageViewPropTypes = { - /** Whether source url requires authentication */ - isAuthTokenRequired: PropTypes.bool, - - /** Handles scale changed event in image zoom component. Used on native only */ - // eslint-disable-next-line react/no-unused-prop-types - onScaleChanged: PropTypes.func.isRequired, - - /** URL to full-sized image */ - url: PropTypes.string.isRequired, - - /** image file name */ - fileName: PropTypes.string.isRequired, - - /** Handles errors while displaying the image */ - onError: PropTypes.func, - - /** Whether this view is the active screen */ - isFocused: PropTypes.bool, - - /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ - isUsedInCarousel: PropTypes.bool, - - /** When "isUsedInCarousel" is set to true, determines whether there is only one item in the carousel */ - isSingleCarouselItem: PropTypes.bool, - - /** The index of the carousel item */ - carouselItemIndex: PropTypes.number, - - /** The index of the currently active carousel item */ - carouselActiveItemIndex: PropTypes.number, -}; - -const imageViewDefaultProps = { - isAuthTokenRequired: false, - onError: () => {}, - isFocused: true, - isUsedInCarousel: false, - isSingleCarouselItem: false, - carouselItemIndex: 0, - carouselActiveItemIndex: 0, -}; - -export {imageViewPropTypes, imageViewDefaultProps}; diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts new file mode 100644 index 000000000000..9bb3584955d4 --- /dev/null +++ b/src/components/ImageView/types.ts @@ -0,0 +1,50 @@ +import type ZoomRange from '@components/MultiGestureCanvas/types'; +import type {StyleProp, ViewStyle} from 'react-native'; + +type ImageViewProps = { + /** Whether source url requires authentication */ + isAuthTokenRequired?: boolean; + + /** Handles scale changed event in image zoom component. Used on native only */ + // eslint-disable-next-line react/no-unused-prop-types + onScaleChanged: (scale: number) => void; + + /** URL to full-sized image */ + url: string; + + /** image file name */ + fileName: string; + + /** Handles errors while displaying the image */ + onError?: () => void; + + /** Whether this view is the active screen */ + isFocused?: boolean; + + /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ + isUsedInCarousel?: boolean; + + /** When "isUsedInCarousel" is set to true, determines whether there is only one item in the carousel */ + isSingleCarouselItem?: boolean; + + /** The index of the carousel item */ + carouselItemIndex?: number; + + /** The index of the currently active carousel item */ + carouselActiveItemIndex?: number; + + /** Function for handle on press */ + onPress?: () => void; + + /** Additional styles to add to the component */ + style?: StyleProp; + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange?: ZoomRange; +}; + +type ImageLoadNativeEventData = { + width: number; + height: number; +}; +export type {ImageViewProps, ImageLoadNativeEventData}; diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 45326edb4610..a941d23a5f16 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -44,7 +44,7 @@ const propTypes = { activeIndex: PropTypes.number, /** Additional styles to add to the component */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style: PropTypes.oneOfType([PropTypes.bool, PropTypes.arrayOf(PropTypes.object), PropTypes.object]), }; const defaultProps = { diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts new file mode 100644 index 000000000000..0242f045feef --- /dev/null +++ b/src/components/MultiGestureCanvas/types.ts @@ -0,0 +1,6 @@ +type ZoomRange = { + min: number; + max: number; +}; + +export default ZoomRange; From 5ffd8e7ad8fcd470a5820af21cae165552828af5 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 10 Jan 2024 12:27:15 +0700 Subject: [PATCH 026/111] lint fix --- src/components/ImageView/index.tsx | 4 ++-- src/components/ImageView/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index b0bc2faed9fc..f0d43121f9ef 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; -import type {LayoutChangeEvent,GestureResponderEvent} from 'react-native'; -import { View} from 'react-native'; +import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; +import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import RESIZE_MODES from '@components/Image/resizeModes'; diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index 9bb3584955d4..80fabed0ee58 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -1,5 +1,5 @@ -import type ZoomRange from '@components/MultiGestureCanvas/types'; import type {StyleProp, ViewStyle} from 'react-native'; +import type ZoomRange from '@components/MultiGestureCanvas/types'; type ImageViewProps = { /** Whether source url requires authentication */ From 32d5cc9e822bd78606c1372b126183baedecf5c6 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 10 Jan 2024 15:34:32 +0100 Subject: [PATCH 027/111] Modify Onyx typings --- src/components/Form/FormProvider.tsx | 19 +++++++++---------- src/components/Form/FormWrapper.tsx | 5 +++-- src/types/onyx/Form.ts | 2 ++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 6581cef8ac95..7177fb88a7db 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -36,14 +36,12 @@ function getInitialValueByType(valueType?: ValueType): InitialDefaultValue { } } -type GenericFormValues = Form & Record; - type FormProviderOnyxProps = { /** Contains the form state that must be accessed outside the component */ - formState: OnyxEntry; + formState: OnyxEntry; /** Contains draft values for each input in the form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; /** Information about the network */ network: OnyxEntry; @@ -86,7 +84,7 @@ function FormProvider( ) { const inputRefs = useRef({} as InputRefs); const touchedInputs = useRef>({}); - const [inputValues, setInputValues] = useState(() => ({...draftValues})); + const [inputValues, setInputValues] = useState(() => ({...draftValues})); const [errors, setErrors] = useState({}); const hasServerError = useMemo(() => !!formState && !isEmptyObject(formState?.errors), [formState]); @@ -185,7 +183,7 @@ function FormProvider( }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); const resetForm = useCallback( - (optionalValue: GenericFormValues) => { + (optionalValue: Form) => { Object.keys(inputValues).forEach((inputID) => { setInputValues((prevState) => { const copyPrevState = {...prevState}; @@ -348,12 +346,13 @@ export default withOnyx({ network: { key: ONYXKEYS.NETWORK, }, + // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we had to cast the keys to any formState: { - // @ts-expect-error TODO: fix this - key: ({formID}) => formID, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any + key: ({formID}) => formID as any, }, draftValues: { - // @ts-expect-error TODO: fix this - key: (props) => `${props.formID}Draft` as const, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any + key: (props) => `${props.formID}Draft` as any, }, })(forwardRef(FormProvider)) as (props: Omit, keyof FormProviderOnyxProps>) => ReactNode; diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index f1d32486de5e..151600c9c12a 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -168,7 +168,8 @@ FormWrapper.displayName = 'FormWrapper'; export default withOnyx({ formState: { - // FIX: Fabio plz help 😂 - key: (props) => props.formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, + // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we had to cast the keys to any + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any + key: (props) => props.formID as any, }, })(FormWrapper); diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 666898450a93..a6d276e50b9f 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -1,6 +1,8 @@ import type * as OnyxCommon from './OnyxCommon'; type Form = { + [key: string]: unknown; + /** Controls the loading state of the form */ isLoading?: boolean; From 029493228a4b120d40646c922f2cbf61008bcff2 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 10 Jan 2024 16:05:16 +0100 Subject: [PATCH 028/111] Code review changes --- src/components/Form/FormProvider.tsx | 4 ++-- src/components/Form/FormWrapper.tsx | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 7177fb88a7db..26e045c6a0b9 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -82,7 +82,7 @@ function FormProvider( }: FormProviderProps, forwardedRef: ForwardedRef, ) { - const inputRefs = useRef({} as InputRefs); + const inputRefs = useRef({}); const touchedInputs = useRef>({}); const [inputValues, setInputValues] = useState(() => ({...draftValues})); const [errors, setErrors] = useState({}); @@ -90,7 +90,7 @@ function FormProvider( const onValidate = useCallback( (values: OnyxFormValuesFields, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values) as OnyxFormValuesFields; + const trimmedStringValues = ValidationUtils.prepareValues(values); if (shouldClearServerError) { FormActions.setErrors(formID, null); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 151600c9c12a..a513b8fa0845 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -11,7 +11,6 @@ import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import type ONYXKEYS from '@src/ONYXKEYS'; import type {Form} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -71,7 +70,6 @@ function FormWrapper({ buttonText={submitButtonText} isAlertVisible={!isEmptyObject(errors) || !!errorMessage || !isEmptyObject(formState?.errorFields)} isLoading={!!formState?.isLoading} - // eslint-disable-next-line no-extra-boolean-cast message={isEmptyObject(formState?.errorFields) ? errorMessage : undefined} onSubmit={onSubmit} footerContent={footerContent} @@ -95,8 +93,7 @@ function FormWrapper({ if (formContentRef.current) { // We measure relative to the content root, not the scroll view, as that gives // consistent results across mobile and web - // eslint-disable-next-line @typescript-eslint/naming-convention - focusInput?.measureLayout?.(formContentRef.current, (_x: number, y: number) => + focusInput?.measureLayout?.(formContentRef.current, (X: number, y: number) => formRef.current?.scrollTo({ y: y - 10, animated: false, From 81d347a375477be76512490e1f4eeec61205006f Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 10 Jan 2024 16:53:26 +0100 Subject: [PATCH 029/111] Code review changes --- src/ONYXKEYS.ts | 9 +++++---- src/libs/ValidationUtils.ts | 11 +++++------ src/types/onyx/Form.ts | 8 +++++++- src/types/onyx/ReimbursementAccount.ts | 5 ++++- src/types/onyx/ReimbursementAccountDraft.ts | 6 +++++- src/types/onyx/index.ts | 7 ++++++- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6f55e771de6a..13de58a2c21c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type * as OnyxTypes from './types/onyx'; +import {ReimbursementAccountForm, ReimbursementAccountFormDraft} from './types/onyx'; import type DeepValueOf from './types/utils/DeepValueOf'; /** @@ -408,8 +409,8 @@ type OnyxValues = { [ONYXKEYS.CARD_LIST]: Record; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount & OnyxTypes.Form; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT]: OnyxTypes.ReimbursementAccountDraft & OnyxTypes.Form; + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccountForm; + [ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT]: OnyxTypes.ReimbursementAccountFormDraft; [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: string | number; [ONYXKEYS.FREQUENTLY_USED_EMOJIS]: OnyxTypes.FrequentlyUsedEmoji[]; [ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID]: string; @@ -475,8 +476,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.Form & {firstName: string; lastName: string}; - [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.Form & {firstName: string; lastName: string}; + [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.DisplayNameForm; + [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.DisplayNameForm; [ONYXKEYS.FORMS.ROOM_NAME_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.ROOM_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM]: OnyxTypes.Form; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 9bbdf20a9003..7eff51c354df 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -392,17 +392,16 @@ function isValidAccountRoute(accountID: number): boolean { return CONST.REGEX.NUMBER.test(String(accountID)) && accountID > 0; } +type DateTimeValidationErrorKeys = { + dateValidationErrorKey: string; + timeValidationErrorKey: string; +}; /** * Validates that the date and time are at least one minute in the future. * data - A date and time string in 'YYYY-MM-DD HH:mm:ss.sssZ' format * returns an object containing the error messages for the date and time */ -const validateDateTimeIsAtLeastOneMinuteInFuture = ( - data: string, -): { - dateValidationErrorKey: string; - timeValidationErrorKey: string; -} => { +const validateDateTimeIsAtLeastOneMinuteInFuture = (data: string): DateTimeValidationErrorKeys => { if (!data) { return { dateValidationErrorKey: '', diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index a6d276e50b9f..9306ab5736fc 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -1,3 +1,4 @@ +import type * as OnyxTypes from './index'; import type * as OnyxCommon from './OnyxCommon'; type Form = { @@ -23,6 +24,11 @@ type DateOfBirthForm = Form & { dob?: string; }; +type DisplayNameForm = OnyxTypes.Form & { + firstName: string; + lastName: string; +}; + export default Form; -export type {AddDebitCardForm, DateOfBirthForm}; +export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm}; diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts index c0ade25e4d79..4779b790eac0 100644 --- a/src/types/onyx/ReimbursementAccount.ts +++ b/src/types/onyx/ReimbursementAccount.ts @@ -1,5 +1,6 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; +import type * as OnyxTypes from './index'; import type * as OnyxCommon from './OnyxCommon'; type BankAccountStep = ValueOf; @@ -48,5 +49,7 @@ type ReimbursementAccount = { pendingAction?: OnyxCommon.PendingAction; }; +type ReimbursementAccountForm = ReimbursementAccount & OnyxTypes.Form; + export default ReimbursementAccount; -export type {BankAccountStep, BankAccountSubStep}; +export type {BankAccountStep, BankAccountSubStep, ReimbursementAccountForm}; diff --git a/src/types/onyx/ReimbursementAccountDraft.ts b/src/types/onyx/ReimbursementAccountDraft.ts index cab1283943bc..5b3c604fdab6 100644 --- a/src/types/onyx/ReimbursementAccountDraft.ts +++ b/src/types/onyx/ReimbursementAccountDraft.ts @@ -1,3 +1,5 @@ +import type * as OnyxTypes from './index'; + type OnfidoData = Record; type BankAccountStepProps = { @@ -57,5 +59,7 @@ type ReimbursementAccountProps = { type ReimbursementAccountDraft = BankAccountStepProps & CompanyStepProps & RequestorStepProps & ACHContractStepProps & ReimbursementAccountProps; +type ReimbursementAccountFormDraft = ReimbursementAccountDraft & OnyxTypes.Form; + export default ReimbursementAccountDraft; -export type {ACHContractStepProps, RequestorStepProps, OnfidoData, BankAccountStepProps, CompanyStepProps, ReimbursementAccountProps}; +export type {ACHContractStepProps, RequestorStepProps, OnfidoData, BankAccountStepProps, CompanyStepProps, ReimbursementAccountProps, ReimbursementAccountFormDraft}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 7bd9c321be5e..efb578a03295 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -9,7 +9,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -38,7 +38,9 @@ import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; import type RecentlyUsedTags from './RecentlyUsedTags'; import type RecentWaypoint from './RecentWaypoint'; import type ReimbursementAccount from './ReimbursementAccount'; +import type {ReimbursementAccountForm} from './ReimbursementAccount'; import type ReimbursementAccountDraft from './ReimbursementAccountDraft'; +import type {ReimbursementAccountFormDraft} from './ReimbursementAccountDraft'; import type Report from './Report'; import type {ReportActions} from './ReportAction'; import type ReportAction from './ReportAction'; @@ -69,6 +71,7 @@ export type { Account, AccountData, AddDebitCardForm, + DisplayNameForm, BankAccount, BankAccountList, Beta, @@ -108,7 +111,9 @@ export type { RecentlyUsedCategories, RecentlyUsedTags, ReimbursementAccount, + ReimbursementAccountForm, ReimbursementAccountDraft, + ReimbursementAccountFormDraft, Report, ReportAction, ReportActionReactions, From 256b9ae9d06765d2ef882fc8440c37f3b7c2a3c0 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Fri, 12 Jan 2024 12:05:36 +0100 Subject: [PATCH 030/111] Update types --- src/components/Form/InputWrapper.tsx | 2 +- src/components/Form/types.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index a12b181c07bd..8e824875c6d4 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -16,7 +16,7 @@ function InputWrapper({InputComponent, inputID, value // TODO: Sometimes we return too many props with register input, so we need to consider if it's better to make the returned type more general and disregard the issue, or we would like to omit the unused props somehow. // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index d6a9463f188f..0a9069ea596a 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,20 +1,19 @@ -import type {FocusEvent, MutableRefObject, ReactNode} from 'react'; +import type {ComponentProps, ElementType, FocusEvent, MutableRefObject, ReactNode} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; -import type TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type {Form} from '@src/types/onyx'; type ValueType = 'string' | 'boolean' | 'date'; -type ValidInput = typeof TextInput; +type ValidInput = ElementType; -type InputProps = Parameters[0] & { +type InputProps = ComponentProps & { shouldSetTouchedOnBlurOnly?: boolean; onValueChange?: (value: unknown, key: string) => void; onTouched?: (event: unknown) => void; valueType?: ValueType; - onBlur: (event: FocusEvent | Parameters[0]['onBlur']>>[0]) => void; + onBlur: (event: FocusEvent | Parameters['onBlur']>>[0]) => void; }; type InputWrapperProps = InputProps & { From 15d87b177f2ec7c233a7ec036afbfab22cfae7cd Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Sat, 13 Jan 2024 03:28:57 +0700 Subject: [PATCH 031/111] dismiss money request banner --- .../iou/MoneyRequestReferralProgramCTA.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx b/src/pages/iou/MoneyRequestReferralProgramCTA.tsx index 31394e1bd0e1..a752696baca2 100644 --- a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx +++ b/src/pages/iou/MoneyRequestReferralProgramCTA.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, {useState} from 'react'; import Icon from '@components/Icon'; -import {Info} from '@components/Icon/Expensicons'; +import {Close} from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -18,6 +18,11 @@ function MoneyRequestReferralProgramCTA({referralContentType}: MoneyRequestRefer const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); + const [isHidden, setIsHidden] = useState(false); + + if (isHidden) { + return null; + } return ( - + setIsHidden(true)} + onMouseDown={(e) => { + e.preventDefault(); + }} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel={translate('common.close')} + > + + ); } From a6b46827254d9e06249d98207771a6ede85150b3 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jan 2024 17:31:34 +0700 Subject: [PATCH 032/111] type fix --- src/components/ImageView/index.native.tsx | 18 +++++++++--------- src/components/ImageView/index.tsx | 21 ++++++++++++++++----- src/components/ImageView/types.ts | 7 ++----- src/components/Lightbox.js | 3 ++- src/components/MultiGestureCanvas/types.ts | 3 ++- src/styles/stylePropTypes.js | 2 +- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/components/ImageView/index.native.tsx b/src/components/ImageView/index.native.tsx index 74db0869f9e1..e36bb39d2bed 100644 --- a/src/components/ImageView/index.native.tsx +++ b/src/components/ImageView/index.native.tsx @@ -1,21 +1,21 @@ import React from 'react'; import Lightbox from '@components/Lightbox'; import {zoomRangeDefaultProps} from '@components/MultiGestureCanvas/propTypes'; -import type * as ImageViewTypes from './types'; +import type {ImageViewProps} from './types'; function ImageView({ - isAuthTokenRequired, + isAuthTokenRequired = false, url, onScaleChanged, - onPress = () => {}, - style = {}, + onPress, + style, zoomRange = zoomRangeDefaultProps.zoomRange, onError, - isUsedInCarousel, - isSingleCarouselItem, - carouselItemIndex, - carouselActiveItemIndex, -}: ImageViewTypes.ImageViewProps) { + isUsedInCarousel = false, + isSingleCarouselItem = false, + carouselItemIndex = 0, + carouselActiveItemIndex = 0, +}: ImageViewProps) { const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; return ( diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index f0d43121f9ef..3731a4806cbc 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; -import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; +import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; @@ -10,9 +10,11 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import viewRef from '@src/types/utils/viewRef'; -import type * as ImageViewTypes from './types'; +import type {ImageLoadNativeEventData, ImageViewProps} from './types'; -function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => {}}: ImageViewTypes.ImageViewProps) { +type ZoomDelta = {offsetX: number; offsetY: number}; + +function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); @@ -28,7 +30,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => const [imgWidth, setImgWidth] = useState(0); const [imgHeight, setImgHeight] = useState(0); const [zoomScale, setZoomScale] = useState(0); - const [zoomDelta, setZoomDelta] = useState({offsetX: 0, offsetY: 0}); + const [zoomDelta, setZoomDelta] = useState(); const scrollableRef = useRef(null); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); @@ -49,6 +51,9 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => setContainerWidth(width); }; + /** + * When open image, set image width, height. + */ const setImageRegion = (imageWidth: number, imageHeight: number) => { if (imageHeight <= 0) { return; @@ -67,7 +72,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => setIsZoomed(false); }; - const imageLoad = ({nativeEvent}: {nativeEvent: ImageViewTypes.ImageLoadNativeEventData}) => { + const imageLoad = ({nativeEvent}: NativeSyntheticEvent) => { setImageRegion(nativeEvent.width, nativeEvent.height); setIsLoading(false); }; @@ -81,6 +86,12 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => setInitialScrollTop(scrollableRef.current?.scrollTop ?? 0); }; + /** + * Convert touch point to zoomed point + * @param x point when click zoom + * @param y point when click zoom + * @returns converted touch point + */ const getScrollOffset = (x: number, y: number) => { let offsetX = 0; let offsetY = 0; diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index 80fabed0ee58..9ea51fd3c82c 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -1,12 +1,11 @@ import type {StyleProp, ViewStyle} from 'react-native'; -import type ZoomRange from '@components/MultiGestureCanvas/types'; +import type {ZoomRange} from '@components/MultiGestureCanvas/types'; type ImageViewProps = { /** Whether source url requires authentication */ isAuthTokenRequired?: boolean; /** Handles scale changed event in image zoom component. Used on native only */ - // eslint-disable-next-line react/no-unused-prop-types onScaleChanged: (scale: number) => void; /** URL to full-sized image */ @@ -18,9 +17,6 @@ type ImageViewProps = { /** Handles errors while displaying the image */ onError?: () => void; - /** Whether this view is the active screen */ - isFocused?: boolean; - /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ isUsedInCarousel?: boolean; @@ -47,4 +43,5 @@ type ImageLoadNativeEventData = { width: number; height: number; }; + export type {ImageViewProps, ImageLoadNativeEventData}; diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index a941d23a5f16..8b7d68befafd 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; +import stylePropTypes from '@styles/stylePropTypes'; import * as AttachmentsPropTypes from './Attachments/propTypes'; import Image from './Image'; import MultiGestureCanvas from './MultiGestureCanvas'; @@ -44,7 +45,7 @@ const propTypes = { activeIndex: PropTypes.number, /** Additional styles to add to the component */ - style: PropTypes.oneOfType([PropTypes.bool, PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style: stylePropTypes, }; const defaultProps = { diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 0242f045feef..3c8480257700 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -3,4 +3,5 @@ type ZoomRange = { max: number; }; -export default ZoomRange; +// eslint-disable-next-line import/prefer-default-export +export type {ZoomRange}; diff --git a/src/styles/stylePropTypes.js b/src/styles/stylePropTypes.js index f9ecdb98ff13..b82db94140ee 100644 --- a/src/styles/stylePropTypes.js +++ b/src/styles/stylePropTypes.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -const stylePropTypes = PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object), PropTypes.func]); +const stylePropTypes = PropTypes.oneOfType([PropTypes.object, PropTypes.bool, PropTypes.arrayOf(PropTypes.object), PropTypes.func]); export default stylePropTypes; From f470ff9d2461632d5ba3800fe1c854f6b2f85768 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 15 Jan 2024 11:35:39 +0100 Subject: [PATCH 033/111] WIP --- src/ONYXKEYS.ts | 1 - src/pages/settings/Profile/DisplayNamePage.tsx | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 13de58a2c21c..2915b7a4aa12 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -2,7 +2,6 @@ import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type * as OnyxTypes from './types/onyx'; -import {ReimbursementAccountForm, ReimbursementAccountFormDraft} from './types/onyx'; import type DeepValueOf from './types/utils/DeepValueOf'; /** diff --git a/src/pages/settings/Profile/DisplayNamePage.tsx b/src/pages/settings/Profile/DisplayNamePage.tsx index 22c1c173e637..a481b9ccdbec 100644 --- a/src/pages/settings/Profile/DisplayNamePage.tsx +++ b/src/pages/settings/Profile/DisplayNamePage.tsx @@ -4,6 +4,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -20,7 +21,6 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import { OnyxFormValuesFields } from '@components/Form/types'; const updateDisplayName = (values: any) => { PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim()); @@ -38,7 +38,6 @@ function DisplayNamePage(props: any) { * @returns - An object containing the errors for each inputID */ const validate = (values: OnyxFormValuesFields) => { - console.log(`values = `, values); const errors = {}; // First we validate the first name field From 3f19479cb86d4204e9faf7d717b705a96a5b60a7 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jan 2024 17:41:57 +0700 Subject: [PATCH 034/111] fix: revert native event --- src/components/ImageView/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 3731a4806cbc..1c10c8116325 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -138,9 +138,10 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV }; const trackPointerPosition = useCallback( - (e: MouseEvent) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e: any) => { // Whether the pointer is released inside the ImageView - const isInsideImageView = scrollableRef.current?.contains(e.target as Node); + const isInsideImageView = scrollableRef.current?.contains(e.nativeEvent.target); if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { setIsDragging(false); From f64a93ad047613831d3e94a136df58d15066a25b Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Tue, 16 Jan 2024 14:08:23 +0700 Subject: [PATCH 035/111] reapply changes --- .../OptionsSelector/BaseOptionsSelector.js | 5 +++- src/components/ReferralProgramCTA.tsx | 28 +++++++++++++------ src/pages/SearchPage/SearchPageFooter.tsx | 8 ++++-- ...yForRefactorRequestParticipantsSelector.js | 12 ++++++-- .../MoneyRequestParticipantsSelector.js | 12 ++++++-- 5 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index f7fc8ca4b77d..bbcce6fff9a6 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -662,7 +662,10 @@ class BaseOptionsSelector extends Component { {this.props.shouldShowReferralCTA && this.state.shouldShowReferralModal && ( - + )} diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 473d5cdbed08..68b97c343f81 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -6,7 +6,7 @@ import CONST from '@src/CONST'; import Navigation from '@src/libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; import Icon from './Icon'; -import {Info} from './Icon/Expensicons'; +import {Close} from './Icon/Expensicons'; import {PressableWithoutFeedback} from './Pressable'; import Text from './Text'; @@ -16,9 +16,12 @@ type ReferralProgramCTAProps = { | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND; + + /** Method to trigger when pressing close button of the banner */ + onCloseButtonPress?: () => void; }; -function ReferralProgramCTA({referralContentType}: ReferralProgramCTAProps) { +function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}}: ReferralProgramCTAProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); @@ -41,12 +44,21 @@ function ReferralProgramCTA({referralContentType}: ReferralProgramCTAProps) { {translate(`referralProgram.${referralContentType}.buttonText2`)} - + { + e.preventDefault(); + }} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel={translate('common.close')} + > + + ); } diff --git a/src/pages/SearchPage/SearchPageFooter.tsx b/src/pages/SearchPage/SearchPageFooter.tsx index e0ef67ad9ec3..a66e24d973d9 100644 --- a/src/pages/SearchPage/SearchPageFooter.tsx +++ b/src/pages/SearchPage/SearchPageFooter.tsx @@ -1,15 +1,19 @@ -import React from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; function SearchPageFooter() { + const [shouldShowReferralCTA, setShouldShowReferralCTA] = useState(true); const themeStyles = useThemeStyles(); return ( - + setShouldShowReferralCTA(false)} + /> ); } diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index f8c412993bab..bd5af413635d 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -80,6 +80,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const {translate} = useLocalize(); const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); + const [shouldShowReferralCTA, setShouldShowReferralCTA] = useState(true); const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); @@ -265,9 +266,14 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const footerContent = useMemo( () => ( - - - + {shouldShowReferralCTA && ( + + setShouldShowReferralCTA(false)} + /> + + )} {shouldShowSplitBillErrorMessage && ( ( - - - + {shouldShowReferralCTA && ( + + setShouldShowReferralCTA(false)} + /> + + )} {shouldShowSplitBillErrorMessage && ( Date: Tue, 16 Jan 2024 14:32:38 +0700 Subject: [PATCH 036/111] increase pressable space --- src/components/ReferralProgramCTA.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 68b97c343f81..4a6b8b03f2b4 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -31,7 +31,7 @@ function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}} onPress={() => { Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(referralContentType)); }} - style={[styles.p5, styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10}]} + style={[styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5]} accessibilityLabel="referral" role={CONST.ACCESSIBILITY_ROLE.BUTTON} > @@ -49,6 +49,7 @@ function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}} onMouseDown={(e) => { e.preventDefault(); }} + style={[styles.touchableButtonImage]} role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('common.close')} > From bdc980df6055cd93f1925c6fd7581f96dad51776 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Tue, 16 Jan 2024 14:43:38 +0700 Subject: [PATCH 037/111] fix lint --- src/pages/SearchPage/SearchPageFooter.tsx | 16 ++++++++++------ ...raryForRefactorRequestParticipantsSelector.js | 2 +- .../MoneyRequestParticipantsSelector.js | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pages/SearchPage/SearchPageFooter.tsx b/src/pages/SearchPage/SearchPageFooter.tsx index a66e24d973d9..fb3644d8e570 100644 --- a/src/pages/SearchPage/SearchPageFooter.tsx +++ b/src/pages/SearchPage/SearchPageFooter.tsx @@ -9,12 +9,16 @@ function SearchPageFooter() { const themeStyles = useThemeStyles(); return ( - - setShouldShowReferralCTA(false)} - /> - + <> + {shouldShowReferralCTA && ( + + setShouldShowReferralCTA(false)} + /> + + )} + ); } diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index bd5af413635d..fa7f13002305 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -294,7 +294,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ )} ), - [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], + [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, shouldShowReferralCTA, styles, translate], ); const itemRightSideComponent = useCallback( diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index f5e332f8eace..59081599736c 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -313,7 +313,7 @@ function MoneyRequestParticipantsSelector({ )} ), - [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], + [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, shouldShowReferralCTA, styles, translate], ); const itemRightSideComponent = useCallback( From d9dc65336727b23811ea5906bacae2c40a726bab Mon Sep 17 00:00:00 2001 From: Pujan Date: Tue, 16 Jan 2024 18:04:56 +0530 Subject: [PATCH 038/111] private notes edit page ts changes --- ...esEditPage.js => PrivateNotesEditPage.tsx} | 80 ++++++++----------- src/types/onyx/Report.ts | 2 +- 2 files changed, 35 insertions(+), 47 deletions(-) rename src/pages/PrivateNotes/{PrivateNotesEditPage.js => PrivateNotesEditPage.tsx} (72%) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.js b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx similarity index 72% rename from src/pages/PrivateNotes/PrivateNotesEditPage.js rename to src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 0d4bc2c3e7e1..b6b178049024 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.js +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -1,12 +1,12 @@ import {useFocusEffect} from '@react-navigation/native'; +import type {RouteProp} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {OnyxCollection} from 'react-native-onyx'; +import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -14,50 +14,42 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withLocalize from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as Report from '@userActions/Report'; +import * as ReportActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type { PersonalDetails, Report } from '@src/types/onyx'; +import type { Note } from '@src/types/onyx/Report'; + +type PrivateNotesEditPageOnyxProps = { + /* Onyx Props */ -const propTypes = { /** All of the personal details for everyone */ - personalDetailsList: PropTypes.objectOf(personalDetailsPropType), + personalDetailsList: OnyxCollection, +} + +type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & { /** The report currently being looked at */ - report: reportPropTypes, - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** reportID and accountID passed via route: /r/:reportID/notes */ - reportID: PropTypes.string, - accountID: PropTypes.string, - }), - }).isRequired, -}; - -const defaultProps = { - report: {}, - personalDetailsList: {}, -}; - -function PrivateNotesEditPage({route, personalDetailsList, report}) { + report: Report, + + route: RouteProp<{params: {reportID: string; accountID: string}}>; +} + +function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotesEditPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); // We need to edit the note in markdown format, but display it in HTML format const parser = new ExpensiMark(); const [privateNote, setPrivateNote] = useState( - () => Report.getDraftPrivateNote(report.reportID).trim() || parser.htmlToMarkdown(lodashGet(report, ['privateNotes', route.params.accountID, 'note'], '')).trim(), + () => ReportActions.getDraftPrivateNote(report.reportID).trim() || parser.htmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(), ); /** @@ -67,8 +59,8 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { */ const debouncedSavePrivateNote = useMemo( () => - _.debounce((text) => { - Report.savePrivateNotesDraft(report.reportID, text); + lodashDebounce((text: string) => { + ReportActions.savePrivateNotesDraft(report.reportID, text); }, 1000), [report.reportID], ); @@ -94,18 +86,18 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { ); const savePrivateNote = () => { - const originalNote = lodashGet(report, ['privateNotes', route.params.accountID, 'note'], ''); + const originalNote = report?.privateNotes?.[Number(route.params.accountID)]?.note ?? ''; let editedNote = ''; if (privateNote.trim() !== originalNote.trim()) { - editedNote = Report.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim()); - Report.updatePrivateNotes(report.reportID, route.params.accountID, editedNote); + editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim()); + ReportActions.updatePrivateNotes(report.reportID, Number(route.params.accountID), editedNote); } // We want to delete saved private note draft after saving the note debouncedSavePrivateNote(''); Keyboard.dismiss(); - if (!_.some({...report.privateNotes, [route.params.accountID]: {note: editedNote}}, (item) => item.note)) { + if(({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); @@ -133,16 +125,16 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { > {translate( - Str.extractEmailDomain(lodashGet(personalDetailsList, [route.params.accountID, 'login'], '')) === CONST.EMAIL.GUIDES_DOMAIN + Str.extractEmailDomain(personalDetailsList?.[route.params.accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN ? 'privateNotes.sharedNoteMessage' : 'privateNotes.personalNoteMessage', )} Report.clearPrivateNotesError(report.reportID, route.params.accountID)} + onClose={() => ReportActions.clearPrivateNotesError(report.reportID, Number(route.params.accountID))} style={[styles.mb3]} > { + onChangeText={(text: string) => { debouncedSavePrivateNote(text); setPrivateNote(text); }} @@ -177,15 +169,11 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { } PrivateNotesEditPage.displayName = 'PrivateNotesEditPage'; -PrivateNotesEditPage.propTypes = propTypes; -PrivateNotesEditPage.defaultProps = defaultProps; -export default compose( - withLocalize, - withReportAndPrivateNotesOrNotFound('privateNotes.title'), - withOnyx({ +export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( + withOnyx({ personalDetailsList: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, - }), -)(PrivateNotesEditPage); + })(PrivateNotesEditPage) +); \ No newline at end of file diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 7cc3c508d926..22a60712597b 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -161,4 +161,4 @@ type Report = { export default Report; -export type {NotificationPreference, WriteCapability}; +export type {NotificationPreference, WriteCapability, Note}; From 1c13f5b86e49879b10a87b8de4c949c3de14fb18 Mon Sep 17 00:00:00 2001 From: Pujan Date: Tue, 16 Jan 2024 18:13:08 +0530 Subject: [PATCH 039/111] corrected the condition --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index b6b178049024..8dff3ffd54d6 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -97,7 +97,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes debouncedSavePrivateNote(''); Keyboard.dismiss(); - if(({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { + if(!({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); From 089c626465bbc3a3280774717bf8b97f803b3679 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 17 Jan 2024 09:10:07 +0100 Subject: [PATCH 040/111] Use logical or since label can be an empty string --- src/components/StatePicker/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/StatePicker/index.tsx b/src/components/StatePicker/index.tsx index 09f3b1a02802..a03e4f15fba0 100644 --- a/src/components/StatePicker/index.tsx +++ b/src/components/StatePicker/index.tsx @@ -62,7 +62,9 @@ function StatePicker({value, onInputChange, label, onBlur, errorText = ''}: Stat ref={ref} shouldShowRightIcon title={title} - description={label ?? translate('common.state')} + // Label can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + description={label || translate('common.state')} descriptionTextStyle={descStyle} onPress={showPickerModal} /> From f8bb83d29eba0e059d54d31bd9cc627fd10be673 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Jan 2024 15:37:25 +0700 Subject: [PATCH 041/111] remove any type --- src/components/ImageView/index.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 1c10c8116325..e8ee2ba3075a 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {MouseEvent as ReactMouseEvent, useCallback, useEffect, useRef, useState} from 'react'; import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -112,8 +112,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV return {offsetX, offsetY}; }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const onContainerPress = (e: any) => { + const onContainerPress = (mouseEvent?: GestureResponderEvent | KeyboardEvent) => { + const e = mouseEvent as unknown as ReactMouseEvent; if (!isZoomed && !isDragging) { if (e.nativeEvent) { const {offsetX, offsetY} = e.nativeEvent; @@ -138,10 +138,9 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV }; const trackPointerPosition = useCallback( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (e: any) => { + (e: MouseEvent) => { // Whether the pointer is released inside the ImageView - const isInsideImageView = scrollableRef.current?.contains(e.nativeEvent.target); + const isInsideImageView = scrollableRef.current?.contains(e.target as Node); if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { setIsDragging(false); From e0c74d377dc06dc718e6a68e5fea927a54011843 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Jan 2024 15:46:20 +0700 Subject: [PATCH 042/111] clean code --- src/components/ImageView/index.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index e8ee2ba3075a..fb696fb03c64 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -112,11 +112,11 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV return {offsetX, offsetY}; }; - const onContainerPress = (mouseEvent?: GestureResponderEvent | KeyboardEvent) => { - const e = mouseEvent as unknown as ReactMouseEvent; + const onContainerPress = (e?: GestureResponderEvent | KeyboardEvent) => { + const mouseEvent = e as unknown as ReactMouseEvent; if (!isZoomed && !isDragging) { - if (e.nativeEvent) { - const {offsetX, offsetY} = e.nativeEvent; + if (mouseEvent.nativeEvent) { + const {offsetX, offsetY} = mouseEvent.nativeEvent; // Dividing clicked positions by the zoom scale to get coordinates // so that once we zoom we will scroll to the clicked location. @@ -139,8 +139,9 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const trackPointerPosition = useCallback( (e: MouseEvent) => { + const mouseEvent = e as unknown as ReactMouseEvent; // Whether the pointer is released inside the ImageView - const isInsideImageView = scrollableRef.current?.contains(e.target as Node); + const isInsideImageView = scrollableRef.current?.contains(mouseEvent.nativeEvent.target as Node); if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { setIsDragging(false); @@ -152,13 +153,14 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const trackMovement = useCallback( (e: MouseEvent) => { + const mouseEvent = e as unknown as ReactMouseEvent; if (!isZoomed) { return; } if (isDragging && isMouseDown && scrollableRef.current) { - const x = e.x; - const y = e.y; + const x = mouseEvent.nativeEvent.x; + const y = mouseEvent.nativeEvent.y; const moveX = initialX - x; const moveY = initialY - y; scrollableRef.current.scrollLeft = initialScrollLeft + moveX; From 3d8c0ebdee04dfc9561050a4e7c79d3e1a20f08a Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 16:09:05 +0530 Subject: [PATCH 043/111] some method fix --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 8dff3ffd54d6..6a3749d9bdc4 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -97,7 +97,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes debouncedSavePrivateNote(''); Keyboard.dismiss(); - if(!({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { + if(!Object.values({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); From 1b687411d3a190e2e892cda60d47dd6dccab1835 Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 19:02:20 +0530 Subject: [PATCH 044/111] private notes list ts migration changes --- ...esListPage.js => PrivateNotesListPage.tsx} | 94 +++++++------------ 1 file changed, 32 insertions(+), 62 deletions(-) rename src/pages/PrivateNotes/{PrivateNotesListPage.js => PrivateNotesListPage.tsx} (59%) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.tsx similarity index 59% rename from src/pages/PrivateNotes/PrivateNotesListPage.js rename to src/pages/PrivateNotes/PrivateNotesListPage.tsx index 8e2f8c9f43e0..167a3523854c 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.js +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -1,68 +1,45 @@ import {useIsFocused} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {withNetwork} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type { PersonalDetails, Report, Session } from '@src/types/onyx'; +import type { OnyxCollection, OnyxEntry } from 'react-native-onyx'; -const propTypes = { - /** The report currently being looked at */ - report: reportPropTypes, - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** reportID and accountID passed via route: /r/:reportID/notes */ - reportID: PropTypes.string, - accountID: PropTypes.string, - }), - }).isRequired, - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), +type PrivateNotesListPageOnyxProps = { + /* Onyx Props */ /** All of the personal details for everyone */ - personalDetailsList: PropTypes.objectOf(personalDetailsPropType), + personalDetailsList: OnyxCollection, - ...withLocalizePropTypes, -}; + /** Session info for the currently logged in user. */ + session: OnyxEntry; +} -const defaultProps = { - report: {}, - session: { - accountID: null, - }, - personalDetailsList: {}, -}; +type PrivateNotesListPageProps = PrivateNotesListPageOnyxProps & { + /** The report currently being looked at */ + report: Report; +} -function PrivateNotesListPage({report, personalDetailsList, session}) { +function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const isFocused = useIsFocused(); useEffect(() => { const navigateToEditPageTimeout = setTimeout(() => { - if (_.some(report.privateNotes, (item) => item.note) || !isFocused) { + if (Object.values(report.privateNotes ?? {}).some((item) => item.note) || !isFocused) { return; } Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); @@ -75,12 +52,8 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { /** * Gets the menu item for each workspace - * - * @param {Object} item - * @param {Number} index - * @returns {JSX} */ - function getMenuItem(item, index) { + function getMenuItem(item, index: number) { const keyTitle = item.translationKey ? translate(item.translationKey) : item.title; return ( { - const privateNoteBrickRoadIndicator = (accountID) => (!_.isEmpty(lodashGet(report, ['privateNotes', accountID, 'errors'], '')) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''); - return _.chain(lodashGet(report, 'privateNotes', {})) - .map((privateNote, accountID) => ({ - title: Number(lodashGet(session, 'accountID', null)) === Number(accountID) ? translate('privateNotes.myNote') : lodashGet(personalDetailsList, [accountID, 'login'], ''), - action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), - brickRoadIndicator: privateNoteBrickRoadIndicator(accountID), - note: lodashGet(privateNote, 'note', ''), - disabled: Number(session.accountID) !== Number(accountID), - })) - .value(); + const privateNoteBrickRoadIndicator = (accountID: number) => report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + return Object.keys(report.privateNotes ?? {}) + .map((accountID: string) => { + const privateNote = report.privateNotes?.[Number(accountID)]; + return { + title: Number(session?.accountID) === Number(accountID) ? translate('privateNotes.myNote') : personalDetailsList?.[accountID]?.login ?? '', + action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), + brickRoadIndicator: privateNoteBrickRoadIndicator(Number(accountID)), + note: privateNote?.note ?? '', + disabled: Number(session?.accountID) !== Number(accountID), + } + }) }, [report, personalDetailsList, session, translate]); return ( @@ -133,25 +108,20 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { onCloseButtonPress={() => Navigation.dismissModal()} /> {translate('privateNotes.personalNoteMessage')} - {_.map(privateNotes, (item, index) => getMenuItem(item, index))} + {privateNotes.map((item, index) => getMenuItem(item, index))} ); } -PrivateNotesListPage.propTypes = propTypes; -PrivateNotesListPage.defaultProps = defaultProps; PrivateNotesListPage.displayName = 'PrivateNotesListPage'; -export default compose( - withLocalize, - withReportAndPrivateNotesOrNotFound('privateNotes.title'), - withOnyx({ +export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( + withOnyx({ personalDetailsList: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, session: { key: ONYXKEYS.SESSION, }, - }), - withNetwork(), -)(PrivateNotesListPage); + })(PrivateNotesListPage) +); From e5db70922171b0163282659ecd1260fb1032e35b Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 19:31:00 +0530 Subject: [PATCH 045/111] corrected back route --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 6a3749d9bdc4..db7a1299bb5c 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -112,7 +112,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes > Navigation.goBack(ROUTES.PRIVATE_NOTES_VIEW.getRoute(report.reportID, route.params.accountID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID))} shouldShowBackButton onCloseButtonPress={() => Navigation.dismissModal()} /> From 9298809b24b2d5a0675b0ba08cafd74e089bcc8e Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 17 Jan 2024 17:00:43 +0100 Subject: [PATCH 046/111] WIP --- src/ONYXKEYS.ts | 17 ++++---- src/components/AmountTextInput.tsx | 3 +- src/components/Composer/index.android.tsx | 3 +- src/components/Composer/index.ios.tsx | 5 ++- src/components/Composer/index.tsx | 6 ++- src/components/Form/FormProvider.tsx | 24 +++++++----- src/components/Form/InputWrapper.tsx | 14 ++++--- src/components/Form/types.ts | 39 +++++++++++-------- src/components/RNTextInput.tsx | 2 +- src/libs/ErrorUtils.ts | 2 +- src/libs/actions/FormActions.ts | 7 ++-- src/libs/actions/Plaid.ts | 2 +- src/libs/actions/Report.ts | 3 +- .../settings/Profile/DisplayNamePage.tsx | 8 ++-- src/types/onyx/Form.ts | 30 +++++++++----- src/types/onyx/ReimbursementAccount.ts | 4 +- src/types/onyx/index.ts | 5 +-- 17 files changed, 101 insertions(+), 73 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2915b7a4aa12..e5df472b5997 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -167,9 +167,6 @@ const ONYXKEYS = { /** Stores information about the active reimbursement account being set up */ REIMBURSEMENT_ACCOUNT: 'reimbursementAccount', - /** Stores draft information about the active reimbursement account being set up */ - REIMBURSEMENT_ACCOUNT_DRAFT: 'reimbursementAccountDraft', - /** Store preferred skintone for emoji */ PREFERRED_EMOJI_SKIN_TONE: 'preferredEmojiSkinTone', @@ -350,13 +347,15 @@ const ONYXKEYS = { REPORT_VIRTUAL_CARD_FRAUD_DRAFT: 'reportVirtualCardFraudFormDraft', GET_PHYSICAL_CARD_FORM: 'getPhysicalCardForm', GET_PHYSICAL_CARD_FORM_DRAFT: 'getPhysicalCardFormDraft', + REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount', + REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', }, } as const; type OnyxKeysMap = typeof ONYXKEYS; type OnyxCollectionKey = ValueOf; type OnyxKey = DeepValueOf>; -type OnyxFormKey = ValueOf | OnyxKeysMap['REIMBURSEMENT_ACCOUNT'] | OnyxKeysMap['REIMBURSEMENT_ACCOUNT_DRAFT']; +type OnyxFormKey = ValueOf; type OnyxValues = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; @@ -408,8 +407,7 @@ type OnyxValues = { [ONYXKEYS.CARD_LIST]: Record; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccountForm; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT]: OnyxTypes.ReimbursementAccountFormDraft; + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: string | number; [ONYXKEYS.FREQUENTLY_USED_EMOJIS]: OnyxTypes.FrequentlyUsedEmoji[]; [ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID]: string; @@ -489,8 +487,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.DateOfBirthForm; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.NEW_ROOM_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.NewRoomForm; + [ONYXKEYS.FORMS.NEW_ROOM_FORM_DRAFT]: OnyxTypes.NewRoomForm; [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.NEW_TASK_FORM]: OnyxTypes.Form; @@ -527,6 +525,9 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; + // @ts-expect-error test + [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index 0f3416076cc0..05080fcdd21c 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type {ForwardedRef} from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; @@ -34,7 +35,7 @@ type AmountTextInputProps = { function AmountTextInput( {formattedAmount, onChangeAmount, placeholder, selection, onSelectionChange, style, touchableInputWrapperStyle, onKeyPress}: AmountTextInputProps, - ref: BaseTextInputRef, + ref: ForwardedRef, ) { const styles = useThemeStyles(); return ( diff --git a/src/components/Composer/index.android.tsx b/src/components/Composer/index.android.tsx index d60a41e0f263..8480636a25bd 100644 --- a/src/components/Composer/index.android.tsx +++ b/src/components/Composer/index.android.tsx @@ -2,6 +2,7 @@ import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {TextInput} from 'react-native'; import {StyleSheet} from 'react-native'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -35,7 +36,7 @@ function Composer( /** * Set the TextInput Ref */ - const setTextInputRef = useCallback((el: TextInput) => { + const setTextInputRef = useCallback((el: AnimatedTextInputRef) => { textInput.current = el; if (typeof ref !== 'function' || textInput.current === null) { return; diff --git a/src/components/Composer/index.ios.tsx b/src/components/Composer/index.ios.tsx index b1357fef9a46..9fd03e3f7485 100644 --- a/src/components/Composer/index.ios.tsx +++ b/src/components/Composer/index.ios.tsx @@ -2,6 +2,7 @@ import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {TextInput} from 'react-native'; import {StyleSheet} from 'react-native'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -27,7 +28,7 @@ function Composer( }: ComposerProps, ref: ForwardedRef, ) { - const textInput = useRef(null); + const textInput = useRef(null); const styles = useThemeStyles(); const theme = useTheme(); @@ -35,7 +36,7 @@ function Composer( /** * Set the TextInput Ref */ - const setTextInputRef = useCallback((el: TextInput) => { + const setTextInputRef = useCallback((el: AnimatedTextInputRef) => { textInput.current = el; if (typeof ref !== 'function' || textInput.current === null) { return; diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 19b7bb6bb30a..3320ef5fb68d 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -6,8 +6,10 @@ import {flushSync} from 'react-dom'; import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; import type {AnimatedProps} from 'react-native-reanimated'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -82,7 +84,7 @@ function Composer( const {windowWidth} = useWindowDimensions(); const navigation = useNavigation(); const textRef = useRef(null); - const textInput = useRef<(HTMLTextAreaElement & TextInput) | null>(null); + const textInput = useRef(null); const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp); const [selection, setSelection] = useState< | { @@ -358,7 +360,7 @@ function Composer( autoComplete="off" autoCorrect={!Browser.isMobileSafari()} placeholderTextColor={theme.placeholderText} - ref={(el: TextInput & HTMLTextAreaElement) => (textInput.current = el)} + ref={(el) => (textInput.current = el)} selection={selection} style={inputStyleMemo} value={value} diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 26e045c6a0b9..b7aab46c94c4 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -10,11 +10,14 @@ import CONST from '@src/CONST'; import type {OnyxFormKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Form, Network} from '@src/types/onyx'; +import type {FormValueType} from '@src/types/onyx/Form'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import type {FormProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueType} from './types'; +import type {BaseInputProps, FormProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; + +// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. @@ -23,7 +26,7 @@ const VALIDATE_DELAY = 200; type InitialDefaultValue = false | Date | ''; -function getInitialValueByType(valueType?: ValueType): InitialDefaultValue { +function getInitialValueByType(valueType?: ValueTypeKey): InitialDefaultValue { switch (valueType) { case 'string': return ''; @@ -151,7 +154,7 @@ function FormProvider( /** @param inputID - The inputID of the input being touched */ const setTouchedInput = useCallback( - (inputID: string) => { + (inputID: keyof Form) => { touchedInputs.current[inputID] = true; }, [touchedInputs], @@ -183,13 +186,13 @@ function FormProvider( }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); const resetForm = useCallback( - (optionalValue: Form) => { + (optionalValue: OnyxFormValuesFields) => { Object.keys(inputValues).forEach((inputID) => { setInputValues((prevState) => { const copyPrevState = {...prevState}; touchedInputs.current[inputID] = false; - copyPrevState[inputID] = optionalValue[inputID] || ''; + copyPrevState[inputID] = optionalValue[inputID as keyof OnyxFormValuesFields] || ''; return copyPrevState; }); @@ -202,8 +205,8 @@ function FormProvider( resetForm, })); - const registerInput: RegisterInput = useCallback( - (inputID, inputProps) => { + const registerInput = useCallback( + (inputID: keyof Form, inputProps: TInputProps): TInputProps => { const newRef: MutableRefObject = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); if (inputRefs.current[inputID] !== newRef) { inputRefs.current[inputID] = newRef; @@ -212,7 +215,7 @@ function FormProvider( inputValues[inputID] = inputProps.value; } else if (inputProps.shouldSaveDraft && draftValues?.[inputID] !== undefined && inputValues[inputID] === undefined) { inputValues[inputID] = draftValues[inputID]; - } else if (inputProps.shouldUseDefaultValue && inputValues[inputID] === undefined) { + } else if (inputProps.shouldUseDefaultValue && inputProps.defaultValue !== undefined && inputValues[inputID] === undefined) { // We force the form to set the input value from the defaultValue props if there is a saved valid value inputValues[inputID] = inputProps.defaultValue; } else if (inputValues[inputID] === undefined) { @@ -228,6 +231,7 @@ function FormProvider( .at(-1) ?? ''; const inputRef = inputProps.ref; + return { ...inputProps, ref: @@ -298,7 +302,7 @@ function FormProvider( } inputProps.onBlur?.(event); }, - onInputChange: (value: unknown, key?: string) => { + onInputChange: (value: FormValueType, key?: string) => { const inputKey = key ?? inputID; setInputValues((prevState) => { const newState = { @@ -309,7 +313,7 @@ function FormProvider( if (shouldValidateOnChange) { onValidate(newState); } - return newState; + return newState as Form; }); if (inputProps.shouldSaveDraft && !(formID as string).includes('Draft')) { diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 8e824875c6d4..4313d800708d 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,13 +1,17 @@ -import type {ForwardedRef} from 'react'; +import type {ForwardedRef, FunctionComponent} from 'react'; import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import FormContext from './FormContext'; -import type {InputWrapperProps, ValidInput} from './types'; +import type {BaseInputProps, InputWrapperProps} from './types'; -function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { - const {registerInput} = useContext(FormContext); +type WrappableInputs = typeof TextInput; +function InputWrapper( + {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, + ref: ForwardedRef, +) { + const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were // calling some methods too early or twice, so we had to add this check to prevent that side effect. @@ -16,7 +20,7 @@ function InputWrapper({InputComponent, inputID, value // TODO: Sometimes we return too many props with register input, so we need to consider if it's better to make the returned type more general and disregard the issue, or we would like to omit the unused props somehow. // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 0a9069ea596a..024d34d7e492 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,32 +1,39 @@ -import type {ComponentProps, ElementType, FocusEvent, MutableRefObject, ReactNode} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; -import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import type {ComponentProps, FocusEvent, ForwardedRef, FunctionComponent, Key, MutableRefObject, ReactNode, Ref, RefAttributes} from 'react'; +import {ComponentType} from 'react'; +import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; -import type {Form} from '@src/types/onyx'; +import type Form from '@src/types/onyx/Form'; +import type {BaseForm, FormValueType} from '@src/types/onyx/Form'; -type ValueType = 'string' | 'boolean' | 'date'; +type ValueTypeKey = 'string' | 'boolean' | 'date'; -type ValidInput = ElementType; - -type InputProps = ComponentProps & { +type BaseInputProps = { shouldSetTouchedOnBlurOnly?: boolean; onValueChange?: (value: unknown, key: string) => void; onTouched?: (event: unknown) => void; - valueType?: ValueType; - onBlur: (event: FocusEvent | Parameters['onBlur']>>[0]) => void; + valueType?: ValueTypeKey; + value?: FormValueType; + defaultValue?: FormValueType; + onBlur?: (event: FocusEvent | NativeSyntheticEvent) => void; + onPressOut?: (event: unknown) => void; + onPress?: (event: unknown) => void; + shouldSaveDraft?: boolean; + shouldUseDefaultValue?: boolean; + key?: Key | null | undefined; + ref?: Ref>; + isFocused?: boolean; }; -type InputWrapperProps = InputProps & { +type InputWrapperProps = TInputProps & { InputComponent: TInput; inputID: string; - valueType?: ValueType; }; type ExcludeDraft = T extends `${string}Draft` ? never : T; type OnyxFormKeyWithoutDraft = ExcludeDraft; type OnyxFormValues = OnyxValues[TOnyxKey]; -type OnyxFormValuesFields = Omit; +type OnyxFormValuesFields = Omit, keyof BaseForm>; type FormProps = { /** A unique Onyx key identifying the form */ @@ -57,9 +64,9 @@ type FormProps = { footerContent?: ReactNode; }; -type RegisterInput = (inputID: string, props: InputProps) => InputProps; +type RegisterInput = (inputID: keyof Form, inputProps: TInputProps) => TInputProps; -type InputRef = BaseTextInputRef; +type InputRef = FunctionComponent; type InputRefs = Record>; -export type {InputWrapperProps, ValidInput, FormProps, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft}; +export type {InputWrapperProps, FormProps, RegisterInput, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft}; diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index 526a5891df16..e21219e99730 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -9,7 +9,7 @@ import useTheme from '@hooks/useTheme'; // 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); -type AnimatedTextInputRef = typeof AnimatedTextInput & TextInput; +type AnimatedTextInputRef = typeof AnimatedTextInput & TextInput & HTMLInputElement; function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef) { const theme = useTheme(); diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 97d180408c8a..18aa262c2079 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -98,7 +98,7 @@ type ErrorsList = Record; /** * Method used to generate error message for given inputID - * @param errorList - An object containing current errors in the form + * @param errors - An object containing current errors in the form * @param message - Message to assign to the inputID errors */ function addErrorMessage(errors: ErrorsList, inputID?: string, message?: TKey) { diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 779874d8b890..243fd062efeb 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -3,19 +3,18 @@ import type {KeyValueMapping, NullishDeep} from 'react-native-onyx/lib/types'; import type {OnyxFormKeyWithoutDraft} from '@components/Form/types'; import FormUtils from '@libs/FormUtils'; import type {OnyxFormKey} from '@src/ONYXKEYS'; -import type {Form} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; function setIsLoading(formID: OnyxFormKey, isLoading: boolean) { - Onyx.merge(formID, {isLoading} satisfies Form); + Onyx.merge(formID, {isLoading}); } function setErrors(formID: OnyxFormKey, errors?: OnyxCommon.Errors | null) { - Onyx.merge(formID, {errors} satisfies Form); + Onyx.merge(formID, {errors}); } function setErrorFields(formID: OnyxFormKey, errorFields?: OnyxCommon.ErrorFields | null) { - Onyx.merge(formID, {errorFields} satisfies Form); + Onyx.merge(formID, {errorFields}); } function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDeep) { diff --git a/src/libs/actions/Plaid.ts b/src/libs/actions/Plaid.ts index ab828eefeece..8c35c391790a 100644 --- a/src/libs/actions/Plaid.ts +++ b/src/libs/actions/Plaid.ts @@ -28,7 +28,7 @@ function openPlaidBankLogin(allowDebit: boolean, bankAccountID: number) { }, { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, + key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, value: { plaidAccountID: '', }, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 4729feea736e..4aecc91c54e1 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2583,7 +2583,8 @@ function updateLastVisitTime(reportID: string) { function clearNewRoomFormError() { Onyx.set(ONYXKEYS.FORMS.NEW_ROOM_FORM, { isLoading: false, - errorFields: {}, + errorFields: null, + errors: null, }); } diff --git a/src/pages/settings/Profile/DisplayNamePage.tsx b/src/pages/settings/Profile/DisplayNamePage.tsx index a481b9ccdbec..75fd2b8dbe3c 100644 --- a/src/pages/settings/Profile/DisplayNamePage.tsx +++ b/src/pages/settings/Profile/DisplayNamePage.tsx @@ -21,6 +21,7 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; const updateDisplayName = (values: any) => { PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim()); @@ -38,13 +39,12 @@ function DisplayNamePage(props: any) { * @returns - An object containing the errors for each inputID */ const validate = (values: OnyxFormValuesFields) => { - const errors = {}; - + const errors: Errors = {}; // First we validate the first name field if (!ValidationUtils.isValidDisplayName(values.firstName)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.hasInvalidCharacter'); } - if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES as string[])) { + if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES as unknown as string[])) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.containsReservedWord'); } @@ -117,7 +117,7 @@ export default compose( withCurrentUserPersonalDetails, withOnyx({ isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP, + key: ONYXKEYS.IS_LOADING_APP as any, }, }), )(DisplayNamePage); diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 9306ab5736fc..8da34697fe5d 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -1,9 +1,9 @@ import type * as OnyxTypes from './index'; import type * as OnyxCommon from './OnyxCommon'; -type Form = { - [key: string]: unknown; +type FormValueType = string | boolean | Date; +type BaseForm = { /** Controls the loading state of the form */ isLoading?: boolean; @@ -14,21 +14,31 @@ type Form = { errorFields?: OnyxCommon.ErrorFields | null; }; -type AddDebitCardForm = Form & { - /** Whether or not the form has been submitted */ +type Form = Record> = TFormValues & BaseForm; + +type AddDebitCardForm = Form<{ + /** Whether the form has been submitted */ setupComplete: boolean; -}; +}>; -type DateOfBirthForm = Form & { +type DateOfBirthForm = Form<{ /** Date of birth */ dob?: string; -}; +}>; -type DisplayNameForm = OnyxTypes.Form & { +type DisplayNameForm = Form<{ firstName: string; lastName: string; -}; +}>; + +type NewRoomForm = Form<{ + roomName?: string; + welcomeMessage?: string; + policyID?: string; + writeCapability?: string; + visibility?: string; +}>; export default Form; -export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm}; +export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, FormValueType, NewRoomForm, BaseForm}; diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts index 4779b790eac0..fca43df9b06e 100644 --- a/src/types/onyx/ReimbursementAccount.ts +++ b/src/types/onyx/ReimbursementAccount.ts @@ -49,7 +49,5 @@ type ReimbursementAccount = { pendingAction?: OnyxCommon.PendingAction; }; -type ReimbursementAccountForm = ReimbursementAccount & OnyxTypes.Form; - export default ReimbursementAccount; -export type {BankAccountStep, BankAccountSubStep, ReimbursementAccountForm}; +export type {BankAccountStep, BankAccountSubStep}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 1436bb38e1e2..6fcc5ec03d58 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -9,7 +9,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, NewRoomForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -38,7 +38,6 @@ import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; import type RecentlyUsedTags from './RecentlyUsedTags'; import type RecentWaypoint from './RecentWaypoint'; import type ReimbursementAccount from './ReimbursementAccount'; -import type {ReimbursementAccountForm} from './ReimbursementAccount'; import type ReimbursementAccountDraft from './ReimbursementAccountDraft'; import type {ReimbursementAccountFormDraft} from './ReimbursementAccountDraft'; import type Report from './Report'; @@ -112,7 +111,6 @@ export type { RecentlyUsedCategories, RecentlyUsedTags, ReimbursementAccount, - ReimbursementAccountForm, ReimbursementAccountDraft, ReimbursementAccountFormDraft, Report, @@ -144,4 +142,5 @@ export type { ReportUserIsTyping, PolicyReportField, RecentlyUsedReportFields, + NewRoomForm, }; From bf7f887234f0e7f9714570d5bd911e1d613e3fd5 Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 21:34:37 +0530 Subject: [PATCH 047/111] removed notes view page --- src/ROUTES.ts | 4 - src/SCREENS.ts | 1 - .../AppNavigator/ModalStackNavigators.tsx | 1 - src/libs/Navigation/linkingConfig.ts | 1 - .../PrivateNotes/PrivateNotesViewPage.js | 112 ------------------ 5 files changed, 119 deletions(-) delete mode 100644 src/pages/PrivateNotes/PrivateNotesViewPage.js diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 37003a09a0cd..532516bf0f42 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -224,10 +224,6 @@ const ROUTES = { route: 'r/:reportID/assignee', getRoute: (reportID: string) => `r/${reportID}/assignee` as const, }, - PRIVATE_NOTES_VIEW: { - route: 'r/:reportID/notes/:accountID', - getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}` as const, - }, PRIVATE_NOTES_LIST: { route: 'r/:reportID/notes', getRoute: (reportID: string) => `r/${reportID}/notes` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 703cb309d641..bf131078466b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -180,7 +180,6 @@ const SCREENS = { }, PRIVATE_NOTES: { - VIEW: 'PrivateNotes_View', LIST: 'PrivateNotes_List', EDIT: 'PrivateNotes_Edit', }, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index b0f33af0ce2e..1d586c6f7378 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -269,7 +269,6 @@ const EditRequestStackNavigator = createModalStackNavigator({ - [SCREENS.PRIVATE_NOTES.VIEW]: () => require('../../../pages/PrivateNotes/PrivateNotesViewPage').default as React.ComponentType, [SCREENS.PRIVATE_NOTES.LIST]: () => require('../../../pages/PrivateNotes/PrivateNotesListPage').default as React.ComponentType, [SCREENS.PRIVATE_NOTES.EDIT]: () => require('../../../pages/PrivateNotes/PrivateNotesEditPage').default as React.ComponentType, }); diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts index 1a495e92eb80..f0a031a88302 100644 --- a/src/libs/Navigation/linkingConfig.ts +++ b/src/libs/Navigation/linkingConfig.ts @@ -278,7 +278,6 @@ const linkingConfig: LinkingOptions = { }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { screens: { - [SCREENS.PRIVATE_NOTES.VIEW]: ROUTES.PRIVATE_NOTES_VIEW.route, [SCREENS.PRIVATE_NOTES.LIST]: ROUTES.PRIVATE_NOTES_LIST.route, [SCREENS.PRIVATE_NOTES.EDIT]: ROUTES.PRIVATE_NOTES_EDIT.route, }, diff --git a/src/pages/PrivateNotes/PrivateNotesViewPage.js b/src/pages/PrivateNotes/PrivateNotesViewPage.js deleted file mode 100644 index f71259a2b685..000000000000 --- a/src/pages/PrivateNotes/PrivateNotesViewPage.js +++ /dev/null @@ -1,112 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {ScrollView} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize from '@components/withLocalize'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import Navigation from '@libs/Navigation/Navigation'; -import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; - -const propTypes = { - /** All of the personal details for everyone */ - personalDetailsList: PropTypes.objectOf(personalDetailsPropType), - - /** The report currently being looked at */ - report: reportPropTypes, - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** reportID and accountID passed via route: /r/:reportID/notes */ - reportID: PropTypes.string, - accountID: PropTypes.string, - }), - }).isRequired, - - /** Session of currently logged in user */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), -}; - -const defaultProps = { - report: {}, - session: { - accountID: null, - }, - personalDetailsList: {}, -}; - -function PrivateNotesViewPage({route, personalDetailsList, session, report}) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const isCurrentUserNote = Number(session.accountID) === Number(route.params.accountID); - const privateNote = lodashGet(report, ['privateNotes', route.params.accountID, 'note'], ''); - - const getFallbackRoute = () => { - const privateNotes = lodashGet(report, 'privateNotes', {}); - - if (_.keys(privateNotes).length === 1) { - return ROUTES.HOME; - } - - return ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID); - }; - - return ( - - Navigation.goBack(getFallbackRoute())} - subtitle={isCurrentUserNote ? translate('privateNotes.myNote') : `${lodashGet(personalDetailsList, [route.params.accountID, 'login'], '')} note`} - shouldShowBackButton - onCloseButtonPress={() => Navigation.dismissModal()} - /> - - - isCurrentUserNote && Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, route.params.accountID))} - shouldShowRightIcon={isCurrentUserNote} - numberOfLinesTitle={0} - shouldRenderAsHTML - brickRoadIndicator={!_.isEmpty(lodashGet(report, ['privateNotes', route.params.accountID, 'errors'], '')) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} - disabled={!isCurrentUserNote} - shouldGreyOutWhenDisabled={false} - /> - - - - ); -} - -PrivateNotesViewPage.displayName = 'PrivateNotesViewPage'; -PrivateNotesViewPage.propTypes = propTypes; -PrivateNotesViewPage.defaultProps = defaultProps; - -export default compose( - withLocalize, - withReportAndPrivateNotesOrNotFound('privateNotes.title'), - withOnyx({ - personalDetailsList: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - }), -)(PrivateNotesViewPage); From 2baa8784975e99dcd3ba9dd4b2658fc46abd84ba Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 21:51:03 +0530 Subject: [PATCH 048/111] removed notes view for types --- src/libs/Navigation/types.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 8d227fa6f697..f87ed5094a82 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -331,10 +331,6 @@ type ProcessMoneyRequestHoldNavigatorParamList = { }; type PrivateNotesNavigatorParamList = { - [SCREENS.PRIVATE_NOTES.VIEW]: { - reportID: string; - accountID: string; - }; [SCREENS.PRIVATE_NOTES.LIST]: { reportID: string; accountID: string; From 64ead2bb48c1df23e1f972c834262980fbf21c37 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 17 Jan 2024 18:42:47 +0100 Subject: [PATCH 049/111] Rename REIMBURSEMENT_ACCOUNT_FORM_DRAFT --- src/libs/actions/ReimbursementAccount/index.js | 2 +- .../actions/ReimbursementAccount/resetFreePlanBankAccount.js | 2 +- src/pages/ReimbursementAccount/ACHContractStep.js | 2 +- src/pages/ReimbursementAccount/BankAccountManualStep.js | 2 +- src/pages/ReimbursementAccount/BankAccountPlaidStep.js | 2 +- src/pages/ReimbursementAccount/CompanyStep.js | 2 +- src/pages/ReimbursementAccount/ReimbursementAccountPage.js | 2 +- src/pages/ReimbursementAccount/RequestorStep.js | 2 +- src/pages/ReimbursementAccount/ValidationStep.js | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/ReimbursementAccount/index.js b/src/libs/actions/ReimbursementAccount/index.js index 0404115f086b..e23f80e61d12 100644 --- a/src/libs/actions/ReimbursementAccount/index.js +++ b/src/libs/actions/ReimbursementAccount/index.js @@ -30,7 +30,7 @@ function setWorkspaceIDForReimbursementAccount(workspaceID) { * @param {Object} bankAccountData */ function updateReimbursementAccountDraft(bankAccountData) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, bankAccountData); + Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, bankAccountData); Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined}); } diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js index 14c988033689..3110c059d2fc 100644 --- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js +++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js @@ -60,7 +60,7 @@ function resetFreePlanBankAccount(bankAccountID, session) { }, { onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, + key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, value: {}, }, ], diff --git a/src/pages/ReimbursementAccount/ACHContractStep.js b/src/pages/ReimbursementAccount/ACHContractStep.js index 806e438d0397..625a29ddc130 100644 --- a/src/pages/ReimbursementAccount/ACHContractStep.js +++ b/src/pages/ReimbursementAccount/ACHContractStep.js @@ -159,7 +159,7 @@ function ACHContractStep(props) { guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_BANK_ACCOUNT} /> Date: Wed, 17 Jan 2024 18:53:49 +0100 Subject: [PATCH 050/111] Clean rest of form PR --- src/ONYXKEYS.ts | 2 +- src/components/Composer/index.tsx | 3 +-- src/components/Form/FormProvider.tsx | 4 ++-- src/components/Form/FormWrapper.tsx | 4 ++-- src/components/Form/InputWrapper.tsx | 16 +++++++++++----- src/components/Form/types.ts | 14 ++++++++------ src/types/onyx/Form.ts | 1 - src/types/onyx/ReimbursementAccount.ts | 1 - 8 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index e5df472b5997..ee6c89b65cbd 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -525,7 +525,7 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; - // @ts-expect-error test + // @ts-expect-error Different values are defined under the same key: ReimbursementAccount and ReimbursementAccountForm [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; }; diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3320ef5fb68d..71ce5e546b16 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -3,13 +3,12 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; -import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData} from 'react-native'; +import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; import type {AnimatedProps} from 'react-native-reanimated'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; -import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index b7aab46c94c4..8fe35c989c62 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -15,7 +15,7 @@ import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import type {BaseInputProps, FormProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; +import type {BaseInputProps, FormProps, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. @@ -207,7 +207,7 @@ function FormProvider( const registerInput = useCallback( (inputID: keyof Form, inputProps: TInputProps): TInputProps => { - const newRef: MutableRefObject = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); + const newRef: MutableRefObject = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); if (inputRefs.current[inputID] !== newRef) { inputRefs.current[inputID] = newRef; } diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index a513b8fa0845..77b34cb551aa 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -85,7 +85,7 @@ function FormWrapper({ const focusInput = inputRef && 'current' in inputRef ? inputRef.current : undefined; // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. - if (typeof focusInput?.isFocused !== 'function') { + if (focusInput && typeof focusInput?.isFocused !== 'function') { Keyboard.dismiss(); } @@ -102,7 +102,7 @@ function FormWrapper({ } // Focus the input after scrolling, as on the Web it gives a slightly better visual result - focusInput?.focus(); + focusInput?.focus?.(); }} containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} enabledWhenOffline={enabledWhenOffline} diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 4313d800708d..e1f210b05ae9 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,13 +1,19 @@ -import type {ForwardedRef, FunctionComponent} from 'react'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; +import type AmountTextInput from '@components/AmountTextInput'; +import type CheckboxWithLabel from '@components/CheckboxWithLabel'; +import type Picker from '@components/Picker'; +import type SingleChoiceQuestion from '@components/SingleChoiceQuestion'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import FormContext from './FormContext'; import type {BaseInputProps, InputWrapperProps} from './types'; -type WrappableInputs = typeof TextInput; +// TODO: Add remaining inputs here once these components are migrated to Typescript: +// AddressSearch | CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker +type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker; -function InputWrapper( +function InputWrapper( {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef, ) { @@ -19,8 +25,8 @@ function InputWrapper; + // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any + return ; } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 024d34d7e492..846322dd719b 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,5 +1,4 @@ -import type {ComponentProps, FocusEvent, ForwardedRef, FunctionComponent, Key, MutableRefObject, ReactNode, Ref, RefAttributes} from 'react'; -import {ComponentType} from 'react'; +import type {FocusEvent, Key, MutableRefObject, ReactNode, Ref} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type Form from '@src/types/onyx/Form'; @@ -7,6 +6,8 @@ import type {BaseForm, FormValueType} from '@src/types/onyx/Form'; type ValueTypeKey = 'string' | 'boolean' | 'date'; +type MeasureLayoutOnSuccessCallback = (left: number, top: number, width: number, height: number) => void; + type BaseInputProps = { shouldSetTouchedOnBlurOnly?: boolean; onValueChange?: (value: unknown, key: string) => void; @@ -20,8 +21,10 @@ type BaseInputProps = { shouldSaveDraft?: boolean; shouldUseDefaultValue?: boolean; key?: Key | null | undefined; - ref?: Ref>; + ref?: Ref; isFocused?: boolean; + measureLayout?: (ref: unknown, callback: MeasureLayoutOnSuccessCallback) => void; + focus?: () => void; }; type InputWrapperProps = TInputProps & { @@ -66,7 +69,6 @@ type FormProps = { type RegisterInput = (inputID: keyof Form, inputProps: TInputProps) => TInputProps; -type InputRef = FunctionComponent; -type InputRefs = Record>; +type InputRefs = Record>; -export type {InputWrapperProps, FormProps, RegisterInput, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft}; +export type {InputWrapperProps, FormProps, RegisterInput, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRefs, OnyxFormKeyWithoutDraft}; diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 8da34697fe5d..6ef0197495d5 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -1,4 +1,3 @@ -import type * as OnyxTypes from './index'; import type * as OnyxCommon from './OnyxCommon'; type FormValueType = string | boolean | Date; diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts index fca43df9b06e..c0ade25e4d79 100644 --- a/src/types/onyx/ReimbursementAccount.ts +++ b/src/types/onyx/ReimbursementAccount.ts @@ -1,6 +1,5 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; -import type * as OnyxTypes from './index'; import type * as OnyxCommon from './OnyxCommon'; type BankAccountStep = ValueOf; From 0258fb46b0a715ffcad5591d2a600e8e41f82889 Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 14:54:07 +0530 Subject: [PATCH 051/111] removed jsdoc type --- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 167a3523854c..60ea21610c0b 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -80,7 +80,6 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot /** * Returns a list of private notes on the given chat report - * @returns {Array} the menu item list */ const privateNotes = useMemo(() => { const privateNoteBrickRoadIndicator = (accountID: number) => report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; From 6d44c76847b5e80302f33b5693fd94460297cd98 Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 14:59:58 +0530 Subject: [PATCH 052/111] prettier --- .../PrivateNotes/PrivateNotesEditPage.tsx | 23 ++++++----- .../PrivateNotes/PrivateNotesListPage.tsx | 39 +++++++++---------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index db7a1299bb5c..b78431601898 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -2,10 +2,10 @@ import {useFocusEffect} from '@react-navigation/native'; import type {RouteProp} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; -import type {OnyxCollection} from 'react-native-onyx'; import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; +import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -24,23 +24,22 @@ import * as ReportActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type { PersonalDetails, Report } from '@src/types/onyx'; -import type { Note } from '@src/types/onyx/Report'; +import type {PersonalDetails, Report} from '@src/types/onyx'; +import type {Note} from '@src/types/onyx/Report'; type PrivateNotesEditPageOnyxProps = { /* Onyx Props */ /** All of the personal details for everyone */ - personalDetailsList: OnyxCollection, -} + personalDetailsList: OnyxCollection; +}; type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & { - /** The report currently being looked at */ - report: Report, + report: Report; route: RouteProp<{params: {reportID: string; accountID: string}}>; -} +}; function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotesEditPageProps) { const styles = useThemeStyles(); @@ -97,7 +96,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes debouncedSavePrivateNote(''); Keyboard.dismiss(); - if(!Object.values({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) { + if (!Object.values({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); @@ -132,7 +131,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes ReportActions.clearPrivateNotesError(report.reportID, Number(route.params.accountID))} style={[styles.mb3]} @@ -175,5 +174,5 @@ export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( personalDetailsList: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, - })(PrivateNotesEditPage) -); \ No newline at end of file + })(PrivateNotesEditPage), +); diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 60ea21610c0b..ef0d279e3de3 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -2,6 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import React, {useEffect, useMemo} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -14,23 +15,22 @@ import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAn import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type { PersonalDetails, Report, Session } from '@src/types/onyx'; -import type { OnyxCollection, OnyxEntry } from 'react-native-onyx'; +import type {PersonalDetails, Report, Session} from '@src/types/onyx'; type PrivateNotesListPageOnyxProps = { /* Onyx Props */ /** All of the personal details for everyone */ - personalDetailsList: OnyxCollection, + personalDetailsList: OnyxCollection; /** Session info for the currently logged in user. */ session: OnyxEntry; -} +}; type PrivateNotesListPageProps = PrivateNotesListPageOnyxProps & { /** The report currently being looked at */ report: Report; -} +}; function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); @@ -42,13 +42,13 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot if (Object.values(report.privateNotes ?? {}).some((item) => item.note) || !isFocused) { return; } - Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); + Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session?.accountID ?? '')); }, CONST.ANIMATED_TRANSITION); return () => { clearTimeout(navigateToEditPageTimeout); }; - }, [report.privateNotes, report.reportID, session.accountID, isFocused]); + }, [report.privateNotes, report.reportID, session?.accountID, isFocused]); /** * Gets the menu item for each workspace @@ -82,18 +82,17 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot * Returns a list of private notes on the given chat report */ const privateNotes = useMemo(() => { - const privateNoteBrickRoadIndicator = (accountID: number) => report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; - return Object.keys(report.privateNotes ?? {}) - .map((accountID: string) => { - const privateNote = report.privateNotes?.[Number(accountID)]; - return { - title: Number(session?.accountID) === Number(accountID) ? translate('privateNotes.myNote') : personalDetailsList?.[accountID]?.login ?? '', - action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), - brickRoadIndicator: privateNoteBrickRoadIndicator(Number(accountID)), - note: privateNote?.note ?? '', - disabled: Number(session?.accountID) !== Number(accountID), - } - }) + const privateNoteBrickRoadIndicator = (accountID: number) => (report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''); + return Object.keys(report.privateNotes ?? {}).map((accountID: string) => { + const privateNote = report.privateNotes?.[Number(accountID)]; + return { + title: Number(session?.accountID) === Number(accountID) ? translate('privateNotes.myNote') : personalDetailsList?.[accountID]?.login ?? '', + action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), + brickRoadIndicator: privateNoteBrickRoadIndicator(Number(accountID)), + note: privateNote?.note ?? '', + disabled: Number(session?.accountID) !== Number(accountID), + }; + }); }, [report, personalDetailsList, session, translate]); return ( @@ -122,5 +121,5 @@ export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( session: { key: ONYXKEYS.SESSION, }, - })(PrivateNotesListPage) + })(PrivateNotesListPage), ); From e744f155c905e37f808065b5a4f7b324b2b79fd0 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 12:15:44 +0100 Subject: [PATCH 053/111] Move onFixTheErrorsLinkPressed to a function --- src/components/Form/FormWrapper.tsx | 69 +++++++++++++++-------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 77b34cb551aa..660f53f1427f 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -52,10 +52,41 @@ function FormWrapper({ scrollContextEnabled = false, }: FormWrapperProps) { const styles = useThemeStyles(); - const formRef = useRef(null); - const formContentRef = useRef(null); + const formRef = useRef(null); + const formContentRef = useRef(null); const errorMessage = useMemo(() => (formState ? ErrorUtils.getLatestErrorMessage(formState) : undefined), [formState]); + const onFixTheErrorsLinkPressed = useCallback(() => { + const errorFields = !isEmptyObject(errors) ? errors : formState?.errorFields ?? {}; + const focusKey = Object.keys(inputRefs.current ?? {}).find((key) => Object.keys(errorFields).includes(key)); + + if (!focusKey) { + return; + } + + const focusInput = inputRefs.current?.[focusKey]?.current; + + // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. + if (typeof focusInput?.isFocused !== 'function') { + Keyboard.dismiss(); + } + + // We subtract 10 to scroll slightly above the input + if (formContentRef.current) { + // We measure relative to the content root, not the scroll view, as that gives + // consistent results across mobile and web + focusInput?.measureLayout?.(formContentRef.current, (X: number, y: number) => + formRef.current?.scrollTo({ + y: y - 10, + animated: false, + }), + ); + } + + // Focus the input after scrolling, as on the Web it gives a slightly better visual result + focusInput?.focus?.(); + }, [errors, formState?.errorFields, inputRefs]); + const scrollViewContent = useCallback( (safeAreaPaddingBottomStyle: SafeAreaChildrenProps['safeAreaPaddingBottomStyle']) => ( { - const errorFields = !isEmptyObject(errors) ? errors : formState?.errorFields ?? {}; - const focusKey = Object.keys(inputRefs.current ?? {}).find((key) => Object.keys(errorFields).includes(key)); - - if (!focusKey) { - return; - } - - const inputRef = inputRefs.current?.[focusKey]; - const focusInput = inputRef && 'current' in inputRef ? inputRef.current : undefined; - - // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. - if (focusInput && typeof focusInput?.isFocused !== 'function') { - Keyboard.dismiss(); - } - - // We subtract 10 to scroll slightly above the input - if (formContentRef.current) { - // We measure relative to the content root, not the scroll view, as that gives - // consistent results across mobile and web - focusInput?.measureLayout?.(formContentRef.current, (X: number, y: number) => - formRef.current?.scrollTo({ - y: y - 10, - animated: false, - }), - ); - } - - // Focus the input after scrolling, as on the Web it gives a slightly better visual result - focusInput?.focus?.(); - }} + onFixTheErrorsLinkPressed={onFixTheErrorsLinkPressed} containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} enabledWhenOffline={enabledWhenOffline} isSubmitActionDangerous={isSubmitActionDangerous} @@ -121,7 +122,6 @@ function FormWrapper({ formID, formState?.errorFields, formState?.isLoading, - inputRefs, isSubmitActionDangerous, isSubmitButtonVisible, onSubmit, @@ -131,6 +131,7 @@ function FormWrapper({ styles.mt5, submitButtonStyles, submitButtonText, + onFixTheErrorsLinkPressed, ], ); From bba2f1a085f4af16146703f6415d21bd0630366c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 12:16:23 +0100 Subject: [PATCH 054/111] Bring back DisplayNamePage --- ...DisplayNamePage.tsx => DisplayNamePage.js} | 69 ++++++++++++------- 1 file changed, 44 insertions(+), 25 deletions(-) rename src/pages/settings/Profile/{DisplayNamePage.tsx => DisplayNamePage.js} (65%) diff --git a/src/pages/settings/Profile/DisplayNamePage.tsx b/src/pages/settings/Profile/DisplayNamePage.js similarity index 65% rename from src/pages/settings/Profile/DisplayNamePage.tsx rename to src/pages/settings/Profile/DisplayNamePage.js index 75fd2b8dbe3c..8ea471283004 100644 --- a/src/pages/settings/Profile/DisplayNamePage.tsx +++ b/src/pages/settings/Profile/DisplayNamePage.js @@ -1,17 +1,17 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type {OnyxFormValuesFields} from '@components/Form/types'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import useLocalize from '@hooks/useLocalize'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -21,30 +21,46 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Errors} from '@src/types/onyx/OnyxCommon'; -const updateDisplayName = (values: any) => { +const propTypes = { + ...withLocalizePropTypes, + ...withCurrentUserPersonalDetailsPropTypes, + isLoadingApp: PropTypes.bool, +}; + +const defaultProps = { + ...withCurrentUserPersonalDetailsDefaultProps, + isLoadingApp: true, +}; + +/** + * Submit form to update user's first and last name (and display name) + * @param {Object} values + * @param {String} values.firstName + * @param {String} values.lastName + */ +const updateDisplayName = (values) => { PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim()); }; -function DisplayNamePage(props: any) { +function DisplayNamePage(props) { const styles = useThemeStyles(); - const {translate} = useLocalize(); const currentUserDetails = props.currentUserPersonalDetails || {}; /** - * @param values - * @param values.firstName - * @param values.lastName - * @returns - An object containing the errors for each inputID + * @param {Object} values + * @param {String} values.firstName + * @param {String} values.lastName + * @returns {Object} - An object containing the errors for each inputID */ - const validate = (values: OnyxFormValuesFields) => { - const errors: Errors = {}; + const validate = (values) => { + const errors = {}; + // First we validate the first name field if (!ValidationUtils.isValidDisplayName(values.firstName)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.hasInvalidCharacter'); } - if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES as unknown as string[])) { + if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.containsReservedWord'); } @@ -62,7 +78,7 @@ function DisplayNamePage(props: any) { testID={DisplayNamePage.displayName} > Navigation.goBack(ROUTES.SETTINGS_PROFILE)} /> {props.isLoadingApp ? ( @@ -73,21 +89,21 @@ function DisplayNamePage(props: any) { formID={ONYXKEYS.FORMS.DISPLAY_NAME_FORM} validate={validate} onSubmit={updateDisplayName} - submitButtonText={translate('common.save')} + submitButtonText={props.translate('common.save')} enabledWhenOffline shouldValidateOnBlur shouldValidateOnChange > - {translate('displayNamePage.isShownOnProfile')} + {props.translate('displayNamePage.isShownOnProfile')} @@ -97,10 +113,10 @@ function DisplayNamePage(props: any) { InputComponent={TextInput} inputID="lastName" name="lname" - label={translate('common.lastName')} - aria-label={translate('common.lastName')} + label={props.translate('common.lastName')} + aria-label={props.translate('common.lastName')} role={CONST.ROLE.PRESENTATION} - defaultValue={currentUserDetails?.lastName ?? ''} + defaultValue={lodashGet(currentUserDetails, 'lastName', '')} maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} spellCheck={false} /> @@ -111,13 +127,16 @@ function DisplayNamePage(props: any) { ); } +DisplayNamePage.propTypes = propTypes; +DisplayNamePage.defaultProps = defaultProps; DisplayNamePage.displayName = 'DisplayNamePage'; export default compose( + withLocalize, withCurrentUserPersonalDetails, withOnyx({ isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP as any, + key: ONYXKEYS.IS_LOADING_APP, }, }), )(DisplayNamePage); From 48b0867d05486bd6a713453110915fe95765bb1b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 12:29:16 +0100 Subject: [PATCH 055/111] Adjust the PR after CK review --- src/components/Form/FormProvider.tsx | 8 ++++---- src/components/Form/FormWrapper.tsx | 2 +- src/components/Form/types.ts | 8 ++++---- src/components/FormAlertWithSubmitButton.tsx | 2 +- src/components/ScrollViewWithContext.tsx | 4 +++- src/libs/actions/FormActions.ts | 5 +---- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 8fe35c989c62..379a13f21711 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -12,7 +12,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Form, Network} from '@src/types/onyx'; import type {FormValueType} from '@src/types/onyx/Form'; import type {Errors} from '@src/types/onyx/OnyxCommon'; -import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; import type {BaseInputProps, FormProps, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; @@ -173,7 +173,7 @@ function FormProvider( Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); // Validate form and return early if any errors are found - if (isNotEmptyObject(onValidate(trimmedStringValues))) { + if (!isEmptyObject(onValidate(trimmedStringValues))) { return; } @@ -280,7 +280,7 @@ function FormProvider( onBlur: (event) => { // Only run validation when user proactively blurs the input. if (Visibility.isVisible() && Visibility.hasFocus()) { - const relatedTarget = 'nativeEvent' in event && 'relatedTarget' in event.nativeEvent && event?.nativeEvent?.relatedTarget; + const relatedTarget = 'relatedTarget' in event.nativeEvent && event?.nativeEvent?.relatedTarget; const relatedTargetId = relatedTarget && 'id' in relatedTarget && typeof relatedTarget.id === 'string' && relatedTarget.id; // We delay the validation in order to prevent Checkbox loss of focus when // the user is focusing a TextInput and proceeds to toggle a CheckBox in @@ -316,7 +316,7 @@ function FormProvider( return newState as Form; }); - if (inputProps.shouldSaveDraft && !(formID as string).includes('Draft')) { + if (inputProps.shouldSaveDraft && !formID.includes('Draft')) { FormActions.setDraftValues(formID as OnyxFormKeyWithoutDraft, {[inputKey]: value}); } inputProps.onValueChange?.(value, inputKey); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 660f53f1427f..45b2edf0badd 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -42,7 +42,7 @@ function FormWrapper({ errors, inputRefs, submitButtonText, - footerContent = null, + footerContent, isSubmitButtonVisible = true, style, submitButtonStyles, diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 846322dd719b..a5825e209147 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,5 +1,5 @@ import type {FocusEvent, Key, MutableRefObject, ReactNode, Ref} from 'react'; -import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type Form from '@src/types/onyx/Form'; import type {BaseForm, FormValueType} from '@src/types/onyx/Form'; @@ -11,13 +11,13 @@ type MeasureLayoutOnSuccessCallback = (left: number, top: number, width: number, type BaseInputProps = { shouldSetTouchedOnBlurOnly?: boolean; onValueChange?: (value: unknown, key: string) => void; - onTouched?: (event: unknown) => void; + onTouched?: (event: GestureResponderEvent) => void; valueType?: ValueTypeKey; value?: FormValueType; defaultValue?: FormValueType; onBlur?: (event: FocusEvent | NativeSyntheticEvent) => void; - onPressOut?: (event: unknown) => void; - onPress?: (event: unknown) => void; + onPressOut?: (event: GestureResponderEvent) => void; + onPress?: (event: GestureResponderEvent) => void; shouldSaveDraft?: boolean; shouldUseDefaultValue?: boolean; key?: Key | null | undefined; diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index 512d2063dc0f..ae96aa6c5359 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -65,7 +65,7 @@ function FormAlertWithSubmitButton({ enabledWhenOffline = false, disablePressOnEnter = false, isSubmitActionDangerous = false, - footerContent = null, + footerContent, buttonStyles, buttonText, isAlertVisible, diff --git a/src/components/ScrollViewWithContext.tsx b/src/components/ScrollViewWithContext.tsx index de32ac3591a8..d8d63ba61012 100644 --- a/src/components/ScrollViewWithContext.tsx +++ b/src/components/ScrollViewWithContext.tsx @@ -54,7 +54,9 @@ function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ...rest {...restProps} ref={scrollViewRef} onScroll={setContextScrollPosition} - scrollEventThrottle={scrollEventThrottle ?? MIN_SMOOTH_SCROLL_EVENT_THROTTLE} + // It's possible for scrollEventThrottle to be 0, so we must use "||" to fallback to MIN_SMOOTH_SCROLL_EVENT_THROTTLE. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + scrollEventThrottle={scrollEventThrottle || MIN_SMOOTH_SCROLL_EVENT_THROTTLE} > {children} diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 243fd062efeb..9daaa4fef20c 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -21,11 +21,8 @@ function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDee Onyx.merge(FormUtils.getDraftKey(formID), draftValues); } -/** - * @param formID - */ function clearDraftValues(formID: OnyxFormKeyWithoutDraft) { - Onyx.merge(FormUtils.getDraftKey(formID), {}); + Onyx.set(FormUtils.getDraftKey(formID), {}); } export {setDraftValues, setErrorFields, setErrors, setIsLoading, clearDraftValues}; From e8bbd93a94d57cbb31ba2ce95716ea6a4d8b50f5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 12:39:50 +0100 Subject: [PATCH 056/111] Add AddressSearch to valid inputs --- src/components/Form/InputWrapper.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index e1f210b05ae9..559166aa5056 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,5 +1,6 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; +import type AddressSearch from '@components/AddressSearch'; import type AmountTextInput from '@components/AmountTextInput'; import type CheckboxWithLabel from '@components/CheckboxWithLabel'; import type Picker from '@components/Picker'; @@ -10,8 +11,8 @@ import FormContext from './FormContext'; import type {BaseInputProps, InputWrapperProps} from './types'; // TODO: Add remaining inputs here once these components are migrated to Typescript: -// AddressSearch | CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker -type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker; +// CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker +type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker | typeof AddressSearch; function InputWrapper( {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, From 6c8201a9e763c66a473c6771e919ae8ae2f861de Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 18:07:18 +0530 Subject: [PATCH 057/111] ref type fixes --- src/libs/updateMultilineInputRange/types.ts | 2 +- .../PrivateNotes/PrivateNotesEditPage.tsx | 8 ++-- .../PrivateNotes/PrivateNotesListPage.tsx | 47 +++++++++---------- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/libs/updateMultilineInputRange/types.ts b/src/libs/updateMultilineInputRange/types.ts index d1b134b09a99..ce8f553c51f8 100644 --- a/src/libs/updateMultilineInputRange/types.ts +++ b/src/libs/updateMultilineInputRange/types.ts @@ -1,5 +1,5 @@ import type {TextInput} from 'react-native'; -type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput, shouldAutoFocus?: boolean) => void; +type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput | null, shouldAutoFocus?: boolean) => void; export default UpdateMultilineInputRange; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index b78431601898..c6095a318029 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -5,6 +5,7 @@ import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; +import type {TextInput as TextInputRN} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -54,7 +55,6 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes /** * Save the draft of the private note. This debounced so that we're not ceaselessly saving your edit. Saving the draft * allows one to navigate somewhere else and come back to the private note and still have it in edit mode. - * @param {String} newDraft */ const debouncedSavePrivateNote = useMemo( () => @@ -65,8 +65,8 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes ); // To focus on the input field when the page loads - const privateNotesInput = useRef(null); - const focusTimeoutRef = useRef(null); + const privateNotesInput = useRef(null); + const focusTimeoutRef = useRef(null); useFocusEffect( useCallback(() => { @@ -115,6 +115,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes shouldShowBackButton onCloseButtonPress={() => Navigation.dismissModal()} /> + {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} void; + brickRoadIndicator: ValueOf | undefined; + note: string; + disabled: boolean; +}; + function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -53,28 +61,19 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot /** * Gets the menu item for each workspace */ - function getMenuItem(item, index: number) { - const keyTitle = item.translationKey ? translate(item.translationKey) : item.title; + function getMenuItem(item: NoteListItem) { return ( - - - + ); } @@ -82,7 +81,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot * Returns a list of private notes on the given chat report */ const privateNotes = useMemo(() => { - const privateNoteBrickRoadIndicator = (accountID: number) => (report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''); + const privateNoteBrickRoadIndicator = (accountID: number) => (report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined); return Object.keys(report.privateNotes ?? {}).map((accountID: string) => { const privateNote = report.privateNotes?.[Number(accountID)]; return { @@ -106,7 +105,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot onCloseButtonPress={() => Navigation.dismissModal()} /> {translate('privateNotes.personalNoteMessage')} - {privateNotes.map((item, index) => getMenuItem(item, index))} + {privateNotes.map((item) => getMenuItem(item))} ); } From 5d52ddad9d98d51c294cf80811e4b5c23d669d35 Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 18:29:54 +0530 Subject: [PATCH 058/111] added key --- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 2cc958e730c1..550234a0707e 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -64,6 +64,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot function getMenuItem(item: NoteListItem) { return ( Date: Thu, 18 Jan 2024 14:47:04 +0100 Subject: [PATCH 059/111] Improve comments and InputWrapper props --- src/components/Form/FormProvider.tsx | 4 +--- src/components/Form/FormWrapper.tsx | 2 +- src/components/Form/InputWrapper.tsx | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 379a13f21711..db9ea2e16d5a 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -17,8 +17,6 @@ import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; import type {BaseInputProps, FormProps, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; -// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. - // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. // More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 @@ -350,7 +348,7 @@ export default withOnyx({ network: { key: ONYXKEYS.NETWORK, }, - // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we had to cast the keys to any + // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we need to cast the keys to any formState: { // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any key: ({formID}) => formID as any, diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index dc0c6d5221a8..c12c9d1b5a44 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -168,7 +168,7 @@ FormWrapper.displayName = 'FormWrapper'; export default withOnyx({ formState: { - // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we had to cast the keys to any + // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we need to cast the keys to any // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any key: (props) => props.formID as any, }, diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 559166aa5056..b4cc5aab2d94 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,4 +1,4 @@ -import type {ForwardedRef} from 'react'; +import type {ComponentProps, ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; import type AddressSearch from '@components/AddressSearch'; import type AmountTextInput from '@components/AmountTextInput'; @@ -15,7 +15,7 @@ import type {BaseInputProps, InputWrapperProps} from './types'; type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker | typeof AddressSearch; function InputWrapper( - {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, + {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps>, ref: ForwardedRef, ) { const {registerInput} = useContext(FormContext); From eb8bb85fe2b8b8a04a5106308fe8739b54ecb145 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 15:52:30 +0100 Subject: [PATCH 060/111] Final touches to InputWrapper --- src/components/Form/InputWrapper.tsx | 18 +++--------------- src/components/Form/types.ts | 28 ++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index b4cc5aab2d94..68dd7219f96a 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,23 +1,11 @@ -import type {ComponentProps, ForwardedRef} from 'react'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; -import type AddressSearch from '@components/AddressSearch'; -import type AmountTextInput from '@components/AmountTextInput'; -import type CheckboxWithLabel from '@components/CheckboxWithLabel'; -import type Picker from '@components/Picker'; -import type SingleChoiceQuestion from '@components/SingleChoiceQuestion'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import FormContext from './FormContext'; -import type {BaseInputProps, InputWrapperProps} from './types'; +import type {InputWrapperProps, ValidInputs} from './types'; -// TODO: Add remaining inputs here once these components are migrated to Typescript: -// CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker -type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker | typeof AddressSearch; - -function InputWrapper( - {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps>, - ref: ForwardedRef, -) { +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index ecbb6a90d458..1418c900c022 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,9 +1,24 @@ -import type {FocusEvent, Key, MutableRefObject, ReactNode, Ref} from 'react'; +import type {ComponentProps, FocusEvent, Key, MutableRefObject, ReactNode, Ref} from 'react'; import type {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; +import type AddressSearch from '@components/AddressSearch'; +import type AmountTextInput from '@components/AmountTextInput'; +import type CheckboxWithLabel from '@components/CheckboxWithLabel'; +import type Picker from '@components/Picker'; +import type SingleChoiceQuestion from '@components/SingleChoiceQuestion'; +import type TextInput from '@components/TextInput'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type Form from '@src/types/onyx/Form'; import type {BaseForm, FormValueType} from '@src/types/onyx/Form'; +/** + * This type specifies all the inputs that can be used with `InputWrapper` component. Make sure to update it + * when adding new inputs or removing old ones. + * + * TODO: Add remaining inputs here once these components are migrated to Typescript: + * CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker + */ +type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker | typeof AddressSearch; + type ValueTypeKey = 'string' | 'boolean' | 'date'; type MeasureLayoutOnSuccessCallback = (left: number, top: number, width: number, height: number) => void; @@ -27,10 +42,11 @@ type BaseInputProps = { focus?: () => void; }; -type InputWrapperProps = TInputProps & { - InputComponent: TInput; - inputID: string; -}; +type InputWrapperProps = BaseInputProps & + ComponentProps & { + InputComponent: TInput; + inputID: string; + }; type ExcludeDraft = T extends `${string}Draft` ? never : T; type OnyxFormKeyWithoutDraft = ExcludeDraft; @@ -74,4 +90,4 @@ type RegisterInput = (inputID: keyof Form, i type InputRefs = Record>; -export type {InputWrapperProps, FormProps, RegisterInput, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRefs, OnyxFormKeyWithoutDraft}; +export type {InputWrapperProps, FormProps, RegisterInput, ValidInputs, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRefs, OnyxFormKeyWithoutDraft}; From bc997b31897023cea6714e4aab62a451e7e01699 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 17:17:13 +0100 Subject: [PATCH 061/111] Fix reimbursment form bug --- src/components/Form/FormProvider.tsx | 2 +- src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index db9ea2e16d5a..10e4952a7896 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -278,7 +278,7 @@ function FormProvider( onBlur: (event) => { // Only run validation when user proactively blurs the input. if (Visibility.isVisible() && Visibility.hasFocus()) { - const relatedTarget = 'relatedTarget' in event.nativeEvent && event?.nativeEvent?.relatedTarget; + const relatedTarget = event && 'relatedTarget' in event.nativeEvent && event?.nativeEvent?.relatedTarget; const relatedTargetId = relatedTarget && 'id' in relatedTarget && typeof relatedTarget.id === 'string' && relatedTarget.id; // We delay the validation in order to prevent Checkbox loss of focus when // the user is focusing a TextInput and proceeds to toggle a CheckBox in diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index b0f33af0ce2e..7a9f92ec7996 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -64,7 +64,7 @@ function createModalStackNavigator(screens: getComponent={(screens as Required)[name as Screen]} /> ))} - + ); } From e1aa79263df57e6c677d6c5d50200883e29bf3fe Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 17:27:20 +0100 Subject: [PATCH 062/111] Fix prettier --- src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 7a9f92ec7996..b0f33af0ce2e 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -64,7 +64,7 @@ function createModalStackNavigator(screens: getComponent={(screens as Required)[name as Screen]} /> ))} - + ); } From 3e4c2fd231f7b378ce5707ea9b3a9a2ab7fd17d0 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 16:21:41 +0700 Subject: [PATCH 063/111] remove nativeEvent --- src/components/ImageView/index.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index fb696fb03c64..13a9033e2d4b 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,4 +1,5 @@ -import React, {MouseEvent as ReactMouseEvent, useCallback, useEffect, useRef, useState} from 'react'; +import type {MouseEvent as ReactMouseEvent} from 'react'; +import React, { useCallback, useEffect, useRef, useState} from 'react'; import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -138,10 +139,9 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV }; const trackPointerPosition = useCallback( - (e: MouseEvent) => { - const mouseEvent = e as unknown as ReactMouseEvent; + (event: MouseEvent) => { // Whether the pointer is released inside the ImageView - const isInsideImageView = scrollableRef.current?.contains(mouseEvent.nativeEvent.target as Node); + const isInsideImageView = scrollableRef.current?.contains(event.target as Node); if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { setIsDragging(false); @@ -152,15 +152,14 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV ); const trackMovement = useCallback( - (e: MouseEvent) => { - const mouseEvent = e as unknown as ReactMouseEvent; + (event: MouseEvent) => { if (!isZoomed) { return; } if (isDragging && isMouseDown && scrollableRef.current) { - const x = mouseEvent.nativeEvent.x; - const y = mouseEvent.nativeEvent.y; + const x = event.x; + const y = event.y; const moveX = initialX - x; const moveY = initialY - y; scrollableRef.current.scrollLeft = initialScrollLeft + moveX; From df1502d54b41d5040d0a044165070d033c4e4e37 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 16:25:17 +0700 Subject: [PATCH 064/111] nativeEvent checker --- src/components/ImageView/index.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 13a9033e2d4b..ed6cfd6413a3 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,4 +1,4 @@ -import type {MouseEvent as ReactMouseEvent} from 'react'; +import type {MouseEvent as ReactMouseEvent, SyntheticEvent} from 'react'; import React, { useCallback, useEffect, useRef, useState} from 'react'; import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; @@ -113,11 +113,10 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV return {offsetX, offsetY}; }; - const onContainerPress = (e?: GestureResponderEvent | KeyboardEvent) => { - const mouseEvent = e as unknown as ReactMouseEvent; + const onContainerPress = (e?: GestureResponderEvent | KeyboardEvent | SyntheticEvent) => { if (!isZoomed && !isDragging) { - if (mouseEvent.nativeEvent) { - const {offsetX, offsetY} = mouseEvent.nativeEvent; + if (e && 'nativeEvent' in e && 'offsetX' in e.nativeEvent) { + const {offsetX, offsetY} = e.nativeEvent; // Dividing clicked positions by the zoom scale to get coordinates // so that once we zoom we will scroll to the clicked location. From dbadc31306a6e4fc53df78ab8031c70b2fbcef87 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 16:28:12 +0700 Subject: [PATCH 065/111] lint fix --- src/components/ImageView/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index ed6cfd6413a3..3677111c09df 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,5 +1,5 @@ -import type {MouseEvent as ReactMouseEvent, SyntheticEvent} from 'react'; -import React, { useCallback, useEffect, useRef, useState} from 'react'; +import type {SyntheticEvent} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; From 9e0c27acd2a6484d7094a30f1a465635211fdbea Mon Sep 17 00:00:00 2001 From: Aldo Canepa Garay <87341702+aldo-expensify@users.noreply.github.com> Date: Fri, 19 Jan 2024 11:42:42 -0300 Subject: [PATCH 066/111] Fix comment --- 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 722d9198751e..0cf427cb2f0c 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1169,7 +1169,7 @@ function updateMoneyRequestMerchant(transactionID, transactionThreadReportID, va } /** - * Updates the tag date of a money request + * Updates the tag of a money request * * @param {String} transactionID * @param {Number} transactionThreadReportID From e9d8f4d7e996fa8d520e2c16a85ddd376a266745 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 22 Jan 2024 09:48:01 +0100 Subject: [PATCH 067/111] Add clear errors utils --- src/components/Form/FormProvider.tsx | 4 ++-- src/libs/actions/FormActions.ts | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 10e4952a7896..b71b611e60e5 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -94,9 +94,9 @@ function FormProvider( const trimmedStringValues = ValidationUtils.prepareValues(values); if (shouldClearServerError) { - FormActions.setErrors(formID, null); + FormActions.clearErrors(formID); } - FormActions.setErrorFields(formID, null); + FormActions.clearErrorFields(formID); const validateErrors = validate?.(trimmedStringValues) ?? {}; diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 9daaa4fef20c..00ad3652c665 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -9,14 +9,22 @@ function setIsLoading(formID: OnyxFormKey, isLoading: boolean) { Onyx.merge(formID, {isLoading}); } -function setErrors(formID: OnyxFormKey, errors?: OnyxCommon.Errors | null) { +function setErrors(formID: OnyxFormKey, errors: OnyxCommon.Errors) { Onyx.merge(formID, {errors}); } -function setErrorFields(formID: OnyxFormKey, errorFields?: OnyxCommon.ErrorFields | null) { +function setErrorFields(formID: OnyxFormKey, errorFields: OnyxCommon.ErrorFields) { Onyx.merge(formID, {errorFields}); } +function clearErrors(formID: OnyxFormKey) { + Onyx.merge(formID, {errors: null}); +} + +function clearErrorFields(formID: OnyxFormKey) { + Onyx.merge(formID, {errorFields: null}); +} + function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDeep) { Onyx.merge(FormUtils.getDraftKey(formID), draftValues); } @@ -25,4 +33,4 @@ function clearDraftValues(formID: OnyxFormKeyWithoutDraft) { Onyx.set(FormUtils.getDraftKey(formID), {}); } -export {setDraftValues, setErrorFields, setErrors, setIsLoading, clearDraftValues}; +export {setDraftValues, setErrorFields, setErrors, clearErrors, clearErrorFields, setIsLoading, clearDraftValues}; From 91a112518f6d0a5ebbf99362cd1799c075ea614f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 22 Jan 2024 09:55:10 +0100 Subject: [PATCH 068/111] Change the order of deps back to original --- src/components/Form/FormWrapper.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index c12c9d1b5a44..cdf66d986472 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -115,25 +115,25 @@ function FormWrapper({ ), [ - formID, - style, - onSubmit, children, - isSubmitButtonVisible, - submitButtonText, + enabledWhenOffline, + errorMessage, errors, + footerContent, + formID, formState?.errorFields, formState?.isLoading, - shouldHideFixErrorsAlert, - errorMessage, - footerContent, - onFixTheErrorsLinkPressed, + isSubmitActionDangerous, + isSubmitButtonVisible, + onSubmit, + style, + styles.flex1, styles.mh0, styles.mt5, - styles.flex1, submitButtonStyles, - enabledWhenOffline, - isSubmitActionDangerous, + submitButtonText, + shouldHideFixErrorsAlert, + onFixTheErrorsLinkPressed, ], ); From 13c2a366301d182b2e59407124c075f4bca39072 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 22 Jan 2024 16:29:07 +0700 Subject: [PATCH 069/111] fix: clean code --- src/components/ImageView/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 3677111c09df..ec37abf6d275 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -115,7 +115,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const onContainerPress = (e?: GestureResponderEvent | KeyboardEvent | SyntheticEvent) => { if (!isZoomed && !isDragging) { - if (e && 'nativeEvent' in e && 'offsetX' in e.nativeEvent) { + if (e && 'nativeEvent' in e && e.nativeEvent instanceof PointerEvent) { const {offsetX, offsetY} = e.nativeEvent; // Dividing clicked positions by the zoom scale to get coordinates From 698dbd9095901a0f87acdfc6df86e0690a14a43f Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Mon, 22 Jan 2024 19:07:54 +0500 Subject: [PATCH 070/111] feat: allow policy admins to edit the report fields of a non-settled report --- .../ReportActionItem/MoneyReportView.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 4fcca3e518a5..7e0ff9487232 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -1,6 +1,8 @@ import React, {useMemo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -17,9 +19,10 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; -import type {PolicyReportField, Report} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; -type MoneyReportViewProps = { +type MoneyReportViewComponentProps = { /** The report currently being looked at */ report: Report; @@ -30,7 +33,14 @@ type MoneyReportViewProps = { shouldShowHorizontalRule: boolean; }; -function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: MoneyReportViewProps) { +type MoneyReportViewOnyxProps = { + /** Policies that the user is part of */ + policies: OnyxCollection; +}; + +type MoneyReportViewProps = MoneyReportViewComponentProps & MoneyReportViewOnyxProps; + +function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule, policies}: MoneyReportViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -57,7 +67,7 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: () => policyReportFields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight), [policyReportFields], ); - + const isAdmin = ReportUtils.isPolicyAdmin(report.policyID ?? '', policies); return ( @@ -65,6 +75,7 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: {canUseReportFields && sortedPolicyReportFields.map((reportField) => { const title = ReportUtils.getReportFieldTitle(report, reportField); + const isDisabled = !isAdmin || isSettled; return ( {}} - shouldShowRightIcon - disabled={false} + shouldShowRightIcon={!isDisabled} + disabled={isDisabled} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} shouldGreyOutWhenDisabled={false} numberOfLinesTitle={0} @@ -165,4 +176,8 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: MoneyReportView.displayName = 'MoneyReportView'; -export default MoneyReportView; +export default withOnyx({ + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, +})(MoneyReportView); From 2aadb0a4fa973b1f64774c5ff70d98ad13283914 Mon Sep 17 00:00:00 2001 From: Pujan Date: Mon, 22 Jan 2024 23:59:32 +0530 Subject: [PATCH 071/111] used StackScreenProps --- src/libs/Navigation/types.ts | 5 +---- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 15 ++++++++------- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 17 +---------------- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fcd01aabda9e..2f1469e40a19 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -331,10 +331,7 @@ type ProcessMoneyRequestHoldNavigatorParamList = { }; type PrivateNotesNavigatorParamList = { - [SCREENS.PRIVATE_NOTES.LIST]: { - reportID: string; - accountID: string; - }; + [SCREENS.PRIVATE_NOTES.LIST]: undefined; [SCREENS.PRIVATE_NOTES.EDIT]: { reportID: string; accountID: string; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index c6095a318029..6292a2e3c412 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -1,5 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; -import type {RouteProp} from '@react-navigation/native'; +import type {StackScreenProps} from '@react-navigation/stack'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; @@ -18,6 +18,7 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import type {PrivateNotesNavigatorParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; @@ -25,6 +26,7 @@ import * as ReportActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; import type {PersonalDetails, Report} from '@src/types/onyx'; import type {Note} from '@src/types/onyx/Report'; @@ -35,12 +37,11 @@ type PrivateNotesEditPageOnyxProps = { personalDetailsList: OnyxCollection; }; -type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & { - /** The report currently being looked at */ - report: Report; - - route: RouteProp<{params: {reportID: string; accountID: string}}>; -}; +type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & + StackScreenProps & { + /** The report currently being looked at */ + report: Report; + }; function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotesEditPageProps) { const styles = useThemeStyles(); diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 550234a0707e..30bd90bed5b6 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -1,5 +1,4 @@ -import {useIsFocused} from '@react-navigation/native'; -import React, {useEffect, useMemo} from 'react'; +import React, {useMemo} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -43,20 +42,6 @@ type NoteListItem = { function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const isFocused = useIsFocused(); - - useEffect(() => { - const navigateToEditPageTimeout = setTimeout(() => { - if (Object.values(report.privateNotes ?? {}).some((item) => item.note) || !isFocused) { - return; - } - Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session?.accountID ?? '')); - }, CONST.ANIMATED_TRANSITION); - - return () => { - clearTimeout(navigateToEditPageTimeout); - }; - }, [report.privateNotes, report.reportID, session?.accountID, isFocused]); /** * Gets the menu item for each workspace From 95f6022c3e0540edfe2dcdc85bd32cd2032954f7 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Mon, 22 Jan 2024 17:06:35 -0500 Subject: [PATCH 072/111] add logging --- src/pages/LogInWithShortLivedAuthTokenPage.tsx | 2 ++ src/pages/LogOutPreviousUserPage.js | 2 ++ src/pages/signin/SAMLSignInPage/index.native.js | 3 +++ 3 files changed, 7 insertions(+) diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx index c5f8a9c20d5b..16e990392f14 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -18,6 +18,7 @@ import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type {Account} from '@src/types/onyx'; +import Log from '@libs/Log'; type LogInWithShortLivedAuthTokenPageOnyxProps = { /** The details about the account that the user is signing in with */ @@ -38,6 +39,7 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA // Try to authenticate using the shortLivedToken if we're not already trying to load the accounts if (token && !account?.isLoading) { + Log.info('LogInWithShortLivedAuthTokenPage - Successfully received shortLivedAuthToken. Signing in...'); Session.signInWithShortLivedAuthToken(email, token); return; } diff --git a/src/pages/LogOutPreviousUserPage.js b/src/pages/LogOutPreviousUserPage.js index 5c8a39204467..974823f489ed 100644 --- a/src/pages/LogOutPreviousUserPage.js +++ b/src/pages/LogOutPreviousUserPage.js @@ -4,6 +4,7 @@ import React, {useEffect} from 'react'; import {Linking} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import Log from '@libs/Log'; import * as SessionUtils from '@libs/SessionUtils'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -51,6 +52,7 @@ function LogOutPreviousUserPage(props) { // On Enabling 2FA, authToken stored in Onyx becomes expired and hence we need to fetch new authToken const shouldForceLogin = lodashGet(props, 'route.params.shouldForceLogin', '') === 'true'; if (shouldForceLogin) { + Log.info('LogOutPreviousUserPage - forcing login with shortLivedAuthToken'); const email = lodashGet(props, 'route.params.email', ''); const shortLivedAuthToken = lodashGet(props, 'route.params.shortLivedAuthToken', ''); Session.signInWithShortLivedAuthToken(email, shortLivedAuthToken); diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index 502e26e337b9..7211122b5d24 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -7,6 +7,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator'; import ScreenWrapper from '@components/ScreenWrapper'; import getPlatform from '@libs/getPlatform'; +import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; @@ -36,6 +37,7 @@ function SAMLSignInPage({credentials}) { */ const handleNavigationStateChange = useCallback( ({url}) => { + Log.info('SAMLSignInPage - Handling SAML navigation change'); // If we've gotten a callback then remove the option to navigate back to the sign in page if (url.includes('loginCallback')) { shouldShowNavigation(false); @@ -43,6 +45,7 @@ function SAMLSignInPage({credentials}) { const searchParams = new URLSearchParams(new URL(url).search); if (searchParams.has('shortLivedAuthToken')) { + Log.info('SAMLSignInPage - Successfully received shortLivedAuthToken. Signing in...'); const shortLivedAuthToken = searchParams.get('shortLivedAuthToken'); Session.signInWithShortLivedAuthToken(credentials.login, shortLivedAuthToken); } From 454c2b1ff8d466be6e8c88617299047e1798a634 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Mon, 22 Jan 2024 17:29:46 -0500 Subject: [PATCH 073/111] check for account.isLoading --- src/pages/signin/SAMLSignInPage/index.native.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index 7211122b5d24..9fe60e56353e 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -20,13 +20,20 @@ const propTypes = { /** The email/phone the user logged in with */ login: PropTypes.string, }), + + /** State of the logging in user's account */ + account: PropTypes.shape({ + /** Whether the account is loading */ + isLoading: PropTypes.bool, + }), }; const defaultProps = { credentials: {}, + account: {}, }; -function SAMLSignInPage({credentials}) { +function SAMLSignInPage({credentials, account}) { const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}&platform=${getPlatform()}`; const [showNavigation, shouldShowNavigation] = useState(true); @@ -44,7 +51,7 @@ function SAMLSignInPage({credentials}) { } const searchParams = new URLSearchParams(new URL(url).search); - if (searchParams.has('shortLivedAuthToken')) { + if (searchParams.has('shortLivedAuthToken') && !account.isLoading) { Log.info('SAMLSignInPage - Successfully received shortLivedAuthToken. Signing in...'); const shortLivedAuthToken = searchParams.get('shortLivedAuthToken'); Session.signInWithShortLivedAuthToken(credentials.login, shortLivedAuthToken); @@ -57,7 +64,7 @@ function SAMLSignInPage({credentials}) { Navigation.navigate(ROUTES.HOME); } }, - [credentials.login, shouldShowNavigation], + [credentials.login, shouldShowNavigation, account.isLoading], ); return ( @@ -95,4 +102,5 @@ SAMLSignInPage.displayName = 'SAMLSignInPage'; export default withOnyx({ credentials: {key: ONYXKEYS.CREDENTIALS}, + account: {key: ONYXKEYS.ACCOUNT}, })(SAMLSignInPage); From 420878d882ae2bf1474658a941ffdb7ea32a8cf4 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Mon, 22 Jan 2024 17:49:32 -0500 Subject: [PATCH 074/111] prettier --- src/pages/LogInWithShortLivedAuthTokenPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx index 16e990392f14..811c35fff34e 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -12,13 +12,13 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {PublicScreensParamList} from '@libs/Navigation/types'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type {Account} from '@src/types/onyx'; -import Log from '@libs/Log'; type LogInWithShortLivedAuthTokenPageOnyxProps = { /** The details about the account that the user is signing in with */ From acdd24bb3ec97736feeaf4d666f7975294cb7709 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 09:34:45 +0100 Subject: [PATCH 075/111] Fix typecheck after merging main --- src/ONYXKEYS.ts | 8 ++++---- src/components/Form/FormWrapper.tsx | 3 +++ src/components/Form/InputWrapper.tsx | 4 ++-- src/components/Form/types.ts | 6 +++--- src/pages/EditReportFieldDatePage.tsx | 17 ++++++++++------- src/pages/EditReportFieldTextPage.tsx | 14 ++++++++------ .../TeachersUnite/IntroSchoolPrincipalPage.tsx | 18 +++++------------- src/pages/TeachersUnite/KnowATeacherPage.tsx | 15 +++------------ src/types/onyx/Form.ts | 14 +++++++++++++- src/types/onyx/index.ts | 4 +++- 10 files changed, 54 insertions(+), 49 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index aa5cd7fe06f1..ab9af6112693 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -518,10 +518,10 @@ type OnyxValues = { [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: OnyxTypes.IKnowATeacherForm; + [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM_DRAFT]: OnyxTypes.IKnowATeacherForm; + [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM]: OnyxTypes.IntroSchoolPrincipalForm; + [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM_DRAFT]: OnyxTypes.IntroSchoolPrincipalForm; [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index cdf66d986472..d5b47761e4c0 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -33,6 +33,9 @@ type FormWrapperProps = ChildrenProps & /** Assuming refs are React refs */ inputRefs: RefObject; + + /** Callback to submit the form */ + onSubmit: () => void; }; function FormWrapper({ diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 68dd7219f96a..ae78e909753b 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,11 +1,11 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import TextInput from '@components/TextInput'; -import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import FormContext from './FormContext'; import type {InputWrapperProps, ValidInputs} from './types'; -function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 1418c900c022..447f3205ad68 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -36,13 +36,13 @@ type BaseInputProps = { shouldSaveDraft?: boolean; shouldUseDefaultValue?: boolean; key?: Key | null | undefined; - ref?: Ref; + ref?: Ref; isFocused?: boolean; measureLayout?: (ref: unknown, callback: MeasureLayoutOnSuccessCallback) => void; focus?: () => void; }; -type InputWrapperProps = BaseInputProps & +type InputWrapperProps = Omit & ComponentProps & { InputComponent: TInput; inputID: string; @@ -65,7 +65,7 @@ type FormProps = { isSubmitButtonVisible?: boolean; /** Callback to submit the form */ - onSubmit: (values?: Record) => void; + onSubmit: (values: OnyxFormValuesFields) => void; /** Should the button be enabled when offline */ enabledWhenOffline?: boolean; diff --git a/src/pages/EditReportFieldDatePage.tsx b/src/pages/EditReportFieldDatePage.tsx index 5ee86b2bf8e6..6faa84ef8b43 100644 --- a/src/pages/EditReportFieldDatePage.tsx +++ b/src/pages/EditReportFieldDatePage.tsx @@ -3,12 +3,15 @@ import {View} from 'react-native'; import DatePicker from '@components/DatePicker'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; type EditReportFieldDatePageProps = { /** Value of the policy report field */ @@ -27,12 +30,13 @@ type EditReportFieldDatePageProps = { function EditReportFieldDatePage({fieldName, onSubmit, fieldValue, fieldID}: EditReportFieldDatePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const inputRef = useRef(null); + const inputRef = useRef(null); const validate = useCallback( - (value: Record) => { - const errors: Record = {}; - if (value[fieldID].trim() === '') { + (values: OnyxFormValuesFields) => { + const errors: Errors = {}; + const value = values[fieldID]; + if (typeof value === 'string' && value.trim() === '') { errors[fieldID] = 'common.error.fieldRequired'; } return errors; @@ -48,7 +52,6 @@ function EditReportFieldDatePage({fieldName, onSubmit, fieldValue, fieldID}: Edi testID={EditReportFieldDatePage.displayName} > - {/* @ts-expect-error TODO: TS migration */} - InputComponent={DatePicker} inputID={fieldID} name={fieldID} diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx index b468861e9a27..80cc700fec69 100644 --- a/src/pages/EditReportFieldTextPage.tsx +++ b/src/pages/EditReportFieldTextPage.tsx @@ -2,13 +2,16 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; type EditReportFieldTextPageProps = { /** Value of the policy report field */ @@ -27,12 +30,13 @@ type EditReportFieldTextPageProps = { function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, fieldID}: EditReportFieldTextPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const inputRef = useRef(null); + const inputRef = useRef(null); const validate = useCallback( - (value: Record) => { - const errors: Record = {}; - if (value[fieldID].trim() === '') { + (values: OnyxFormValuesFields<'policyReportFieldEditForm'>) => { + const errors: Errors = {}; + const value = values[fieldID]; + if (typeof value === 'string' && value.trim() === '') { errors[fieldID] = 'common.error.fieldRequired'; } return errors; @@ -48,7 +52,6 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, fieldID}: Edi testID={EditReportFieldTextPage.displayName} > - {/* @ts-expect-error TODO: TS migration */} ; @@ -42,7 +38,7 @@ function IntroSchoolPrincipalPage(props: IntroSchoolPrincipalPageProps) { /** * Submit form to pass firstName, partnerUserID and lastName */ - const onSubmit = (values: IntroSchoolPrincipalFormData) => { + const onSubmit = (values: OnyxFormValuesFields) => { const policyID = isProduction ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; TeachersUnite.addSchoolPrincipal(values.firstName.trim(), values.partnerUserID.trim(), values.lastName.trim(), policyID); }; @@ -51,8 +47,8 @@ function IntroSchoolPrincipalPage(props: IntroSchoolPrincipalPageProps) { * @returns - An object containing the errors for each inputID */ const validate = useCallback( - (values: IntroSchoolPrincipalFormData) => { - const errors = {}; + (values: OnyxFormValuesFields) => { + const errors: Errors = {}; if (!ValidationUtils.isValidLegalName(values.firstName)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'privatePersonalDetails.error.hasInvalidCharacter'); @@ -91,7 +87,6 @@ function IntroSchoolPrincipalPage(props: IntroSchoolPrincipalPageProps) { title={translate('teachersUnitePage.introSchoolPrincipal')} onBackButtonPress={() => Navigation.goBack(ROUTES.TEACHERS_UNITE)} /> - {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} {translate('teachersUnitePage.schoolPrincipalVerfiyExpense')} ; }; @@ -42,7 +37,7 @@ function KnowATeacherPage(props: KnowATeacherPageProps) { /** * Submit form to pass firstName, partnerUserID and lastName */ - const onSubmit = (values: KnowATeacherFormData) => { + const onSubmit = (values: OnyxFormValuesFields) => { const phoneLogin = LoginUtils.getPhoneLogin(values.partnerUserID); const validateIfnumber = LoginUtils.validateNumber(phoneLogin); const contactMethod = (validateIfnumber || values.partnerUserID).trim().toLowerCase(); @@ -58,7 +53,7 @@ function KnowATeacherPage(props: KnowATeacherPageProps) { * @returns - An object containing the errors for each inputID */ const validate = useCallback( - (values: KnowATeacherFormData) => { + (values: OnyxFormValuesFields) => { const errors = {}; const phoneLogin = LoginUtils.getPhoneLogin(values.partnerUserID); const validateIfnumber = LoginUtils.validateNumber(phoneLogin); @@ -97,7 +92,6 @@ function KnowATeacherPage(props: KnowATeacherPageProps) { title={translate('teachersUnitePage.iKnowATeacher')} onBackButtonPress={() => Navigation.goBack(ROUTES.TEACHERS_UNITE)} /> - {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} {translate('teachersUnitePage.getInTouch')} ; +type IKnowATeacherForm = Form<{ + firstName: string; + lastName: string; + partnerUserID: string; +}>; + +type IntroSchoolPrincipalForm = Form<{ + firstName: string; + lastName: string; + partnerUserID: string; +}>; + export default Form; -export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, FormValueType, NewRoomForm, BaseForm}; +export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, FormValueType, NewRoomForm, BaseForm, IKnowATeacherForm, IntroSchoolPrincipalForm}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 50497667917e..2aa794ffc5a3 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -9,7 +9,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, NewRoomForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -146,4 +146,6 @@ export type { PolicyReportFields, RecentlyUsedReportFields, NewRoomForm, + IKnowATeacherForm, + IntroSchoolPrincipalForm, }; From 4bc5494da62b0f4dcf4774a34e95a1dcdf0ed8e1 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 09:46:31 +0100 Subject: [PATCH 076/111] Use Onyx key instead of plain string --- src/pages/EditReportFieldTextPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx index 80cc700fec69..733bfd6e5fee 100644 --- a/src/pages/EditReportFieldTextPage.tsx +++ b/src/pages/EditReportFieldTextPage.tsx @@ -33,7 +33,7 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, fieldID}: Edi const inputRef = useRef(null); const validate = useCallback( - (values: OnyxFormValuesFields<'policyReportFieldEditForm'>) => { + (values: OnyxFormValuesFields) => { const errors: Errors = {}; const value = values[fieldID]; if (typeof value === 'string' && value.trim() === '') { From da41efb6a64e444c7611ad528c9d702e42012dbf Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Tue, 23 Jan 2024 23:47:20 +0500 Subject: [PATCH 077/111] feat: merge with main --- src/components/ReportActionItem/MoneyReportView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index ae3cc4c91b86..ed7c05b828a9 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -21,8 +21,8 @@ import * as ReportUtils from '@libs/ReportUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; import ROUTES from '@src/ROUTES'; +import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; type MoneyReportViewComponentProps = { /** The report currently being looked at */ From 0f43b03c4a8572de1a5a15120e527388ebcc5cf0 Mon Sep 17 00:00:00 2001 From: caitlinwhite1 Date: Tue, 23 Jan 2024 15:08:13 -0600 Subject: [PATCH 078/111] Delete docs/articles/expensify-classic/getting-started/Plan-Types.md this resource exists on UseDot, so let's delete it --- .../getting-started/Plan-Types.md | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 docs/articles/expensify-classic/getting-started/Plan-Types.md diff --git a/docs/articles/expensify-classic/getting-started/Plan-Types.md b/docs/articles/expensify-classic/getting-started/Plan-Types.md deleted file mode 100644 index 4f8c52c2e1a1..000000000000 --- a/docs/articles/expensify-classic/getting-started/Plan-Types.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Plan Types -description: Learn which Expensify plan is the best fit for you ---- -# Overview -You can access comprehensive information about Expensify's plans and pricing by visiting www.expensify.com/pricing. Below, we provide an overview of each plan type to assist you in selecting the one that best suits your business or personal requirements. - -## Free Plan -The Free plan is suited for small businesses, offering a dedicated workspace for efficiently handling Expensify card management, expense reimbursement, invoicing, and bill payment. This plan includes unlimited receipt scanning for all users within the company and the potential to earn up to 1% cashback on card spending exceeding $25,000 per month (across all cards). - -## Collect Workspace Plan -The Collect Workspace Plan is designed with small companies in mind, providing essential features like a single layer of expense approvals, reimbursement capabilities, corporate card management, and basic integration options such as QuickBooks Online, QuickBooks Desktop, and Xero. This plan is ideal for those who require simple expense management functions. - -## Control Workspace Plan -Our most popular option, the Control Workspace plan, offers a heightened level of control and Workspace customization. With a Control Workspace, you gain access to multi-level approval workflows, comprehensive corporate card management, advanced accounting integration, tax tracking capabilities, and advanced expense rules that facilitate the enforcement of your internal expense policy. This plan provides a robust set of features for effective expense management. - -## Individual Track Plan -The Track plan is tailored for solo Expensify users who don't require expense submission to others. Individuals or sole proprietors can choose the Track plan to customize their Individual Workspace to align with their personal expense tracking requirements. - -## Individual Submit Plan -The Submit plan is designed for individuals who need to keep track of their expenses and share them with someone else, such as their boss, accountant, or even a housemate. It's specifically tailored for single users who want to both track and submit their expenses efficiently. - -{% include faq-begin.md %} - -## How can I change Individual plans? -You have the flexibility to switch between a Track and Submit plan, or vice versa, at any time by navigating to **Settings > Workspaces > Individual > *Workspace Name* > Plan**. This allows you to adapt your expense management approach as needed. - -## How can I upgrade Group plans? -You can easily upgrade from a Collect to a Control plan at any time by going to **Settings > Workspaces > Group > *Workspace Name* > Plan**. However, it's important to note that if you have an active Annual Subscription, downgrading from Control to Collect is not possible until your current commitment period expires. - -## How does pricing work if I have two types of Group Workspace plans? -If you have a Control and Collect Workspace, you will be charged at the Control Workspace rate. - -{% include faq-end.md %} From 2ff79c0afc5bf287b8ae9eeae04029bf0b910e6f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 24 Jan 2024 10:10:34 +0100 Subject: [PATCH 079/111] Replace nullish coalescing with logical or --- src/components/StatePicker/StateSelectorModal.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index 5be88a77f887..cc6f88617907 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -82,14 +82,18 @@ function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStat testID={StateSelectorModal.displayName} > Date: Wed, 24 Jan 2024 15:16:15 +0530 Subject: [PATCH 080/111] removed comment --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 -- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 6292a2e3c412..6b96ee657d65 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -31,8 +31,6 @@ import type {PersonalDetails, Report} from '@src/types/onyx'; import type {Note} from '@src/types/onyx/Report'; type PrivateNotesEditPageOnyxProps = { - /* Onyx Props */ - /** All of the personal details for everyone */ personalDetailsList: OnyxCollection; }; diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 30bd90bed5b6..d7fb1f6497be 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -17,8 +17,6 @@ import ROUTES from '@src/ROUTES'; import type {PersonalDetails, Report, Session} from '@src/types/onyx'; type PrivateNotesListPageOnyxProps = { - /* Onyx Props */ - /** All of the personal details for everyone */ personalDetailsList: OnyxCollection; From 7617114e87e97f4c7a4dd0cea0ba2b82155cba92 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 24 Jan 2024 11:30:59 +0100 Subject: [PATCH 081/111] Remove outdated TODO --- src/components/StatePicker/StateSelectorModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index cc6f88617907..798d3be7a698 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -89,7 +89,6 @@ function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStat onBackButtonPress={onClose} /> Date: Wed, 24 Jan 2024 16:11:26 +0300 Subject: [PATCH 082/111] clean tag for old money confirmation list --- src/components/MoneyRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 590154b48bca..d967d04ab94b 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -758,7 +758,7 @@ function MoneyRequestConfirmationList(props) { {shouldShowTags && ( { From 50c6a645d7c0428c45ade406a6805016a09ab821 Mon Sep 17 00:00:00 2001 From: Conor Pendergrast Date: Thu, 25 Jan 2024 02:49:38 +1300 Subject: [PATCH 083/111] Update Set-Up-the-Card-for-Your-Company.md --- .../expensify-card/Set-Up-the-Card-for-Your-Company.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md index 464f2129d800..4228c4f8618c 100644 --- a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md +++ b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md @@ -58,7 +58,7 @@ Applying for or using the Expensify Card will never have any positive or negativ ## How much does the Expensify Card cost? -The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription. +The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription (based on how much of your approved spend happens on the Expensify Card compared with reimbursable spend in each month). ## If I have staff outside the US, can they use the Expensify Card? From f1352d3f0b17947d30a86f5acfc038d9ab91dd45 Mon Sep 17 00:00:00 2001 From: Conor Pendergrast Date: Thu, 25 Jan 2024 02:52:52 +1300 Subject: [PATCH 084/111] Update Receipt-Breakdown.md --- .../billing-and-subscriptions/Receipt-Breakdown.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md index d4181735298e..dad0b5fbb6c5 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md @@ -16,8 +16,7 @@ Your receipt is broken up into multiple sections that include: The top section will show the total amount you paid as the billing owner of Expensify workspaces and give you a breakdown of price per member. Every member of your workspace(s) gets to store data, review data, and access free features like Expensify Chat. Thus, we show the total price and then use all of the members across all of the workspaces you own to calculate the price per member. Further down in the receipt, and in this article, we break down the members who generated billable activity. ## How-to reduce your bill and get paid to use Expensify -Chances are you can actually get paid to use Expensify with the Expensify Card. In this section of the receipt, we outline how much money you're leaving on the table by not using the Expensify Card. You can click `Get started` to connect with your account manager (if you have one) or Concierge, both of whom can help get you started with the card. - +Chances are you can actually get paid to use Expensify with the Expensify Card. In this section of the receipt, we outline how much money you're leaving on the table by not using the Expensify Card. You can click `Get started` to connect with your account manager (if you have one) or Concierge, both of whom can help get you started with the card. _Note: Currently, we offer Expensify Cards to companies with USD bank accounts._ ## How-to understand your billing breakdown @@ -37,9 +36,9 @@ Your receipt will have a detailed breakdown of activity and discounts across all - [Number of] Free members @ $0.00 - All members across any of your Free workspaces. - X% Expensify Card discount with $Y spend - - This shows the % discount you're getting based on total spend across your Expensify Cards. This is only available in the US. + - This shows the % discount you're getting based on total approved spend across your Expensify Cards. This is only available in the US. - X% Expensify Card cash back credit for $Y spend - - The amount of cash back you've earned based on total spend across your Expensify Cards. This is only available in the US. + - The amount of cash back you've earned based on total approved spend across your Expensify Cards. This is only available in the US. - 50% ExpensifyApproved! partner discount - If you're part of an accounting firm, you get an additional discount for being our partner. [Learn more about our ExpensifyApproved! accountants program.](https://use.expensify.com/accountants-program) - Total From e911ee0611d8a51a1adf7d81a0e12365fbde9bc3 Mon Sep 17 00:00:00 2001 From: Conor Pendergrast Date: Thu, 25 Jan 2024 02:54:39 +1300 Subject: [PATCH 085/111] Update Receipt-Breakdown.md --- .../billing-and-subscriptions/Receipt-Breakdown.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md index dad0b5fbb6c5..65618d816eff 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md @@ -16,7 +16,8 @@ Your receipt is broken up into multiple sections that include: The top section will show the total amount you paid as the billing owner of Expensify workspaces and give you a breakdown of price per member. Every member of your workspace(s) gets to store data, review data, and access free features like Expensify Chat. Thus, we show the total price and then use all of the members across all of the workspaces you own to calculate the price per member. Further down in the receipt, and in this article, we break down the members who generated billable activity. ## How-to reduce your bill and get paid to use Expensify -Chances are you can actually get paid to use Expensify with the Expensify Card. In this section of the receipt, we outline how much money you're leaving on the table by not using the Expensify Card. You can click `Get started` to connect with your account manager (if you have one) or Concierge, both of whom can help get you started with the card. +Chances are you can actually get paid to use Expensify with the Expensify Card. In this section of the receipt, we outline how much money you're leaving on the table by not using the Expensify Card. You can click `Get started` to connect with your account manager (if you have one) or Concierge, both of whom can help get you started with the card. + _Note: Currently, we offer Expensify Cards to companies with USD bank accounts._ ## How-to understand your billing breakdown From 3f8930c150b0f06cd980f0aa3e07dc079a8a7430 Mon Sep 17 00:00:00 2001 From: Conor Pendergrast Date: Thu, 25 Jan 2024 02:57:45 +1300 Subject: [PATCH 086/111] Update Set-Up-the-Card-for-Your-Company.md --- .../expensify-card/Set-Up-the-Card-for-Your-Company.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md index 4228c4f8618c..531648b10350 100644 --- a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md +++ b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md @@ -58,7 +58,7 @@ Applying for or using the Expensify Card will never have any positive or negativ ## How much does the Expensify Card cost? -The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription (based on how much of your approved spend happens on the Expensify Card compared with reimbursable spend in each month). +The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription (based on how much of your approved spend happens on the Expensify Card, compared with other approved spend, in each month). ## If I have staff outside the US, can they use the Expensify Card? From 5547b80395cc53c20623b67854471f0d610db0e0 Mon Sep 17 00:00:00 2001 From: Conor Pendergrast Date: Thu, 25 Jan 2024 03:05:38 +1300 Subject: [PATCH 087/111] Update Receipt-Breakdown.md --- .../billing-and-subscriptions/Receipt-Breakdown.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md index 65618d816eff..fd137aab62fb 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md @@ -25,7 +25,7 @@ Your receipt will have a detailed breakdown of activity and discounts across all - [Number of] Inactive workspace members @ $0.00 - All inactive members from any of your workspaces. - [Number of] Chat-only members @ $0.00 - - Any workspace members who chatted but didn't generate any other billable activity. Learn more about [chatting for free.](https://help.expensify.com/articles/new-expensify/getting-started/chat/Everything-About-Chat) + - Any workspace members who chatted but didn't generate any other billable activity. Learn more about [chatting for free.](https://help.expensify.com/articles/new-expensify/chat/Introducing-Expensify-Chat) - [Number of] Annual Control members @ $18.00 - Any members included in your annual subscription on the Control plan. - [Number of] Pay-per-use Control members @ $36.00 From cf1ca25e405ca9accd8980f9f996e6a9294a0c96 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:42:53 -0500 Subject: [PATCH 088/111] Delete docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md Moving this info to a new section of the help site / deleting for now. --- ...nue-Share-For-Expensify-Approved-Partners.md | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md diff --git a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md b/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md deleted file mode 100644 index 189ff671b213..000000000000 --- a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Expensify Card revenue share for ExpensifyApproved! partners -description: Earn money when your clients adopt the Expensify Card -redirect_from: articles/other/Card-Revenue-Share-for-ExpensifyApproved!-Partners/ ---- - - -Start making more with us! We're thrilled to announce a new incentive for our US-based ExpensifyApproved! partner accountants. You can now earn additional income for your firm every time your client uses their Expensify Card. **In short, your firm gets 0.5% of your clients’ total Expensify Card spend as cash back**. The more your clients spend, the more cashback your firm receives!
-
This program is currently only available to US-based ExpensifyApproved! partner accountants. - -# How-to -To benefit from this program, all you need to do is ensure that you are listed as a domain admin on your client's Expensify account. If you're not currently a domain admin, your client can follow the instructions outlined in [our help article](https://community.expensify.com/discussion/5749/how-to-add-and-remove-domain-admins#:~:text=Domain%20Admins%20have%20total%20control,a%20member%20of%20the%20domain.) to assign you this role. -{% include faq-begin.md %} -- What if my firm is not permitted to accept revenue share from our clients?
-
We understand that different firms may have different policies. If your firm is unable to accept this revenue share, you can pass the revenue share back to your client to give them an additional 0.5% of cash back using your own internal payment tools.

-- What if my firm does not wish to participate in the program?
-
Please reach out to your assigned partner manager at new.expensify.com to inform them you would not like to accept the revenue share nor do you want to pass the revenue share to your clients. \ No newline at end of file From 18a10b909d3a122ff35015b372cd5a514aa974e9 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 24 Jan 2024 18:16:07 +0100 Subject: [PATCH 089/111] Fix that user wasn't able to focus errors in the form --- src/components/Form/FormProvider.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index b71b611e60e5..424fd989291a 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -234,11 +234,9 @@ function FormProvider( ...inputProps, ref: typeof inputRef === 'function' - ? (node) => { + ? (node: BaseInputProps) => { inputRef(node); - if (node && typeof newRef !== 'function') { - newRef.current = node; - } + newRef.current = node; } : newRef, inputID, From 0ce476092a6efa36fba45983869064299c6e6b0f Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Wed, 24 Jan 2024 11:57:02 -0700 Subject: [PATCH 090/111] Update copy --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 46d9e90e84d9..8a959b5da550 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -583,7 +583,7 @@ export default { canceled: 'Canceled', posted: 'Posted', deleteReceipt: 'Delete receipt', - receiptScanning: 'Receipt scan in progress…', + receiptScanning: 'Scan in progress…', receiptMissingDetails: 'Receipt missing details', receiptStatusTitle: 'Scanning…', receiptStatusText: "Only you can see this receipt when it's scanning. Check back later or enter the details now.", diff --git a/src/languages/es.ts b/src/languages/es.ts index db010d3266c2..271e564c9b1f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -575,7 +575,7 @@ export default { canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', - receiptScanning: 'Escaneo de recibo en curso…', + receiptScanning: 'Escaneo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', receiptStatusTitle: 'Escaneando…', receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.', From 25b802e9c3ad886c2255dc286ff01ad6d11c3687 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 22:22:16 +0200 Subject: [PATCH 091/111] Default shouldShowLoading to false in WorkspacePageWithSections --- src/pages/workspace/WorkspacePageWithSections.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 8817f813a990..46fa8f14fac7 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -84,7 +84,7 @@ function WorkspacePageWithSections({ shouldUseScrollView = false, shouldSkipVBBACall = false, user, - shouldShowLoading = true, + shouldShowLoading = false, }: WorkspacePageWithSectionsProps) { const styles = useThemeStyles(); useNetwork({onReconnect: () => fetchData(shouldSkipVBBACall)}); From ee1b7c826c6e5c75cfb6035f192ce6fd1641d858 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 24 Jan 2024 15:33:34 -0500 Subject: [PATCH 092/111] Revert "Merge pull request #34237 from FitseTLT/fix-include-current-user-on-members-list" This reverts commit 5d41d84a6a24910affaf224351971991a177e034, reversing changes made to c0fbb9798af00257b3d2fb504be63d797a622087. --- src/libs/ReportUtils.ts | 146 ++++++++++++---------------- src/pages/ReportParticipantsPage.js | 3 +- src/pages/home/HeaderView.js | 2 +- 3 files changed, 66 insertions(+), 85 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1d888b087e53..e9c3b1710cc0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1460,84 +1460,6 @@ function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry = return workspaceIcon; } -/** - * Checks if a report is a group chat. - * - * A report is a group chat if it meets the following conditions: - * - Not a chat thread. - * - Not a task report. - * - Not a money request / IOU report. - * - Not an archived room. - * - Not a public / admin / announce chat room (chat type doesn't match any of the specified types). - * - More than 1 participant (note that participantAccountIDs excludes the current user). - * - */ -function isGroupChat(report: OnyxEntry): boolean { - return Boolean( - report && - !isChatThread(report) && - !isTaskReport(report) && - !isMoneyRequestReport(report) && - !isArchivedRoom(report) && - !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && - (report.participantAccountIDs?.length ?? 0) > 1, - ); -} - -function getGroupChatParticipantIDs(participants: number[]): number[] { - return [...new Set([...participants, ...(currentUserAccountID ? [currentUserAccountID] : [])])]; -} - -/** - * Returns an array of the participants Ids of a report - * - * @deprecated Use getVisibleMemberIDs instead - */ -function getParticipantsIDs(report: OnyxEntry): number[] { - if (!report) { - return []; - } - - const participants = report.participantAccountIDs ?? []; - - // Build participants list for IOU/expense reports - if (isMoneyRequestReport(report)) { - const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...participants].filter(Boolean) as number[]; - const onlyUnique = [...new Set([...onlyTruthyValues])]; - return onlyUnique; - } - - if (isGroupChat(report)) { - return getGroupChatParticipantIDs(participants); - } - - return participants; -} - -/** - * Returns an array of the visible member accountIDs for a report - */ -function getVisibleMemberIDs(report: OnyxEntry): number[] { - if (!report) { - return []; - } - - const visibleChatMemberAccountIDs = report.visibleChatMemberAccountIDs ?? []; - - // Build participants list for IOU/expense reports - if (isMoneyRequestReport(report)) { - const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...visibleChatMemberAccountIDs].filter(Boolean) as number[]; - const onlyUnique = [...new Set([...onlyTruthyValues])]; - return onlyUnique; - } - - if (isGroupChat(report)) { - return getGroupChatParticipantIDs(visibleChatMemberAccountIDs); - } - - return visibleChatMemberAccountIDs; -} - /** * Returns the appropriate icons for the given chat report using the stored personalDetails. * The Avatar sources can be URLs or Icon components according to the chat type. @@ -1654,10 +1576,6 @@ function getIcons( return isPayer ? [managerIcon, ownerIcon] : [ownerIcon, managerIcon]; } - if (isGroupChat(report)) { - return getIconsForParticipants(getVisibleMemberIDs(report), personalDetails); - } - return getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails); } @@ -4449,6 +4367,46 @@ function getTaskAssigneeChatOnyxData( }; } +/** + * Returns an array of the participants Ids of a report + * + * @deprecated Use getVisibleMemberIDs instead + */ +function getParticipantsIDs(report: OnyxEntry): number[] { + if (!report) { + return []; + } + + const participants = report.participantAccountIDs ?? []; + + // Build participants list for IOU/expense reports + if (isMoneyRequestReport(report)) { + const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...participants].filter(Boolean) as number[]; + const onlyUnique = [...new Set([...onlyTruthyValues])]; + return onlyUnique; + } + return participants; +} + +/** + * Returns an array of the visible member accountIDs for a report* + */ +function getVisibleMemberIDs(report: OnyxEntry): number[] { + if (!report) { + return []; + } + + const visibleChatMemberAccountIDs = report.visibleChatMemberAccountIDs ?? []; + + // Build participants list for IOU/expense reports + if (isMoneyRequestReport(report)) { + const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...visibleChatMemberAccountIDs].filter(Boolean) as number[]; + const onlyUnique = [...new Set([...onlyTruthyValues])]; + return onlyUnique; + } + return visibleChatMemberAccountIDs; +} + /** * Return iou report action display message */ @@ -4505,6 +4463,30 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) }); } +/** + * Checks if a report is a group chat. + * + * A report is a group chat if it meets the following conditions: + * - Not a chat thread. + * - Not a task report. + * - Not a money request / IOU report. + * - Not an archived room. + * - Not a public / admin / announce chat room (chat type doesn't match any of the specified types). + * - More than 2 participants. + * + */ +function isGroupChat(report: OnyxEntry): boolean { + return Boolean( + report && + !isChatThread(report) && + !isTaskReport(report) && + !isMoneyRequestReport(report) && + !isArchivedRoom(report) && + !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && + (report.participantAccountIDs?.length ?? 0) > 2, + ); +} + function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report); } diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index 65238fd5ea8c..7dbc1c7036c4 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -100,8 +100,7 @@ function ReportParticipantsPage(props) { 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant); From 1ba471fca14b8ff5a6a8a59991233afd5c393fd2 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 22:43:13 +0200 Subject: [PATCH 093/111] revert bad initial change --- src/pages/workspace/WorkspacePageWithSections.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 46fa8f14fac7..67c31c5329a8 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -84,7 +84,7 @@ function WorkspacePageWithSections({ shouldUseScrollView = false, shouldSkipVBBACall = false, user, - shouldShowLoading = false, + shouldShowLoading = true, }: WorkspacePageWithSectionsProps) { const styles = useThemeStyles(); useNetwork({onReconnect: () => fetchData(shouldSkipVBBACall)}); @@ -115,6 +115,10 @@ function WorkspacePageWithSections({ return !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy); }, [policy]); + console.debug("CRISTI - isLoading: " + isLoading); + console.debug("CRISTI - firstRender.current: " + firstRender.current); + console.debug("CRISTI - shouldShowLoading: " + shouldShowLoading); + return ( Date: Wed, 24 Jan 2024 22:54:11 +0200 Subject: [PATCH 094/111] Fix deploy blocker --- src/pages/workspace/WorkspacePageWithSections.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 67c31c5329a8..d516f024aef0 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -79,7 +79,7 @@ function WorkspacePageWithSections({ guidesCallTaskID = '', headerText, policy, - reimbursementAccount = {}, + reimbursementAccount = {isLoading: false}, route, shouldUseScrollView = false, shouldSkipVBBACall = false, From fd6abead14a08df9d8d7814fc758d4476789db12 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 22:56:08 +0200 Subject: [PATCH 095/111] Remove console debug --- src/pages/workspace/WorkspacePageWithSections.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index d516f024aef0..c9c371a6a0c7 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -115,10 +115,6 @@ function WorkspacePageWithSections({ return !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy); }, [policy]); - console.debug("CRISTI - isLoading: " + isLoading); - console.debug("CRISTI - firstRender.current: " + firstRender.current); - console.debug("CRISTI - shouldShowLoading: " + shouldShowLoading); - return ( Date: Wed, 24 Jan 2024 23:16:37 +0200 Subject: [PATCH 096/111] make currentStep option in ACHData --- src/types/onyx/ReimbursementAccount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts index c0ade25e4d79..8ab10e79cb09 100644 --- a/src/types/onyx/ReimbursementAccount.ts +++ b/src/types/onyx/ReimbursementAccount.ts @@ -8,7 +8,7 @@ type BankAccountSubStep = ValueOf; type ACHData = { /** Step of the setup flow that we are on. Determines which view is presented. */ - currentStep: BankAccountStep; + currentStep?: BankAccountStep; /** Optional subStep we would like the user to start back on */ subStep?: BankAccountSubStep; From ba79d4e78db87dd9208d312796c1b47d226a9f8c Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 23:26:39 +0200 Subject: [PATCH 097/111] Use ReimbursementAccountProps.reimbursementAccountDefaultProps --- src/pages/workspace/WorkspacePageWithSections.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index c9c371a6a0c7..8b98d29245d5 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -22,6 +22,7 @@ import type {Policy, ReimbursementAccount, User} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; type WorkspacePageWithSectionsOnyxProps = { /** From Onyx */ @@ -79,7 +80,7 @@ function WorkspacePageWithSections({ guidesCallTaskID = '', headerText, policy, - reimbursementAccount = {isLoading: false}, + reimbursementAccount = ReimbursementAccountProps.reimbursementAccountDefaultProps, route, shouldUseScrollView = false, shouldSkipVBBACall = false, From 20f70c64b4aa61f6dacc0c90ad92e09eb68ee2b3 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 23:27:21 +0200 Subject: [PATCH 098/111] Run prettier --- src/pages/workspace/WorkspacePageWithSections.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 8b98d29245d5..7a4d9c1f4106 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -14,6 +14,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import BankAccount from '@libs/models/BankAccount'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -22,7 +23,6 @@ import type {Policy, ReimbursementAccount, User} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; -import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; type WorkspacePageWithSectionsOnyxProps = { /** From Onyx */ From 9a9ccab2051f36d3d2a3cfde847976919b9db4aa Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 23:46:38 +0200 Subject: [PATCH 099/111] Use report.reportID when updating notification preferences --- src/pages/settings/Report/NotificationPreferencePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Report/NotificationPreferencePage.js b/src/pages/settings/Report/NotificationPreferencePage.js index 488afa7c5a71..307a539942c7 100644 --- a/src/pages/settings/Report/NotificationPreferencePage.js +++ b/src/pages/settings/Report/NotificationPreferencePage.js @@ -43,7 +43,7 @@ function NotificationPreferencePage(props) { /> Report.updateNotificationPreference(props.reportID, props.report.notificationPreference, option.value, true, undefined, undefined, props.report)} + onSelectRow={(option) => Report.updateNotificationPreference(props.report.reportID, props.report.notificationPreference, option.value, true, undefined, undefined, props.report)} initiallyFocusedOptionKey={_.find(notificationPreferenceOptions, (locale) => locale.isSelected).keyForList} /> From 00abcd8e8b41caea4d6ffec0add0d532f023bbe0 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 23:59:22 +0200 Subject: [PATCH 100/111] Run prettier --- src/pages/settings/Report/NotificationPreferencePage.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Report/NotificationPreferencePage.js b/src/pages/settings/Report/NotificationPreferencePage.js index 307a539942c7..c6044bd81efe 100644 --- a/src/pages/settings/Report/NotificationPreferencePage.js +++ b/src/pages/settings/Report/NotificationPreferencePage.js @@ -43,7 +43,9 @@ function NotificationPreferencePage(props) { /> Report.updateNotificationPreference(props.report.reportID, props.report.notificationPreference, option.value, true, undefined, undefined, props.report)} + onSelectRow={(option) => + Report.updateNotificationPreference(props.report.reportID, props.report.notificationPreference, option.value, true, undefined, undefined, props.report) + } initiallyFocusedOptionKey={_.find(notificationPreferenceOptions, (locale) => locale.isSelected).keyForList} /> From 149eda177f73fd9e6c0c214652d8a6533d543638 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 24 Jan 2024 22:01:11 +0000 Subject: [PATCH 101/111] Update version to 1.4.31-5 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 759abc31c058..ac2399b76b00 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043104 - versionName "1.4.31-4" + versionCode 1001043105 + versionName "1.4.31-5" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 31e13ef3d283..c3fe8e5b3e84 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.4 + 1.4.31.5 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 557832679cd6..3fe876e853a9 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.31.4 + 1.4.31.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 6d1c222e3ab9..4df7a4feae72 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.31 CFBundleVersion - 1.4.31.4 + 1.4.31.5 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index be3f8f18bff3..471e9e8dd439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-4", + "version": "1.4.31-5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-4", + "version": "1.4.31-5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 255261713c0a..01838db6bcf4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-4", + "version": "1.4.31-5", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 08e9276da00387cc2352f5a748bec8175ecf526f Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 24 Jan 2024 22:51:36 +0000 Subject: [PATCH 102/111] Update version to 1.4.31-6 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ac2399b76b00..ae418094ace6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043105 - versionName "1.4.31-5" + versionCode 1001043106 + versionName "1.4.31-6" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index c3fe8e5b3e84..9b1906646e2d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.5 + 1.4.31.6 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3fe876e853a9..78a6f4c0f922 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.31.5 + 1.4.31.6 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 4df7a4feae72..91aa2b889fe7 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.31 CFBundleVersion - 1.4.31.5 + 1.4.31.6 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 471e9e8dd439..ab9d3f9c6967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-5", + "version": "1.4.31-6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-5", + "version": "1.4.31-6", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 01838db6bcf4..271909ef3f65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-5", + "version": "1.4.31-6", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From a207382c11e2b8699d907b25fff38765c160dc0e Mon Sep 17 00:00:00 2001 From: Sophie Pinto-Raetz <42940078+sophiepintoraetz@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:49:56 +1300 Subject: [PATCH 103/111] Update Set-Up-the-Card-for-Your-Company.md --- .../expensify-card/Set-Up-the-Card-for-Your-Company.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md index 531648b10350..1cf29531f696 100644 --- a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md +++ b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md @@ -58,7 +58,7 @@ Applying for or using the Expensify Card will never have any positive or negativ ## How much does the Expensify Card cost? -The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription (based on how much of your approved spend happens on the Expensify Card, compared with other approved spend, in each month). +The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription (based on how much of your approved spend occurs on the Expensify Card, compared with other approved spend, in each month). ## If I have staff outside the US, can they use the Expensify Card? From bb2e0098b7bf4d213bc193ef010a8873a20acd00 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 25 Jan 2024 01:56:08 +0000 Subject: [PATCH 104/111] Update version to 1.4.31-7 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ae418094ace6..1872168bc2a0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043106 - versionName "1.4.31-6" + versionCode 1001043107 + versionName "1.4.31-7" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 9b1906646e2d..a977be3acc70 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.6 + 1.4.31.7 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 78a6f4c0f922..2fb6a26d633d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.31.6 + 1.4.31.7 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 91aa2b889fe7..d438940a2d1b 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.31 CFBundleVersion - 1.4.31.6 + 1.4.31.7 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index ab9d3f9c6967..2bfec0ea4577 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-6", + "version": "1.4.31-7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-6", + "version": "1.4.31-7", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 271909ef3f65..7f09c49ad2bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-6", + "version": "1.4.31-7", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From d64895f0258d260a9f0f6b96fda27a65c572f54c Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Wed, 24 Jan 2024 22:30:34 -0600 Subject: [PATCH 105/111] fix: show parent action message or deleted message when original message doesn't exist --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e9c3b1710cc0..fa51ca06e68e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2336,7 +2336,7 @@ function isChangeLogObject(originalMessage?: ChangeLog): ChangeLog | undefined { */ function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | Record, parentReportActionMessage: string) { if (!parentReportAction?.originalMessage) { - return ''; + return parentReportActionMessage || Localize.translateLocal('parentReportAction.deletedMessage'); } const originalMessage = isChangeLogObject(parentReportAction.originalMessage); const participantAccountIDs = originalMessage?.targetAccountIDs ?? []; From 999d576e8673171cc4bde83d5ae0c662db7cb57a Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Wed, 24 Jan 2024 23:03:57 -0600 Subject: [PATCH 106/111] fix: create child report with parent's policy name --- src/libs/actions/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 36ac445a78d4..020c22cefae0 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -754,7 +754,7 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: P parentReport?.policyID ?? CONST.POLICY.OWNER_EMAIL_FAKE, CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, false, - '', + parentReport?.policyName ?? '', undefined, undefined, ReportUtils.getChildReportNotificationPreference(parentReportAction), From 71fa6ac36742ef9c60baed78318c8af2aa7dc62b Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 25 Jan 2024 08:39:41 +0000 Subject: [PATCH 107/111] Update version to 1.4.32-0 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 4 ++-- ios/NewExpensifyTests/Info.plist | 4 ++-- ios/NotificationServiceExtension/Info.plist | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1872168bc2a0..e1c404a73d4b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043107 - versionName "1.4.31-7" + versionCode 1001043200 + versionName "1.4.32-0" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index a977be3acc70..7092d3239ae3 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.31 + 1.4.32 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.7 + 1.4.32.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 2fb6a26d633d..a402788ad579 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.31 + 1.4.32 CFBundleSignature ???? CFBundleVersion - 1.4.31.7 + 1.4.32.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index d438940a2d1b..58a90b1796e9 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -3,9 +3,9 @@ CFBundleShortVersionString - 1.4.31 + 1.4.32 CFBundleVersion - 1.4.31.7 + 1.4.32.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 2bfec0ea4577..ad9754c4184b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-7", + "version": "1.4.32-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-7", + "version": "1.4.32-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7f09c49ad2bb..ad48cc7093b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-7", + "version": "1.4.32-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 22f73e22300f70e9fbe2fbb67bd0d3d59898a4ee Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 25 Jan 2024 15:43:44 +0700 Subject: [PATCH 108/111] change to default export --- src/components/ImageView/types.ts | 2 +- src/components/MultiGestureCanvas/types.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index 9ea51fd3c82c..bf83bc44d47b 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -1,5 +1,5 @@ import type {StyleProp, ViewStyle} from 'react-native'; -import type {ZoomRange} from '@components/MultiGestureCanvas/types'; +import type ZoomRange from '@components/MultiGestureCanvas/types'; type ImageViewProps = { /** Whether source url requires authentication */ diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 3c8480257700..0242f045feef 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -3,5 +3,4 @@ type ZoomRange = { max: number; }; -// eslint-disable-next-line import/prefer-default-export -export type {ZoomRange}; +export default ZoomRange; From cccb89a12b2f6a901a6326fe9f17c9646f54e4cf Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 25 Jan 2024 09:16:22 +0000 Subject: [PATCH 109/111] Update version to 1.4.32-1 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index e1c404a73d4b..6d3168382073 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043200 - versionName "1.4.32-0" + versionCode 1001043201 + versionName "1.4.32-1" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7092d3239ae3..6e62db6dea30 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.32.0 + 1.4.32.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index a402788ad579..45e0b42db439 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.32.0 + 1.4.32.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 58a90b1796e9..2f0ce291dfc7 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.32 CFBundleVersion - 1.4.32.0 + 1.4.32.1 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index ad9754c4184b..5a774298babd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.32-0", + "version": "1.4.32-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.32-0", + "version": "1.4.32-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ad48cc7093b9..055958b9a375 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.32-0", + "version": "1.4.32-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 2f91117eb6498de1687188047a7f406e7bdf9adf Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 25 Jan 2024 11:48:11 +0100 Subject: [PATCH 110/111] Remove ts-expect-error after merging main --- src/ONYXKEYS.ts | 4 ++-- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 8 +++----- src/types/onyx/Form.ts | 6 +++++- src/types/onyx/index.ts | 3 ++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2a535bab038c..7abf6db1769d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -524,8 +524,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: OnyxTypes.PrivateNotesForm; + [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM_DRAFT]: OnyxTypes.PrivateNotesForm; [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: OnyxTypes.IKnowATeacherForm; [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM_DRAFT]: OnyxTypes.IKnowATeacherForm; [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM]: OnyxTypes.IntroSchoolPrincipalForm; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 6b96ee657d65..805f2d8fcb90 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -5,13 +5,13 @@ import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; -import type {TextInput as TextInputRN} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; @@ -64,7 +64,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes ); // To focus on the input field when the page loads - const privateNotesInput = useRef(null); + const privateNotesInput = useRef(null); const focusTimeoutRef = useRef(null); useFocusEffect( @@ -114,7 +114,6 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes shouldShowBackButton onCloseButtonPress={() => Navigation.dismissModal()} /> - {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} { + ref={(el: AnimatedTextInputRef) => { if (!el) { return; } diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index f341cef8b7b8..c3bcec2a2d3b 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -50,6 +50,10 @@ type IntroSchoolPrincipalForm = Form<{ partnerUserID: string; }>; +type PrivateNotesForm = Form<{ + privateNotes: string; +}>; + export default Form; -export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, FormValueType, NewRoomForm, BaseForm, IKnowATeacherForm, IntroSchoolPrincipalForm}; +export type {AddDebitCardForm, DateOfBirthForm, PrivateNotesForm, DisplayNameForm, FormValueType, NewRoomForm, BaseForm, IKnowATeacherForm, IntroSchoolPrincipalForm}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index bceba6496851..5b04cae58671 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -9,7 +9,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm, PrivateNotesForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -150,4 +150,5 @@ export type { NewRoomForm, IKnowATeacherForm, IntroSchoolPrincipalForm, + PrivateNotesForm, }; From 6e2c58de92b81c754390a2dd9fc45e6a77557974 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 25 Jan 2024 11:01:58 +0000 Subject: [PATCH 111/111] Update version to 1.4.32-2 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 6d3168382073..e135d44eb834 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043201 - versionName "1.4.32-1" + versionCode 1001043202 + versionName "1.4.32-2" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 6e62db6dea30..c636ced8e7f9 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.32.1 + 1.4.32.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 45e0b42db439..ef1ef0d998d5 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.32.1 + 1.4.32.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 2f0ce291dfc7..16439b1d24d9 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.32 CFBundleVersion - 1.4.32.1 + 1.4.32.2 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 5a774298babd..543a1366f8d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.32-1", + "version": "1.4.32-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.32-1", + "version": "1.4.32-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 055958b9a375..8ceac3912660 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.32-1", + "version": "1.4.32-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",