From 8ffcb6b472082f123a12cf0d5cecf45cc6566868 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 3 Jun 2024 20:26:36 +0100 Subject: [PATCH 001/124] Reapply "Merge pull request #35045 from callstack-internal/issues/30123" This reverts commit ad202800542b480ab212b5acabe88b73c2834a7f. --- src/CONST.ts | 1 + src/components/AddressSearch/index.tsx | 61 ++++++++++++---- .../listViewOverflow/index.native.ts | 4 + .../AddressSearch/listViewOverflow/index.ts | 4 + src/components/AddressSearch/types.ts | 11 ++- src/components/Form/FormProvider.tsx | 3 + src/components/Form/FormWrapper.tsx | 14 ++-- src/hooks/useSubmitButtonVisibility.native.ts | 33 +++++++++ src/hooks/useSubmitButtonVisibility.ts | 58 +++++++++++++++ src/libs/actions/Transaction.ts | 2 +- .../request/step/IOURequestStepWaypoint.tsx | 73 ++++++++++--------- src/styles/index.ts | 1 - src/styles/utils/sizing.ts | 3 + 13 files changed, 212 insertions(+), 56 deletions(-) create mode 100644 src/components/AddressSearch/listViewOverflow/index.native.ts create mode 100644 src/components/AddressSearch/listViewOverflow/index.ts create mode 100644 src/hooks/useSubmitButtonVisibility.native.ts create mode 100644 src/hooks/useSubmitButtonVisibility.ts diff --git a/src/CONST.ts b/src/CONST.ts index bdf5463031f7..26d481564870 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -73,6 +73,7 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { + RECENT_WAYPOINTS_NUMBER: 20, DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], // Note: Group and Self-DM excluded as these are not tied to a Workspace diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 17a2f6212447..d1dc42bb4678 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -23,7 +23,25 @@ import CONST from '@src/CONST'; import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import CurrentLocationButton from './CurrentLocationButton'; import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer'; -import type {AddressSearchProps} from './types'; +import listViewOverflow from './listViewOverflow'; +import type {AddressSearchProps, PredefinedPlace} from './types'; + +/** + * Check if the place matches the search by the place name or description. + * @param search The search string for a place + * @param place The place to check for a match on the search + * @returns true if search is related to place, otherwise it returns false. + */ +function isPlaceMatchForSearch(search: string, place: PredefinedPlace): boolean { + if (!search) { + return true; + } + if (!place) { + return false; + } + const fullSearchSentence = `${place.name ?? ''} ${place.description}`; + return search.split(' ').every((searchTerm) => !searchTerm || (searchTerm && fullSearchSentence.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()))); +} // The error that's being thrown below will be ignored until we fork the // react-native-google-places-autocomplete repo and replace the @@ -41,6 +59,7 @@ function AddressSearch( isLimitedToUSA = false, label, maxInputLength, + onFocus, onBlur, onInputChange, onPress, @@ -298,10 +317,16 @@ function AddressSearch( }; }, []); + const filteredPredefinedPlaces = useMemo(() => { + if (!isOffline || !searchValue) { + return predefinedPlaces ?? []; + } + return predefinedPlaces?.filter((predefinedPlace) => isPlaceMatchForSearch(searchValue, predefinedPlace)) ?? []; + }, [isOffline, predefinedPlaces, searchValue]); + const listEmptyComponent = useCallback( - () => - !!isOffline || !isTyping ? null : {translate('common.noResultsFound')}, - [isOffline, isTyping, styles, translate], + () => (!isTyping ? null : {translate('common.noResultsFound')}), + [isTyping, styles, translate], ); const listLoader = useCallback( @@ -338,11 +363,10 @@ function AddressSearch( ref={containerRef} > - {!!title && {title}} + {!!title && {title}} {subtitle} ); @@ -385,6 +409,7 @@ function AddressSearch( shouldSaveDraft, onFocus: () => { setIsFocused(true); + onFocus?.(); }, onBlur: (event) => { if (!isCurrentTargetInsideContainer(event, containerRef)) { @@ -414,10 +439,18 @@ function AddressSearch( }} styles={{ textInputContainer: [styles.flexColumn], - listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.overflowAuto, styles.borderLeft, styles.borderRight, !isFocused && {height: 0}], + listView: [ + StyleUtils.getGoogleListViewStyle(displayListViewBorder), + listViewOverflow, + styles.borderLeft, + styles.borderRight, + styles.flexGrow0, + !isFocused && styles.h0, + ], row: [styles.pv4, styles.ph3, styles.overflowAuto], description: [styles.googleSearchText], separator: [styles.googleSearchSeparator], + container: [styles.mh100], }} numberOfLines={2} isRowScrollable={false} @@ -441,11 +474,13 @@ function AddressSearch( ) } placeholder="" - /> - setLocationErrorCode(null)} - locationErrorCode={locationErrorCode} - /> + listViewDisplayed + > + setLocationErrorCode(null)} + locationErrorCode={locationErrorCode} + /> + {isFetchingCurrentLocation && } diff --git a/src/components/AddressSearch/listViewOverflow/index.native.ts b/src/components/AddressSearch/listViewOverflow/index.native.ts new file mode 100644 index 000000000000..36b9f4005376 --- /dev/null +++ b/src/components/AddressSearch/listViewOverflow/index.native.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line no-restricted-imports +import {defaultStyles} from '@styles/index'; + +export default defaultStyles.overflowHidden; diff --git a/src/components/AddressSearch/listViewOverflow/index.ts b/src/components/AddressSearch/listViewOverflow/index.ts new file mode 100644 index 000000000000..ae8bf35cc80c --- /dev/null +++ b/src/components/AddressSearch/listViewOverflow/index.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line no-restricted-imports +import {defaultStyles} from '@styles/index'; + +export default defaultStyles.overflowAuto; diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index bc7acf3f7e40..22cc3834b7a9 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -24,6 +24,10 @@ type StreetValue = { street: string; }; +type PredefinedPlace = Place & { + name?: string; +}; + type AddressSearchProps = { /** The ID used to uniquely identify the input in a Form */ inputID?: string; @@ -31,6 +35,9 @@ type AddressSearchProps = { /** Saves a draft of the input value when used in a form */ shouldSaveDraft?: boolean; + /** Callback that is called when the text input is focused */ + onFocus?: () => void; + /** Callback that is called when the text input is blurred */ onBlur?: () => void; @@ -65,7 +72,7 @@ type AddressSearchProps = { canUseCurrentLocation?: boolean; /** A list of predefined places that can be shown when the user isn't searching for something */ - predefinedPlaces?: Place[] | null; + predefinedPlaces?: PredefinedPlace[] | null; /** A map of inputID key names */ renamedInputKeys?: Address; @@ -85,4 +92,4 @@ type AddressSearchProps = { type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent, containerRef: RefObject) => boolean; -export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue}; +export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue, PredefinedPlace}; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 3d20f910dca0..b6699c0cf3f4 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -74,6 +74,9 @@ type FormProviderProps = FormProvider /** Whether to apply flex to the submit button */ submitFlexEnabled?: boolean; + + /** Whether the form container should grow or adapt to the viewable available space */ + shouldContainerGrow?: boolean; }; function FormProvider( diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 5c74fd466a15..e7c9dba5c77a 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,8 +1,8 @@ import React, {useCallback, useMemo, useRef} from 'react'; import type {RefObject} from 'react'; // eslint-disable-next-line no-restricted-imports -import type {ScrollView as RNScrollView, StyleProp, View, ViewStyle} from 'react-native'; -import {Keyboard} from 'react-native'; +import type {ScrollView as RNScrollView, StyleProp, ViewStyle} from 'react-native'; +import {Keyboard, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; @@ -32,6 +32,9 @@ type FormWrapperProps = ChildrenProps & /** Whether to apply flex to the submit button */ submitFlexEnabled?: boolean; + /** Whether the form container should grow or adapt to the viewable available space */ + shouldContainerGrow?: boolean; + /** Server side errors keyed by microtime */ errors: FormInputErrors; @@ -60,6 +63,7 @@ function FormWrapper({ scrollContextEnabled = false, shouldHideFixErrorsAlert = false, disablePressOnEnter = true, + shouldContainerGrow = true, }: FormWrapperProps) { const styles = useThemeStyles(); const formRef = useRef(null); @@ -104,7 +108,7 @@ function FormWrapper({ ref={formContentRef} style={[style, safeAreaPaddingBottomStyle.paddingBottom ? safeAreaPaddingBottomStyle : styles.pb5]} > - {children} + {children} {isSubmitButtonVisible && ( @@ -164,7 +168,7 @@ function FormWrapper({ ) : ( diff --git a/src/hooks/useSubmitButtonVisibility.native.ts b/src/hooks/useSubmitButtonVisibility.native.ts new file mode 100644 index 000000000000..a962b52ce1f0 --- /dev/null +++ b/src/hooks/useSubmitButtonVisibility.native.ts @@ -0,0 +1,33 @@ +import {useState} from 'react'; +import useSafeAreaInsets from './useSafeAreaInsets'; +import useThemeStyles from './useThemeStyles'; + +// Useful when there's a need to hide the submit button from FormProvider, +// to let form content fill the page when virtual keyboard is shown +function useSubmitButtonVisibility() { + const styles = useThemeStyles(); + const [isSubmitButtonVisible, setIsSubmitButtonVisible] = useState(true); + const {bottom} = useSafeAreaInsets(); + + const showSubmitButton = () => { + setIsSubmitButtonVisible(true); + }; + + const hideSubmitButton = () => { + setIsSubmitButtonVisible(false); + }; + + // When the submit button is hidden there's a need to manually + // add its bottom style to the FormProvider style prop, + // otherwise the form content will touch the bottom of the page/screen + const formStyle = !isSubmitButtonVisible && bottom === 0 && styles.mb5; + + return { + isSubmitButtonVisible, + showSubmitButton, + hideSubmitButton, + formStyle, + }; +} + +export default useSubmitButtonVisibility; diff --git a/src/hooks/useSubmitButtonVisibility.ts b/src/hooks/useSubmitButtonVisibility.ts new file mode 100644 index 000000000000..72f704edbe13 --- /dev/null +++ b/src/hooks/useSubmitButtonVisibility.ts @@ -0,0 +1,58 @@ +import {useEffect, useRef, useState} from 'react'; +import {Dimensions} from 'react-native'; +import useSafeAreaInsets from './useSafeAreaInsets'; +import useThemeStyles from './useThemeStyles'; +import useWindowDimensions from './useWindowDimensions'; + +// Useful when there's a need to hide the submit button from FormProvider, +// to let form content fill the page when virtual keyboard is shown +function useSubmitButtonVisibility() { + const styles = useThemeStyles(); + const {windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const [isSubmitButtonVisible, setIsSubmitButtonVisible] = useState(true); + const initialWindowHeightRef = useRef(windowHeight); + const isSmallScreenWidthRef = useRef(isSmallScreenWidth); + const {bottom} = useSafeAreaInsets(); + + // Web: the submit button is shown when the height of the window is the same or greater, + // otherwise it's hidden + useEffect(() => { + const dimensionsListener = Dimensions.addEventListener('change', ({window}) => { + if (!isSmallScreenWidthRef.current) { + return; + } + + if (window.height < initialWindowHeightRef.current) { + setIsSubmitButtonVisible(false); + return; + } + + setIsSubmitButtonVisible(true); + }); + + return () => dimensionsListener.remove(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Web: the submit button is only shown when the window height is the same or greater, + // so executing this function won't do anything + const showSubmitButton = () => {}; + + // Web: the submit button is only hidden when the window height becomes smaller, + // so executing this function won't do anything + const hideSubmitButton = () => {}; + + // When the submit button is hidden there's a need to manually + // add its bottom style to the FormProvider style prop, + // otherwise the form content will touch the bottom of the page/screen + const formStyle = !isSubmitButtonVisible && bottom === 0 && styles.mb5; + + return { + isSubmitButtonVisible, + showSubmitButton, + hideSubmitButton, + formStyle, + }; +} + +export default useSubmitButtonVisibility; diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 251c8d046562..ee306fd44fc8 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -113,7 +113,7 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp if (!recentWaypointAlreadyExists && waypoint !== null) { const clonedWaypoints = lodashClone(recentWaypoints); clonedWaypoints.unshift(waypoint); - Onyx.merge(ONYXKEYS.NVP_RECENT_WAYPOINTS, clonedWaypoints.slice(0, 5)); + Onyx.merge(ONYXKEYS.NVP_RECENT_WAYPOINTS, clonedWaypoints.slice(0, CONST.RECENT_WAYPOINTS_NUMBER)); } } diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx index 6c0eae98fb85..374e27819e70 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx @@ -1,7 +1,6 @@ import {useNavigation} from '@react-navigation/native'; import React, {useMemo, useRef, useState} from 'react'; import type {TextInput} from 'react-native'; -import {View} from 'react-native'; import type {Place} from 'react-native-google-places-autocomplete'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -17,6 +16,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useLocationBias from '@hooks/useLocationBias'; import useNetwork from '@hooks/useNetwork'; +import useSubmitButtonVisibility from '@hooks/useSubmitButtonVisibility'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -57,6 +57,7 @@ function IOURequestStepWaypoint({ }: IOURequestStepWaypointProps) { const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); + const {isSubmitButtonVisible, showSubmitButton, hideSubmitButton, formStyle} = useSubmitButtonVisibility(); const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false); const navigation = useNavigation(); const isFocused = navigation.isFocused(); @@ -157,6 +158,7 @@ function IOURequestStepWaypoint({ onEntryTransitionEnd={() => textInput.current?.focus()} shouldEnableMaxHeight testID={IOURequestStepWaypoint.displayName} + style={styles.overflowHidden} > - - { - textInput.current = e as unknown as TextInput; - }} - hint={!isOffline ? 'distance.error.selectSuggestedAddress' : ''} - containerStyles={[styles.mt4]} - label={translate('distance.address')} - defaultValue={waypointAddress} - onPress={selectWaypoint} - maxInputLength={CONST.FORM_CHARACTER_LIMIT} - renamedInputKeys={{ - address: `waypoint${pageIndex}`, - city: '', - country: '', - street: '', - street2: '', - zipCode: '', - lat: '', - lng: '', - state: '', - }} - predefinedPlaces={recentWaypoints} - resultTypes="" - /> - + { + textInput.current = e as unknown as TextInput; + }} + hint={!isOffline ? 'distance.error.selectSuggestedAddress' : ''} + containerStyles={[styles.mt4]} + label={translate('distance.address')} + defaultValue={waypointAddress} + onPress={selectWaypoint} + onFocus={hideSubmitButton} + onBlur={showSubmitButton} + maxInputLength={CONST.FORM_CHARACTER_LIMIT} + renamedInputKeys={{ + address: `waypoint${pageIndex}`, + city: '', + country: '', + street: '', + street2: '', + zipCode: '', + lat: '', + lng: '', + state: '', + }} + predefinedPlaces={recentWaypoints} + resultTypes="" + /> @@ -244,10 +249,10 @@ export default withWritableReportOrNotFound( recentWaypoints: { key: ONYXKEYS.NVP_RECENT_WAYPOINTS, - // Only grab the most recent 5 waypoints because that's all that is shown in the UI. This also puts them into the format of data + // Only grab the most recent 20 waypoints because that's all that is shown in the UI. This also puts them into the format of data // that the google autocomplete component expects for it's "predefined places" feature. selector: (waypoints) => - (waypoints ? waypoints.slice(0, 5) : []).map((waypoint) => ({ + (waypoints ? waypoints.slice(0, CONST.RECENT_WAYPOINTS_NUMBER) : []).map((waypoint) => ({ name: waypoint.name, description: waypoint.address ?? '', geometry: { diff --git a/src/styles/index.ts b/src/styles/index.ts index 9980a96f64ae..51820ff827f4 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3481,7 +3481,6 @@ const styles = (theme: ThemeColors) => fontSize: variables.fontSizeNormal, lineHeight: variables.fontSizeNormalHeight, fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, - flex: 1, }, searchPressable: { diff --git a/src/styles/utils/sizing.ts b/src/styles/utils/sizing.ts index bde2837f16ba..ba232ef99b81 100644 --- a/src/styles/utils/sizing.ts +++ b/src/styles/utils/sizing.ts @@ -6,6 +6,9 @@ import type {ViewStyle} from 'react-native'; * https://getbootstrap.com/docs/5.0/utilities/sizing/ */ export default { + h0: { + height: 0, + }, h100: { height: '100%', }, From e81675ecae5bc1df5dfb9f2cf59b64b988cc5f7f Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 3 Jun 2024 21:10:38 +0100 Subject: [PATCH 002/124] fix: unreachable form inputs --- src/components/Form/FormWrapper.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index e7c9dba5c77a..500f7d985599 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -108,7 +108,7 @@ function FormWrapper({ ref={formContentRef} style={[style, safeAreaPaddingBottomStyle.paddingBottom ? safeAreaPaddingBottomStyle : styles.pb5]} > - {children} + {shouldContainerGrow ? children : {children}} {isSubmitButtonVisible && ( Date: Mon, 3 Jun 2024 21:11:30 +0100 Subject: [PATCH 003/124] chore: disable typescript rule for constant --- src/pages/iou/request/step/IOURequestStepWaypoint.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx index 374e27819e70..2c2c27d2c497 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx @@ -252,6 +252,7 @@ export default withWritableReportOrNotFound( // Only grab the most recent 20 waypoints because that's all that is shown in the UI. This also puts them into the format of data // that the google autocomplete component expects for it's "predefined places" feature. selector: (waypoints) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument (waypoints ? waypoints.slice(0, CONST.RECENT_WAYPOINTS_NUMBER) : []).map((waypoint) => ({ name: waypoint.name, description: waypoint.address ?? '', From 12dc2c67d2213b144904b4c8aa8c8e26f092dbcb Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 6 Jun 2024 16:22:01 +0700 Subject: [PATCH 004/124] fix If we alter transaction details, new message green line disappears --- src/libs/actions/IOU.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 1bd4de43acfb..48f004f2f800 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2505,6 +2505,22 @@ function getUpdateMoneyRequestParams( [updatedReportAction.reportActionID]: updatedReportAction as OnyxTypes.ReportAction, }, }); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThread?.reportID}`, + value: { + lastVisibleActionCreated: updatedReportAction.created, + lastReadTime: updatedReportAction.created, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThread?.reportID}`, + value: { + lastVisibleActionCreated: transactionThread?.lastVisibleActionCreated, + lastReadTime: transactionThread?.lastReadTime, + }, + }); successData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`, From 838218d992a2d43547bd85de40bdf6b6c57ed30f Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 11 Jun 2024 19:46:03 +0100 Subject: [PATCH 005/124] fix: hiding submit button on address search screen --- src/components/AddressSearch/index.tsx | 1 + src/components/Form/FormProvider.tsx | 3 - src/components/Form/FormWrapper.tsx | 17 ++--- src/hooks/useSubmitButtonVisibility.native.ts | 33 --------- src/hooks/useSubmitButtonVisibility.ts | 58 --------------- .../request/step/IOURequestStepWaypoint.tsx | 71 +++++++++---------- src/styles/index.ts | 1 + 7 files changed, 41 insertions(+), 143 deletions(-) delete mode 100644 src/hooks/useSubmitButtonVisibility.native.ts delete mode 100644 src/hooks/useSubmitButtonVisibility.ts diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index d1dc42bb4678..e3723b85e3d9 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -475,6 +475,7 @@ function AddressSearch( } placeholder="" listViewDisplayed + disableScroll > setLocationErrorCode(null)} diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index b6699c0cf3f4..3d20f910dca0 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -74,9 +74,6 @@ type FormProviderProps = FormProvider /** Whether to apply flex to the submit button */ submitFlexEnabled?: boolean; - - /** Whether the form container should grow or adapt to the viewable available space */ - shouldContainerGrow?: boolean; }; function FormProvider( diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 500f7d985599..5c74fd466a15 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,8 +1,8 @@ import React, {useCallback, useMemo, useRef} from 'react'; import type {RefObject} from 'react'; // eslint-disable-next-line no-restricted-imports -import type {ScrollView as RNScrollView, StyleProp, ViewStyle} from 'react-native'; -import {Keyboard, View} from 'react-native'; +import type {ScrollView as RNScrollView, StyleProp, View, ViewStyle} from 'react-native'; +import {Keyboard} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; @@ -32,9 +32,6 @@ type FormWrapperProps = ChildrenProps & /** Whether to apply flex to the submit button */ submitFlexEnabled?: boolean; - /** Whether the form container should grow or adapt to the viewable available space */ - shouldContainerGrow?: boolean; - /** Server side errors keyed by microtime */ errors: FormInputErrors; @@ -63,7 +60,6 @@ function FormWrapper({ scrollContextEnabled = false, shouldHideFixErrorsAlert = false, disablePressOnEnter = true, - shouldContainerGrow = true, }: FormWrapperProps) { const styles = useThemeStyles(); const formRef = useRef(null); @@ -108,7 +104,7 @@ function FormWrapper({ ref={formContentRef} style={[style, safeAreaPaddingBottomStyle.paddingBottom ? safeAreaPaddingBottomStyle : styles.pb5]} > - {shouldContainerGrow ? children : {children}} + {children} {isSubmitButtonVisible && ( @@ -169,7 +164,7 @@ function FormWrapper({ ) : ( diff --git a/src/hooks/useSubmitButtonVisibility.native.ts b/src/hooks/useSubmitButtonVisibility.native.ts deleted file mode 100644 index a962b52ce1f0..000000000000 --- a/src/hooks/useSubmitButtonVisibility.native.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {useState} from 'react'; -import useSafeAreaInsets from './useSafeAreaInsets'; -import useThemeStyles from './useThemeStyles'; - -// Useful when there's a need to hide the submit button from FormProvider, -// to let form content fill the page when virtual keyboard is shown -function useSubmitButtonVisibility() { - const styles = useThemeStyles(); - const [isSubmitButtonVisible, setIsSubmitButtonVisible] = useState(true); - const {bottom} = useSafeAreaInsets(); - - const showSubmitButton = () => { - setIsSubmitButtonVisible(true); - }; - - const hideSubmitButton = () => { - setIsSubmitButtonVisible(false); - }; - - // When the submit button is hidden there's a need to manually - // add its bottom style to the FormProvider style prop, - // otherwise the form content will touch the bottom of the page/screen - const formStyle = !isSubmitButtonVisible && bottom === 0 && styles.mb5; - - return { - isSubmitButtonVisible, - showSubmitButton, - hideSubmitButton, - formStyle, - }; -} - -export default useSubmitButtonVisibility; diff --git a/src/hooks/useSubmitButtonVisibility.ts b/src/hooks/useSubmitButtonVisibility.ts deleted file mode 100644 index 72f704edbe13..000000000000 --- a/src/hooks/useSubmitButtonVisibility.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {useEffect, useRef, useState} from 'react'; -import {Dimensions} from 'react-native'; -import useSafeAreaInsets from './useSafeAreaInsets'; -import useThemeStyles from './useThemeStyles'; -import useWindowDimensions from './useWindowDimensions'; - -// Useful when there's a need to hide the submit button from FormProvider, -// to let form content fill the page when virtual keyboard is shown -function useSubmitButtonVisibility() { - const styles = useThemeStyles(); - const {windowHeight, isSmallScreenWidth} = useWindowDimensions(); - const [isSubmitButtonVisible, setIsSubmitButtonVisible] = useState(true); - const initialWindowHeightRef = useRef(windowHeight); - const isSmallScreenWidthRef = useRef(isSmallScreenWidth); - const {bottom} = useSafeAreaInsets(); - - // Web: the submit button is shown when the height of the window is the same or greater, - // otherwise it's hidden - useEffect(() => { - const dimensionsListener = Dimensions.addEventListener('change', ({window}) => { - if (!isSmallScreenWidthRef.current) { - return; - } - - if (window.height < initialWindowHeightRef.current) { - setIsSubmitButtonVisible(false); - return; - } - - setIsSubmitButtonVisible(true); - }); - - return () => dimensionsListener.remove(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Web: the submit button is only shown when the window height is the same or greater, - // so executing this function won't do anything - const showSubmitButton = () => {}; - - // Web: the submit button is only hidden when the window height becomes smaller, - // so executing this function won't do anything - const hideSubmitButton = () => {}; - - // When the submit button is hidden there's a need to manually - // add its bottom style to the FormProvider style prop, - // otherwise the form content will touch the bottom of the page/screen - const formStyle = !isSubmitButtonVisible && bottom === 0 && styles.mb5; - - return { - isSubmitButtonVisible, - showSubmitButton, - hideSubmitButton, - formStyle, - }; -} - -export default useSubmitButtonVisibility; diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx index d5684c373a87..a5c7df4254d9 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx @@ -1,6 +1,7 @@ import {useNavigation} from '@react-navigation/native'; import React, {useMemo, useRef, useState} from 'react'; import type {TextInput} from 'react-native'; +import {View} from 'react-native'; import type {Place} from 'react-native-google-places-autocomplete'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -17,7 +18,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useLocationBias from '@hooks/useLocationBias'; import useNetwork from '@hooks/useNetwork'; -import useSubmitButtonVisibility from '@hooks/useSubmitButtonVisibility'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -58,7 +58,6 @@ function IOURequestStepWaypoint({ }: IOURequestStepWaypointProps) { const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); - const {isSubmitButtonVisible, showSubmitButton, hideSubmitButton, formStyle} = useSubmitButtonVisibility(); const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false); const [restoreFocusType, setRestoreFocusType] = useState(); const navigation = useNavigation(); @@ -198,48 +197,45 @@ function IOURequestStepWaypoint({ restoreFocusType={restoreFocusType} /> - { - textInput.current = e as unknown as TextInput; - }} - hint={!isOffline ? 'distance.error.selectSuggestedAddress' : ''} - containerStyles={[styles.mt4]} - label={translate('distance.address')} - defaultValue={waypointAddress} - onPress={selectWaypoint} - onFocus={hideSubmitButton} - onBlur={showSubmitButton} - maxInputLength={CONST.FORM_CHARACTER_LIMIT} - renamedInputKeys={{ - address: `waypoint${pageIndex}`, - city: '', - country: '', - street: '', - street2: '', - zipCode: '', - lat: '', - lng: '', - state: '', - }} - predefinedPlaces={recentWaypoints} - resultTypes="" - /> + + { + textInput.current = e as unknown as TextInput; + }} + hint={!isOffline ? 'distance.error.selectSuggestedAddress' : ''} + containerStyles={[styles.mt4]} + label={translate('distance.address')} + defaultValue={waypointAddress} + onPress={selectWaypoint} + maxInputLength={CONST.FORM_CHARACTER_LIMIT} + renamedInputKeys={{ + address: `waypoint${pageIndex}`, + city: '', + country: '', + street: '', + street2: '', + zipCode: '', + lat: '', + lng: '', + state: '', + }} + predefinedPlaces={recentWaypoints} + resultTypes="" + /> + @@ -260,8 +256,7 @@ export default withWritableReportOrNotFound( // Only grab the most recent 20 waypoints because that's all that is shown in the UI. This also puts them into the format of data // that the google autocomplete component expects for it's "predefined places" feature. selector: (waypoints) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - (waypoints ? waypoints.slice(0, CONST.RECENT_WAYPOINTS_NUMBER) : []).map((waypoint) => ({ + (waypoints ? waypoints.slice(0, CONST.RECENT_WAYPOINTS_NUMBER as number) : []).map((waypoint) => ({ name: waypoint.name, description: waypoint.address ?? '', geometry: { diff --git a/src/styles/index.ts b/src/styles/index.ts index cf2ff76a1ae9..6d2631c6669f 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3516,6 +3516,7 @@ const styles = (theme: ThemeColors) => fontSize: variables.fontSizeNormal, lineHeight: variables.fontSizeNormalHeight, fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, + flex: 1, }, searchPressable: { From 6b111840569ebd7702328d816f9d02d9348d1337 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 12 Jun 2024 13:48:30 +0100 Subject: [PATCH 006/124] refactor: remove unnecessary code and comments --- src/components/AddressSearch/index.tsx | 10 +--------- .../AddressSearch/listViewOverflow/index.native.ts | 4 ---- src/components/AddressSearch/listViewOverflow/index.ts | 4 ---- src/styles/utils/index.ts | 2 -- 4 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 src/components/AddressSearch/listViewOverflow/index.native.ts delete mode 100644 src/components/AddressSearch/listViewOverflow/index.ts diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index e3723b85e3d9..0f7d1f578861 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -23,7 +23,6 @@ import CONST from '@src/CONST'; import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import CurrentLocationButton from './CurrentLocationButton'; import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer'; -import listViewOverflow from './listViewOverflow'; import type {AddressSearchProps, PredefinedPlace} from './types'; /** @@ -439,14 +438,7 @@ function AddressSearch( }} styles={{ textInputContainer: [styles.flexColumn], - listView: [ - StyleUtils.getGoogleListViewStyle(displayListViewBorder), - listViewOverflow, - styles.borderLeft, - styles.borderRight, - styles.flexGrow0, - !isFocused && styles.h0, - ], + listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.borderLeft, styles.borderRight, styles.flexGrow0, !isFocused && styles.h0], row: [styles.pv4, styles.ph3, styles.overflowAuto], description: [styles.googleSearchText], separator: [styles.googleSearchSeparator], diff --git a/src/components/AddressSearch/listViewOverflow/index.native.ts b/src/components/AddressSearch/listViewOverflow/index.native.ts deleted file mode 100644 index 36b9f4005376..000000000000 --- a/src/components/AddressSearch/listViewOverflow/index.native.ts +++ /dev/null @@ -1,4 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import {defaultStyles} from '@styles/index'; - -export default defaultStyles.overflowHidden; diff --git a/src/components/AddressSearch/listViewOverflow/index.ts b/src/components/AddressSearch/listViewOverflow/index.ts deleted file mode 100644 index ae8bf35cc80c..000000000000 --- a/src/components/AddressSearch/listViewOverflow/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import {defaultStyles} from '@styles/index'; - -export default defaultStyles.overflowAuto; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index f3c145210764..cfa885ba7be9 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1428,8 +1428,6 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ getGoogleListViewStyle: (shouldDisplayBorder: boolean): ViewStyle => { if (shouldDisplayBorder) { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...styles.borderTopRounded, ...styles.borderBottomRounded, From 30c08c869e4d017574a685f47bab226f467754dd Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Wed, 12 Jun 2024 16:53:39 -0700 Subject: [PATCH 007/124] allow displaying default rooms with empty participants when searching for reports --- src/libs/OptionsListUtils.ts | 4 ++++ src/libs/ReportUtils.ts | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 9b30764b065d..e2d959ce4945 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -175,6 +175,7 @@ type GetOptionsConfig = { recentlyUsedPolicyReportFieldOptions?: string[]; transactionViolations?: OnyxCollection; includeInvoiceRooms?: boolean; + isSearchingForReports?: boolean; }; type GetUserToInviteConfig = { @@ -1749,6 +1750,7 @@ function getOptions( policyReportFieldOptions = [], recentlyUsedPolicyReportFieldOptions = [], includeInvoiceRooms = false, + isSearchingForReports = false, }: GetOptionsConfig, ): Options { if (includeCategories) { @@ -1831,6 +1833,7 @@ function getOptions( isInFocusMode: false, excludeEmptyChats: false, includeSelfDM, + isSearchingForReports }); }); @@ -2082,6 +2085,7 @@ function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = includeMoneyRequests: true, includeTasks: true, includeSelfDM: true, + isSearchingForReports: true, }); Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 914c653b6f91..a65749a2fa1b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5334,6 +5334,7 @@ function shouldReportBeInOptionList({ excludeEmptyChats, doesReportHaveViolations, includeSelfDM = false, + isSearchingForReports = false, }: { report: OnyxEntry; currentReportId: string; @@ -5343,6 +5344,7 @@ function shouldReportBeInOptionList({ excludeEmptyChats: boolean; doesReportHaveViolations: boolean; includeSelfDM?: boolean; + isSearchingForReports?: boolean; }) { const isInDefaultMode = !isInFocusMode; // Exclude reports that have no data because there wouldn't be anything to show in the option item. @@ -5370,7 +5372,8 @@ function shouldReportBeInOptionList({ !isSelfDM(report) && !isSystemChat(report) && !isGroupChat(report) && - !isInvoiceRoom(report)) + !isInvoiceRoom(report) && + (!isSearchingForReports && isDefaultRoom(report))) ) { return false; } From fbb2d4add636dda4444da6e2c8ae127c9dbecdb2 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 13 Jun 2024 14:20:20 +0100 Subject: [PATCH 008/124] refactor: apply suggestion --- src/components/AddressSearch/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 0f7d1f578861..f2da76513625 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -39,7 +39,7 @@ function isPlaceMatchForSearch(search: string, place: PredefinedPlace): boolean return false; } const fullSearchSentence = `${place.name ?? ''} ${place.description}`; - return search.split(' ').every((searchTerm) => !searchTerm || (searchTerm && fullSearchSentence.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()))); + return search.split(' ').every((searchTerm) => !searchTerm || fullSearchSentence.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase())); } // The error that's being thrown below will be ignored until we fork the From 72e3f6d3d887521c8ed69f80a0735bd8f7882faf Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 17 Jun 2024 12:32:24 -0700 Subject: [PATCH 009/124] add comment --- src/libs/ReportUtils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c6ba5fb2734a..bfc8cf5b4e04 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5370,6 +5370,10 @@ function shouldReportBeInOptionList({ !isSystemChat(report) && !isGroupChat(report) && !isInvoiceRoom(report) && + + // We omit sending back participants for default rooms when searching for reports since they aren't needed to display the results and can get very large. + // So we allow showing default rooms with no participants when searching for reports. + // In any other circumstances we should never have default rooms with no participants in Onyx. (!isSearchingForReports && isDefaultRoom(report))) ) { return false; From 76b6469092860356cf64107d5b68b43d007cb53d Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 17 Jun 2024 12:41:03 -0700 Subject: [PATCH 010/124] style --- src/libs/OptionsListUtils.ts | 2 +- src/libs/ReportUtils.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index cd78d6740996..09f6c8c90b18 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1833,7 +1833,7 @@ function getOptions( isInFocusMode: false, excludeEmptyChats: false, includeSelfDM, - isSearchingForReports + isSearchingForReports, }); }); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bfc8cf5b4e04..75f4e08973df 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5370,11 +5370,11 @@ function shouldReportBeInOptionList({ !isSystemChat(report) && !isGroupChat(report) && !isInvoiceRoom(report) && - // We omit sending back participants for default rooms when searching for reports since they aren't needed to display the results and can get very large. // So we allow showing default rooms with no participants when searching for reports. // In any other circumstances we should never have default rooms with no participants in Onyx. - (!isSearchingForReports && isDefaultRoom(report))) + !isSearchingForReports && + isDefaultRoom(report)) ) { return false; } From 57d33566d9fa86aeaa36a7bc1112f36859156f12 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 17 Jun 2024 13:36:49 -0700 Subject: [PATCH 011/124] prevent rooms from getting filtered out during participantLogins/display names check --- src/libs/OptionsListUtils.ts | 7 +++++++ src/libs/ReportUtils.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 09f6c8c90b18..03a0754a5028 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2420,6 +2420,13 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt }); } + // If the item is a chat room, then we just include all the search terms as the keys. + // Since we don't send back participants with chat rooms in SearchForReports, this ensures they don't get filtered out during this check. + // Note that the back-end already handles including rooms that have participants matching the search term. + if (item.isChatRoom) { + keys.concat(searchTerms); + } + return keys; }; const matchResults = searchTerms.reduceRight((items, term) => { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 75f4e08973df..c51d917992a3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5370,6 +5370,7 @@ function shouldReportBeInOptionList({ !isSystemChat(report) && !isGroupChat(report) && !isInvoiceRoom(report) && + // We omit sending back participants for default rooms when searching for reports since they aren't needed to display the results and can get very large. // So we allow showing default rooms with no participants when searching for reports. // In any other circumstances we should never have default rooms with no participants in Onyx. From 932b8ce9388a6ba0a53cd8e11858c89e99f8830a Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 17 Jun 2024 14:44:20 -0700 Subject: [PATCH 012/124] style --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c51d917992a3..75f4e08973df 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5370,7 +5370,6 @@ function shouldReportBeInOptionList({ !isSystemChat(report) && !isGroupChat(report) && !isInvoiceRoom(report) && - // We omit sending back participants for default rooms when searching for reports since they aren't needed to display the results and can get very large. // So we allow showing default rooms with no participants when searching for reports. // In any other circumstances we should never have default rooms with no participants in Onyx. From 1464ba19c124723faf6d0572190c555abd2cda0a Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 17 Jun 2024 16:26:49 -0700 Subject: [PATCH 013/124] ensure chat rooms don't get filtered out in the front-end if they don't have participants in Onyx --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 03a0754a5028..de7f814799da 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2424,7 +2424,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt // Since we don't send back participants with chat rooms in SearchForReports, this ensures they don't get filtered out during this check. // Note that the back-end already handles including rooms that have participants matching the search term. if (item.isChatRoom) { - keys.concat(searchTerms); + keys.push(...searchTerms); } return keys; From e8cae103e43cc725ee15a0d5455cc5fb2d02f172 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 17 Jun 2024 16:29:50 -0700 Subject: [PATCH 014/124] update comment --- src/libs/OptionsListUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index de7f814799da..b7da700d7b53 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2423,6 +2423,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt // If the item is a chat room, then we just include all the search terms as the keys. // Since we don't send back participants with chat rooms in SearchForReports, this ensures they don't get filtered out during this check. // Note that the back-end already handles including rooms that have participants matching the search term. + // This effectively means that only the back-end logic is used for filtering chat rooms. if (item.isChatRoom) { keys.push(...searchTerms); } From acc477f17da78c6f5a8c56284132bd1c2b8ab5d3 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 17 Jun 2024 16:38:54 -0700 Subject: [PATCH 015/124] remove unused --- src/libs/OptionsListUtils.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index b7da700d7b53..09f6c8c90b18 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2420,14 +2420,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt }); } - // If the item is a chat room, then we just include all the search terms as the keys. - // Since we don't send back participants with chat rooms in SearchForReports, this ensures they don't get filtered out during this check. - // Note that the back-end already handles including rooms that have participants matching the search term. - // This effectively means that only the back-end logic is used for filtering chat rooms. - if (item.isChatRoom) { - keys.push(...searchTerms); - } - return keys; }; const matchResults = searchTerms.reduceRight((items, term) => { From 4d5b18bdbc7df83b700089d4649de2003b6c374f Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:34:30 +0530 Subject: [PATCH 016/124] Use suitable decimals while calculating tax --- src/components/MoneyRequestConfirmationList.tsx | 2 +- src/libs/CurrencyUtils.ts | 15 ++++++++------- src/libs/TransactionUtils.ts | 7 +++++-- .../iou/request/step/IOURequestStepAmount.tsx | 2 +- .../request/step/IOURequestStepDistanceRate.tsx | 1 + .../request/step/IOURequestStepTaxAmountPage.tsx | 2 +- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 87fc207dd1fd..5b2eca279d4e 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -389,7 +389,7 @@ function MoneyRequestConfirmationList({ taxCode = transaction?.taxCode ?? TransactionUtils.getDefaultTaxCode(policy, transaction) ?? ''; } const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxCode) ?? ''; - const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount); + const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, currency); const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount.toString())); IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', taxAmountInSmallestCurrencyUnits); }, [policy, shouldShowTax, previousTransactionAmount, previousTransactionCurrency, transaction, isDistanceRequest, customUnitRateID]); diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index 7b54fbf0bed7..064c69799bb7 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -87,8 +87,9 @@ function convertToBackendAmount(amountAsFloat: number): number { * * @note we do not support any currencies with more than two decimal places. */ -function convertToFrontendAmountAsInteger(amountAsInt: number): number { - return Math.trunc(amountAsInt) / 100.0; +function convertToFrontendAmountAsInteger(amountAsInt: number, currency: string): number { + const decimals = getCurrencyDecimals(currency); + return toFixedNumber((Math.trunc(amountAsInt) / 100.0), decimals); } /** @@ -96,11 +97,11 @@ function convertToFrontendAmountAsInteger(amountAsInt: number): number { * * @note we do not support any currencies with more than two decimal places. */ -function convertToFrontendAmountAsString(amountAsInt: number | null | undefined): string { +function convertToFrontendAmountAsString(amountAsInt: number | null | undefined, currency: string): string { if (amountAsInt === null || amountAsInt === undefined) { return ''; } - return convertToFrontendAmountAsInteger(amountAsInt).toFixed(2); + return convertToFrontendAmountAsInteger(amountAsInt, currency).toFixed(2); } /** @@ -111,7 +112,7 @@ function convertToFrontendAmountAsString(amountAsInt: number | null | undefined) * @param currency - IOU currency */ function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURRENCY.USD): string { - const convertedAmount = convertToFrontendAmountAsInteger(amountInCents); + const convertedAmount = convertToFrontendAmountAsInteger(amountInCents, currency); /** * Fallback currency to USD if it empty string or undefined */ @@ -137,7 +138,7 @@ function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURR * @param currency - IOU currency */ function convertToShortDisplayString(amountInCents = 0, currency: string = CONST.CURRENCY.USD): string { - const convertedAmount = convertToFrontendAmountAsInteger(amountInCents); + const convertedAmount = convertToFrontendAmountAsInteger(amountInCents, currency); return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, { style: 'currency', @@ -168,7 +169,7 @@ function convertAmountToDisplayString(amount = 0, currency: string = CONST.CURRE * Acts the same as `convertAmountToDisplayString` but the result string does not contain currency */ function convertToDisplayStringWithoutCurrency(amountInCents: number, currency: string = CONST.CURRENCY.USD) { - const convertedAmount = convertToFrontendAmountAsInteger(amountInCents); + const convertedAmount = convertToFrontendAmountAsInteger(amountInCents, currency); return NumberFormatUtils.formatToParts(BaseLocaleListener.getPreferredLocale(), convertedAmount, { style: 'currency', currency, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index cfeb866e572f..5a83bd95c4ed 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -13,6 +13,7 @@ import DateUtils from './DateUtils'; import * as Localize from './Localize'; import * as NumberUtils from './NumberUtils'; import {getCleanedTagName, getCustomUnitRate} from './PolicyUtils'; +import {getCurrencyDecimals} from "@libs/CurrencyUtils"; let allTransactions: OnyxCollection = {}; Onyx.connect({ @@ -699,9 +700,11 @@ function hasWarningTypeViolation(transactionID: string, transactionViolations: O /** * Calculates tax amount from the given expense amount and tax percentage */ -function calculateTaxAmount(percentage: string, amount: number) { +function calculateTaxAmount(percentage: string, amount: number, currency: string | undefined = undefined) { const divisor = Number(percentage.slice(0, -1)) / 100 + 1; - return Math.round(amount - amount / divisor) / 100; + const taxAmount = (amount - amount / divisor) / 100; + const decimals = getCurrencyDecimals(currency); + return parseFloat(taxAmount.toFixed(decimals)); } /** diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 04c8e772844b..f234b3b71718 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -294,7 +294,7 @@ function IOURequestStepAmount({ const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, currentTransaction, currency) ?? ''; const taxCode = (currency !== transactionCurrency ? defaultTaxCode : transactionTaxCode) ?? defaultTaxCode; const taxPercentage = TransactionUtils.getTaxValue(policy, currentTransaction, taxCode) ?? ''; - const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, newAmount)); + const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, newAmount, currency)); if (isSplitBill) { IOU.setDraftSplitTransaction(transactionID, {amount: newAmount, currency, taxCode, taxAmount}); diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 0f2d0cc0da61..f745cb1e89e8 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -23,6 +23,7 @@ import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import currency from "@src/types/onyx/Currency"; type IOURequestStepDistanceRateOnyxProps = { /** Policy details */ diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx index e67f83708938..d54cd6129e12 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx @@ -50,7 +50,7 @@ function getTaxAmount(transaction: OnyxEntry, policy: OnyxEntry Date: Wed, 19 Jun 2024 18:19:05 +0100 Subject: [PATCH 017/124] refactor: remove unnecessary styles --- src/components/AddressSearch/index.tsx | 4 ++-- src/pages/iou/request/step/IOURequestStepWaypoint.tsx | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index f2da76513625..275cbbf6f39b 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -438,8 +438,8 @@ function AddressSearch( }} styles={{ textInputContainer: [styles.flexColumn], - listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.borderLeft, styles.borderRight, styles.flexGrow0, !isFocused && styles.h0], - row: [styles.pv4, styles.ph3, styles.overflowAuto], + listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.borderLeft, styles.borderRight, !isFocused && styles.h0], + row: [styles.pv4, styles.ph3], description: [styles.googleSearchText], separator: [styles.googleSearchSeparator], container: [styles.mh100], diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx index 61abb4f0428d..0ba61d92071d 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx @@ -160,7 +160,6 @@ function IOURequestStepWaypoint({ onEntryTransitionEnd={() => textInput.current?.focus()} shouldEnableMaxHeight testID={IOURequestStepWaypoint.displayName} - style={styles.overflowHidden} > Date: Fri, 21 Jun 2024 12:52:35 -0700 Subject: [PATCH 018/124] prevent all chat rooms from being searchable by participant --- src/libs/OptionsListUtils.ts | 12 ++++++++++++ src/libs/ReportUtils.ts | 1 + 2 files changed, 13 insertions(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f937eb9df8de..868171ea1d3a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -769,6 +769,7 @@ function createOption( isPolicyExpenseChat: false, isOwnPolicyExpenseChat: false, isExpenseReport: false, + isTripRoom: false, policyID: undefined, isOptimisticPersonalDetail: false, lastMessageText: '', @@ -806,6 +807,7 @@ function createOption( result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; result.policyID = report.policyID; result.isSelfDM = ReportUtils.isSelfDM(report); + result.isTripRoom = ReportUtils.isTripRoom(report); const visibleParticipantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, true); @@ -2494,6 +2496,16 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt values = values.concat(getParticipantsLoginsArray(item)); } + // We don't want the following to be searchable by participant: + // - Default rooms + // - Policy rooms + // - Policy expense chats + // - Invoice rooms + // - Trip rooms + if (!item.isChatRoom && !item.isPolicyExpenseChat && !item.isTripRoom) { + values = values.concat(getParticipantsLoginsArray(item)); + } + return uniqFast(values); }); const personalDetails = filterArrayByMatch(items.personalDetails, term, (item) => diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 96cb8d79de56..3c37e0a034f5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -411,6 +411,7 @@ type OptionData = { displayNamesWithTooltips?: DisplayNameWithTooltips | null; isDefaultRoom?: boolean; isInvoiceRoom?: boolean; + isTripRoom?: boolean; isExpenseReport?: boolean; isOptimisticPersonalDetail?: boolean; selected?: boolean; From 3e6b24ec35020c2247ee1ace15a59dc870594c3a Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 21 Jun 2024 15:25:36 -0700 Subject: [PATCH 019/124] Allow empty rooms to show up when searching --- src/libs/OptionsListUtils.ts | 24 +++++++++++++++++++++--- src/libs/ReportUtils.ts | 16 ++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 868171ea1d3a..504c6c8ac8d0 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -773,6 +773,7 @@ function createOption( policyID: undefined, isOptimisticPersonalDetail: false, lastMessageText: '', + isUnsearchableViaParticipants: false, }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); @@ -808,6 +809,8 @@ function createOption( result.policyID = report.policyID; result.isSelfDM = ReportUtils.isSelfDM(report); result.isTripRoom = ReportUtils.isTripRoom(report); + result.policyName = ReportUtils.getPolicyName(report); + result.isUnsearchableViaParticipants = ReportUtils.isUnsearchableViaParticipants(report); const visibleParticipantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, true); @@ -1872,6 +1875,8 @@ function getOptions( }); }); + console.log(">>>> filtered", filteredReportOptions); + // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - When searching, self DM is put at the top @@ -1935,13 +1940,23 @@ function getOptions( return; } - if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) { - return; + if (!accountIDs || accountIDs.length === 0) { + if (!isSearchingForReports) { + if (!isChatRoom) { + return; + } + } + + if (!ReportUtils.isUnsearchableViaParticipants(report)) { + return; + } } return option; }); + console.log(">>>> allReportOptions", allReportOptions); + const havingLoginPersonalDetails = includeP2P ? options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail) : []; let allPersonalDetailsOptions = havingLoginPersonalDetails; @@ -2022,6 +2037,7 @@ function getOptions( const isSearchMatch = isSearchStringMatch(searchValue, searchText, participantNames, isChatRoom); if (!isReportIdSearch && !isSearchMatch) { + console.log(">>> filtering out", reportOption); continue; } } @@ -2446,6 +2462,8 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt optionsToExclude.push({login}); }); + console.log(">>>> options", options); + const getParticipantsLoginsArray = (item: ReportUtils.OptionData) => { const keys: string[] = []; const visibleChatMemberAccountIDs = item.participantsList ?? []; @@ -2502,7 +2520,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt // - Policy expense chats // - Invoice rooms // - Trip rooms - if (!item.isChatRoom && !item.isPolicyExpenseChat && !item.isTripRoom) { + if (!item.isUnsearchableViaParticipants) { values = values.concat(getParticipantsLoginsArray(item)); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 3c37e0a034f5..c17af5cbedb0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -430,6 +430,7 @@ type OptionData = { shouldShowAmountInput?: boolean; amountInputProps?: MoneyRequestAmountInputProps; tabIndex?: 0 | -1; + isUnsearchableViaParticipants?: boolean; } & Report; type OnyxDataTaskAssigneeChat = { @@ -972,6 +973,13 @@ function isChatRoom(report: OnyxEntry): boolean { return isUserCreatedPolicyRoom(report) || isDefaultRoom(report) || isInvoiceRoom(report); } +/** + * Whether the provided report is not searchable via participant + */ +function isUnsearchableViaParticipants(report: OnyxEntry): boolean { + return isUserCreatedPolicyRoom(report) || isDefaultRoom(report) || isInvoiceRoom(report) || isInvoiceRoom(report) || isTripRoom(report); +} + /** * Whether the provided report is a public room */ @@ -5403,11 +5411,10 @@ function shouldReportBeInOptionList({ !isSystemChat(report) && !isGroupChat(report) && !isInvoiceRoom(report) && - // We omit sending back participants for default rooms when searching for reports since they aren't needed to display the results and can get very large. - // So we allow showing default rooms with no participants when searching for reports. + // We omit sending back participants for chat rooms when searching for reports since they aren't needed to display the results and can get very large. + // So we allow showing rooms with no participants when searching for reports. // In any other circumstances we should never have default rooms with no participants in Onyx. - !isSearchingForReports && - isDefaultRoom(report)) + (!isSearchingForReports && isUnsearchableViaParticipants(report))) ) { return false; } @@ -7290,6 +7297,7 @@ export { createDraftWorkspaceAndNavigateToConfirmationScreen, isChatUsedForOnboarding, getChatUsedForOnboarding, + isUnsearchableViaParticipants }; export type { From 6aebea173837c6fcc67b12b3ebda125ac69b334d Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 24 Jun 2024 12:52:40 -0700 Subject: [PATCH 020/124] remove debug logs --- src/libs/OptionsListUtils.ts | 7 +------ src/libs/ReportUtils.ts | 5 +++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 504c6c8ac8d0..b47583f3469e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1875,8 +1875,6 @@ function getOptions( }); }); - console.log(">>>> filtered", filteredReportOptions); - // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - When searching, self DM is put at the top @@ -1955,7 +1953,7 @@ function getOptions( return option; }); - console.log(">>>> allReportOptions", allReportOptions); + console.log('>>>> allReportOptions', allReportOptions); const havingLoginPersonalDetails = includeP2P ? options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail) : []; let allPersonalDetailsOptions = havingLoginPersonalDetails; @@ -2037,7 +2035,6 @@ function getOptions( const isSearchMatch = isSearchStringMatch(searchValue, searchText, participantNames, isChatRoom); if (!isReportIdSearch && !isSearchMatch) { - console.log(">>> filtering out", reportOption); continue; } } @@ -2462,8 +2459,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt optionsToExclude.push({login}); }); - console.log(">>>> options", options); - const getParticipantsLoginsArray = (item: ReportUtils.OptionData) => { const keys: string[] = []; const visibleChatMemberAccountIDs = item.participantsList ?? []; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c17af5cbedb0..b7560a096cf5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5414,7 +5414,8 @@ function shouldReportBeInOptionList({ // We omit sending back participants for chat rooms when searching for reports since they aren't needed to display the results and can get very large. // So we allow showing rooms with no participants when searching for reports. // In any other circumstances we should never have default rooms with no participants in Onyx. - (!isSearchingForReports && isUnsearchableViaParticipants(report))) + !isSearchingForReports && + isUnsearchableViaParticipants(report)) ) { return false; } @@ -7297,7 +7298,7 @@ export { createDraftWorkspaceAndNavigateToConfirmationScreen, isChatUsedForOnboarding, getChatUsedForOnboarding, - isUnsearchableViaParticipants + isUnsearchableViaParticipants, }; export type { From d0fb81dbf1ed47bbf1788c95f795fc87044a89e9 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 24 Jun 2024 12:53:18 -0700 Subject: [PATCH 021/124] remove debug logs --- src/libs/OptionsListUtils.ts | 2 -- src/libs/ReportUtils.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index b47583f3469e..137f01ed1c7d 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1953,8 +1953,6 @@ function getOptions( return option; }); - console.log('>>>> allReportOptions', allReportOptions); - const havingLoginPersonalDetails = includeP2P ? options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail) : []; let allPersonalDetailsOptions = havingLoginPersonalDetails; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b7560a096cf5..b350eaaeb3e8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -977,7 +977,7 @@ function isChatRoom(report: OnyxEntry): boolean { * Whether the provided report is not searchable via participant */ function isUnsearchableViaParticipants(report: OnyxEntry): boolean { - return isUserCreatedPolicyRoom(report) || isDefaultRoom(report) || isInvoiceRoom(report) || isInvoiceRoom(report) || isTripRoom(report); + return isUserCreatedPolicyRoom(report) || isDefaultRoom(report) || isInvoiceRoom(report) || isTripRoom(report); } /** From 76f675ddcb6e13ce03f495de3327a640344e33cd Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 24 Jun 2024 12:53:47 -0700 Subject: [PATCH 022/124] update comment --- src/libs/OptionsListUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 137f01ed1c7d..56a892d70020 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2510,7 +2510,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt // We don't want the following to be searchable by participant: // - Default rooms // - Policy rooms - // - Policy expense chats // - Invoice rooms // - Trip rooms if (!item.isUnsearchableViaParticipants) { From fe894c629ef46f296eed36156e23b89143fcdd9b Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 25 Jun 2024 19:22:14 +0530 Subject: [PATCH 023/124] fix: Categorizing - RHP closes instead of returning to category list after deleting a category. Signed-off-by: Krishna Gupta --- .../workspace/categories/CategorySettingsPage.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 444e0e8a47d2..f5e6d3824a5f 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -44,6 +44,14 @@ function CategorySettingsPage({route, policyCategories, navigation}: CategorySet const policyCategory = policyCategories?.[route.params.categoryName] ?? Object.values(policyCategories ?? {}).find((category) => category.previousCategoryName === route.params.categoryName); + const navigateBack = () => { + if (backTo) { + Navigation.goBack(ROUTES.SETTINGS_CATEGORIES_ROOT.getRoute(route.params.policyID, backTo)); + return; + } + Navigation.goBack(); + }; + useEffect(() => { if (policyCategory?.name === route.params.categoryName || !policyCategory) { return; @@ -70,7 +78,7 @@ function CategorySettingsPage({route, policyCategories, navigation}: CategorySet const deleteCategory = () => { Category.deleteWorkspaceCategories(route.params.policyID, [route.params.categoryName]); setDeleteCategoryConfirmModalVisible(false); - Navigation.dismissModal(); + navigateBack(); }; const isThereAnyAccountingConnection = Object.keys(policy?.connections ?? {}).length !== 0; @@ -88,7 +96,7 @@ function CategorySettingsPage({route, policyCategories, navigation}: CategorySet > (backTo ? Navigation.goBack(ROUTES.SETTINGS_CATEGORIES_ROOT.getRoute(route.params.policyID, backTo)) : Navigation.goBack())} + onBackButtonPress={navigateBack} /> Date: Tue, 25 Jun 2024 16:22:23 +0100 Subject: [PATCH 024/124] fix: address suggestions list not scrollable on web --- src/components/AddressSearch/index.tsx | 5 +++-- src/components/AddressSearch/isRowScrollable.native.ts | 8 ++++++++ src/components/AddressSearch/isRowScrollable.ts | 6 ++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/components/AddressSearch/isRowScrollable.native.ts create mode 100644 src/components/AddressSearch/isRowScrollable.ts diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 925bb5660830..b0acc19cc1dc 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -25,6 +25,7 @@ import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import CurrentLocationButton from './CurrentLocationButton'; import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer'; import type {AddressSearchProps, PredefinedPlace} from './types'; +import isRowScrollable from './isRowScrollable'; /** * Check if the place matches the search by the place name or description. @@ -451,7 +452,7 @@ function AddressSearch( container: [styles.mh100], }} numberOfLines={2} - isRowScrollable={false} + isRowScrollable={isRowScrollable} listHoverColor={theme.border} listUnderlayColor={theme.buttonPressedBG} onLayout={(event: LayoutChangeEvent) => { @@ -473,7 +474,7 @@ function AddressSearch( } placeholder="" listViewDisplayed - disableScroll + disableScroll={!isRowScrollable} > setLocationErrorCode(null)} diff --git a/src/components/AddressSearch/isRowScrollable.native.ts b/src/components/AddressSearch/isRowScrollable.native.ts new file mode 100644 index 000000000000..dd1f0d94be97 --- /dev/null +++ b/src/components/AddressSearch/isRowScrollable.native.ts @@ -0,0 +1,8 @@ +/** + * Whether the address suggestions list is scrollable + * + * On native, this is set to false because the whole screen is scrollable + */ +const isRowScrollable: boolean = false + +export default isRowScrollable \ No newline at end of file diff --git a/src/components/AddressSearch/isRowScrollable.ts b/src/components/AddressSearch/isRowScrollable.ts new file mode 100644 index 000000000000..96cc2f1236ec --- /dev/null +++ b/src/components/AddressSearch/isRowScrollable.ts @@ -0,0 +1,6 @@ +/** + * Whether the address suggestions list is scrollable + */ +const isRowScrollable: boolean = true + +export default isRowScrollable \ No newline at end of file From d03daf48ed1c844adb6182536408d8d465711e80 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 26 Jun 2024 01:03:31 +0530 Subject: [PATCH 025/124] Update --- src/components/MoneyRequestAmountInput.tsx | 2 +- src/libs/CurrencyUtils.ts | 5 +++-- src/pages/iou/MoneyRequestAmountForm.tsx | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index 0c3868312c41..ef03a5d4d717 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -103,7 +103,7 @@ const getNewSelection = (oldSelection: Selection, prevLength: number, newLength: return {start: cursorPosition, end: cursorPosition}; }; -const defaultOnFormatAmount = (amount: number) => CurrencyUtils.convertToFrontendAmountAsString(amount); +const defaultOnFormatAmount = (amount: number, currency?: string) => CurrencyUtils.convertToFrontendAmountAsString(amount, currency ?? CONST.CURRENCY.USD); function MoneyRequestAmountInput( { diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index 064c69799bb7..712d73569b0d 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -89,7 +89,7 @@ function convertToBackendAmount(amountAsFloat: number): number { */ function convertToFrontendAmountAsInteger(amountAsInt: number, currency: string): number { const decimals = getCurrencyDecimals(currency); - return toFixedNumber((Math.trunc(amountAsInt) / 100.0), decimals); + return Number((Math.round(amountAsInt) / 100.0).toFixed(decimals)); } /** @@ -101,7 +101,8 @@ function convertToFrontendAmountAsString(amountAsInt: number | null | undefined, if (amountAsInt === null || amountAsInt === undefined) { return ''; } - return convertToFrontendAmountAsInteger(amountAsInt, currency).toFixed(2); + const decimals = getCurrencyDecimals(currency); + return convertToFrontendAmountAsInteger(amountAsInt, currency).toFixed(decimals); } /** diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx index ff061e7382c6..e911c332a5c7 100644 --- a/src/pages/iou/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -70,8 +70,8 @@ type MoneyRequestAmountFormProps = { }; const isAmountInvalid = (amount: string) => !amount.length || parseFloat(amount) < 0.01; -const isTaxAmountInvalid = (currentAmount: string, taxAmount: number, isTaxAmountForm: boolean) => - isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmountAsInteger(Math.abs(taxAmount)); +const isTaxAmountInvalid = (currentAmount: string, taxAmount: number, isTaxAmountForm: boolean, currency: string) => + isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmountAsInteger(Math.abs(taxAmount), currency); const AMOUNT_VIEW_ID = 'amountView'; const NUM_PAD_CONTAINER_VIEW_ID = 'numPadContainerView'; @@ -149,7 +149,7 @@ function MoneyRequestAmountForm( }, [isFocused, wasFocused]); const initializeAmount = useCallback((newAmount: number) => { - const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmountAsString(newAmount) : ''; + const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmountAsString(newAmount, currency) : ''; moneyRequestAmountInput.current?.changeAmount(frontendAmount); moneyRequestAmountInput.current?.changeSelection({ start: frontendAmount.length, @@ -218,7 +218,7 @@ function MoneyRequestAmountForm( return; } - if (isTaxAmountInvalid(currentAmount, taxAmount, isTaxAmountForm)) { + if (isTaxAmountInvalid(currentAmount, taxAmount, isTaxAmountForm, currency)) { setFormError(translate('iou.error.invalidTaxAmount', {amount: formattedTaxAmount})); return; } From f1b4060af49c1b33c5927f38d48b57b45155681f Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 26 Jun 2024 01:17:00 +0530 Subject: [PATCH 026/124] Add and update tests --- tests/unit/CurrencyUtilsTest.ts | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/unit/CurrencyUtilsTest.ts b/tests/unit/CurrencyUtilsTest.ts index 87b7c7ee4569..dc1a4888e9b4 100644 --- a/tests/unit/CurrencyUtilsTest.ts +++ b/tests/unit/CurrencyUtilsTest.ts @@ -113,7 +113,20 @@ describe('CurrencyUtils', () => { [2500, 25], [2500.5, 25], // The backend should never send a decimal .5 value ])('Correctly converts %s to amount in units handled in frontend as an integer', (amount, expectedResult) => { - expect(CurrencyUtils.convertToFrontendAmountAsInteger(amount)).toBe(expectedResult); + expect(CurrencyUtils.convertToFrontendAmountAsInteger(amount, CONST.CURRENCY.USD)).toBe(expectedResult); + }); + }); + + describe('convertToFrontendAmountAsInteger VND', () => { + test.each([ + [2500, 25], + [2550, 26], + [25, 0], + [2500, 25], + [2586, 26], + [2500.5, 25], // The backend should never send a decimal .5 value + ])('Correctly converts %s to amount in units handled in frontend as an integer', (amount, expectedResult) => { + expect(CurrencyUtils.convertToFrontendAmountAsInteger(amount, 'VND')).toBe(expectedResult); }); }); @@ -127,7 +140,22 @@ describe('CurrencyUtils', () => { [undefined, ''], [0, '0.00'], ])('Correctly converts %s to amount in units handled in frontend as a string', (input, expectedResult) => { - expect(CurrencyUtils.convertToFrontendAmountAsString(input)).toBe(expectedResult); + expect(CurrencyUtils.convertToFrontendAmountAsString(input, CONST.CURRENCY.USD)).toBe(expectedResult); + }); + }); + + describe('convertToFrontendAmountAsString VND', () => { + test.each([ + [2500, '25'], + [2550, '26'], + [25, '0'], + [2500.5, '25'], + [null, ''], + [undefined, ''], + [0, '0'], + [2586, '26'] + ])('Correctly converts %s to amount in units handled in frontend as a string', (input, expectedResult) => { + expect(CurrencyUtils.convertToFrontendAmountAsString(input, 'VND')).toBe(expectedResult); }); }); From c362173ef3bce5a5ec9903fd8efe1f4c47ec51c3 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 26 Jun 2024 01:19:25 +0530 Subject: [PATCH 027/124] Update IOURequestStepDistanceRate.tsx --- src/pages/iou/request/step/IOURequestStepDistanceRate.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index f745cb1e89e8..0f2d0cc0da61 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -23,7 +23,6 @@ import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -import currency from "@src/types/onyx/Currency"; type IOURequestStepDistanceRateOnyxProps = { /** Policy details */ From e89431e2904b09e6849449194dd2a1ee3ee1b0a6 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:00:28 +0530 Subject: [PATCH 028/124] Update --- src/components/MoneyRequestConfirmationList.tsx | 2 +- src/libs/CurrencyUtils.ts | 4 ++-- src/libs/TransactionUtils.ts | 2 +- src/pages/iou/MoneyRequestAmountForm.tsx | 2 +- tests/unit/CurrencyUtilsTest.ts | 1 - 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 75114755e350..419b5374239b 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -332,7 +332,7 @@ function MoneyRequestConfirmationList({ const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, currency); const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount.toString())); IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', taxAmountInSmallestCurrencyUnits); - }, [policy, shouldShowTax, previousTransactionAmount, previousTransactionCurrency, transaction, isDistanceRequest, customUnitRateID]); + }, [policy, shouldShowTax, previousTransactionAmount, previousTransactionCurrency, transaction, isDistanceRequest, customUnitRateID, currency]); // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index 712d73569b0d..6bbafe50268e 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -87,7 +87,7 @@ function convertToBackendAmount(amountAsFloat: number): number { * * @note we do not support any currencies with more than two decimal places. */ -function convertToFrontendAmountAsInteger(amountAsInt: number, currency: string): number { +function convertToFrontendAmountAsInteger(amountAsInt: number, currency: string = CONST.CURRENCY.USD): number { const decimals = getCurrencyDecimals(currency); return Number((Math.round(amountAsInt) / 100.0).toFixed(decimals)); } @@ -97,7 +97,7 @@ function convertToFrontendAmountAsInteger(amountAsInt: number, currency: string) * * @note we do not support any currencies with more than two decimal places. */ -function convertToFrontendAmountAsString(amountAsInt: number | null | undefined, currency: string): string { +function convertToFrontendAmountAsString(amountAsInt: number | null | undefined, currency: string = CONST.CURRENCY.USD): string { if (amountAsInt === null || amountAsInt === undefined) { return ''; } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 8e0ad61eb020..b836b7bc3ec5 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -14,7 +14,7 @@ import * as Localize from './Localize'; import * as NumberUtils from './NumberUtils'; import Permissions from './Permissions'; import {getCleanedTagName, getCustomUnitRate} from './PolicyUtils'; -import {getCurrencyDecimals} from "@libs/CurrencyUtils"; +import {getCurrencyDecimals} from "./CurrencyUtils"; let allTransactions: OnyxCollection = {}; Onyx.connect({ diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx index e911c332a5c7..f193773f9466 100644 --- a/src/pages/iou/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -155,7 +155,7 @@ function MoneyRequestAmountForm( start: frontendAmount.length, end: frontendAmount.length, }); - }, []); + }, [currency]); useEffect(() => { if (!currency || typeof amount !== 'number') { diff --git a/tests/unit/CurrencyUtilsTest.ts b/tests/unit/CurrencyUtilsTest.ts index dc1a4888e9b4..b5b56806bc5c 100644 --- a/tests/unit/CurrencyUtilsTest.ts +++ b/tests/unit/CurrencyUtilsTest.ts @@ -122,7 +122,6 @@ describe('CurrencyUtils', () => { [2500, 25], [2550, 26], [25, 0], - [2500, 25], [2586, 26], [2500.5, 25], // The backend should never send a decimal .5 value ])('Correctly converts %s to amount in units handled in frontend as an integer', (amount, expectedResult) => { From f6d8de663216d716020b7cde61f251cf8cb03280 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:30:00 +0530 Subject: [PATCH 029/124] Update --- src/libs/CurrencyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index 6bbafe50268e..f07407972c8e 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -89,7 +89,7 @@ function convertToBackendAmount(amountAsFloat: number): number { */ function convertToFrontendAmountAsInteger(amountAsInt: number, currency: string = CONST.CURRENCY.USD): number { const decimals = getCurrencyDecimals(currency); - return Number((Math.round(amountAsInt) / 100.0).toFixed(decimals)); + return Number((Math.trunc(amountAsInt) / 100.0).toFixed(decimals)); } /** From 2a7588d22cdb7f5b068568ec95ec46282dc7fee6 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:31:23 +0530 Subject: [PATCH 030/124] Update --- src/libs/CurrencyUtils.ts | 2 +- src/libs/TransactionUtils.ts | 2 +- src/pages/iou/MoneyRequestAmountForm.tsx | 19 +++++++++++-------- tests/unit/CurrencyUtilsTest.ts | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index f07407972c8e..be7ce9aca8b5 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -97,7 +97,7 @@ function convertToFrontendAmountAsInteger(amountAsInt: number, currency: string * * @note we do not support any currencies with more than two decimal places. */ -function convertToFrontendAmountAsString(amountAsInt: number | null | undefined, currency: string = CONST.CURRENCY.USD): string { +function convertToFrontendAmountAsString(amountAsInt: number | null | undefined, currency: string = CONST.CURRENCY.USD): string { if (amountAsInt === null || amountAsInt === undefined) { return ''; } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index b836b7bc3ec5..6d55792e57f1 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -9,12 +9,12 @@ import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {IOURequestType} from './actions/IOU'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; +import {getCurrencyDecimals} from './CurrencyUtils'; import DateUtils from './DateUtils'; import * as Localize from './Localize'; import * as NumberUtils from './NumberUtils'; import Permissions from './Permissions'; import {getCleanedTagName, getCustomUnitRate} from './PolicyUtils'; -import {getCurrencyDecimals} from "./CurrencyUtils"; let allTransactions: OnyxCollection = {}; Onyx.connect({ diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx index f193773f9466..4db7a13171cb 100644 --- a/src/pages/iou/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -148,14 +148,17 @@ function MoneyRequestAmountForm( }); }, [isFocused, wasFocused]); - const initializeAmount = useCallback((newAmount: number) => { - const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmountAsString(newAmount, currency) : ''; - moneyRequestAmountInput.current?.changeAmount(frontendAmount); - moneyRequestAmountInput.current?.changeSelection({ - start: frontendAmount.length, - end: frontendAmount.length, - }); - }, [currency]); + const initializeAmount = useCallback( + (newAmount: number) => { + const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmountAsString(newAmount, currency) : ''; + moneyRequestAmountInput.current?.changeAmount(frontendAmount); + moneyRequestAmountInput.current?.changeSelection({ + start: frontendAmount.length, + end: frontendAmount.length, + }); + }, + [currency], + ); useEffect(() => { if (!currency || typeof amount !== 'number') { diff --git a/tests/unit/CurrencyUtilsTest.ts b/tests/unit/CurrencyUtilsTest.ts index b5b56806bc5c..d6c18ad82471 100644 --- a/tests/unit/CurrencyUtilsTest.ts +++ b/tests/unit/CurrencyUtilsTest.ts @@ -152,7 +152,7 @@ describe('CurrencyUtils', () => { [null, ''], [undefined, ''], [0, '0'], - [2586, '26'] + [2586, '26'], ])('Correctly converts %s to amount in units handled in frontend as a string', (input, expectedResult) => { expect(CurrencyUtils.convertToFrontendAmountAsString(input, 'VND')).toBe(expectedResult); }); From a5d2f9bfa45ecec17a423124c5aabb6368de3da4 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 26 Jun 2024 10:11:10 +0100 Subject: [PATCH 031/124] refactor: fix linter issues --- src/components/AddressSearch/isRowScrollable.native.ts | 6 +++--- src/components/AddressSearch/isRowScrollable.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/AddressSearch/isRowScrollable.native.ts b/src/components/AddressSearch/isRowScrollable.native.ts index dd1f0d94be97..aab4c3e3aa07 100644 --- a/src/components/AddressSearch/isRowScrollable.native.ts +++ b/src/components/AddressSearch/isRowScrollable.native.ts @@ -1,8 +1,8 @@ /** * Whether the address suggestions list is scrollable - * + * * On native, this is set to false because the whole screen is scrollable */ -const isRowScrollable: boolean = false +const isRowScrollable = false; -export default isRowScrollable \ No newline at end of file +export default isRowScrollable; diff --git a/src/components/AddressSearch/isRowScrollable.ts b/src/components/AddressSearch/isRowScrollable.ts index 96cc2f1236ec..3627a5b218a7 100644 --- a/src/components/AddressSearch/isRowScrollable.ts +++ b/src/components/AddressSearch/isRowScrollable.ts @@ -1,6 +1,6 @@ /** * Whether the address suggestions list is scrollable */ -const isRowScrollable: boolean = true +const isRowScrollable = true; -export default isRowScrollable \ No newline at end of file +export default isRowScrollable; From 69ac6d276c2ffca9ad28f82bb188873f69f1fcbc Mon Sep 17 00:00:00 2001 From: dominictb Date: Wed, 26 Jun 2024 17:03:32 +0700 Subject: [PATCH 032/124] fix: negate amount for expense report in ReportUtils.getNonHeldAndFullAmount Signed-off-by: dominictb --- src/libs/ReportUtils.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7a3b2d1d0869..71ed75a6457d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6592,15 +6592,18 @@ function getNonHeldAndFullAmount(iouReport: OnyxEntry, policy: OnyxEntry const transactions = TransactionUtils.getAllReportTransactions(iouReport?.reportID ?? '-1'); const hasPendingTransaction = transactions.some((transaction) => !!transaction.pendingAction); + // if the report is an expense report, the total amount should be negated + const coefficient = isExpenseReport(iouReport) ? -1 : 1; + if (hasUpdatedTotal(iouReport, policy) && hasPendingTransaction) { const unheldTotal = transactions.reduce((currentVal, transaction) => currentVal - (!TransactionUtils.isOnHold(transaction) ? transaction.amount : 0), 0); - return [CurrencyUtils.convertToDisplayString(unheldTotal, iouReport?.currency), CurrencyUtils.convertToDisplayString((iouReport?.total ?? 0) * -1, iouReport?.currency)]; + return [CurrencyUtils.convertToDisplayString(unheldTotal, iouReport?.currency), CurrencyUtils.convertToDisplayString((iouReport?.total ?? 0) * coefficient, iouReport?.currency)]; } return [ - CurrencyUtils.convertToDisplayString((iouReport?.unheldTotal ?? 0) * -1, iouReport?.currency), - CurrencyUtils.convertToDisplayString((iouReport?.total ?? 0) * -1, iouReport?.currency), + CurrencyUtils.convertToDisplayString((iouReport?.unheldTotal ?? 0) * coefficient, iouReport?.currency), + CurrencyUtils.convertToDisplayString((iouReport?.total ?? 0) * coefficient, iouReport?.currency), ]; } From f1c0d53194e40bbba501dc3959dd72b683b195a2 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 26 Jun 2024 16:33:21 +0100 Subject: [PATCH 033/124] refactor: fix prettier issues --- src/components/AddressSearch/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index b0acc19cc1dc..d37796f65524 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -24,8 +24,8 @@ import CONST from '@src/CONST'; import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import CurrentLocationButton from './CurrentLocationButton'; import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer'; -import type {AddressSearchProps, PredefinedPlace} from './types'; import isRowScrollable from './isRowScrollable'; +import type {AddressSearchProps, PredefinedPlace} from './types'; /** * Check if the place matches the search by the place name or description. From a5b70cc4451261c3947f9dc0fa2cbc8537e95ce6 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Wed, 26 Jun 2024 15:05:45 -0700 Subject: [PATCH 034/124] Flip searchable via participants conditions --- src/libs/OptionsListUtils.ts | 8 ++++---- src/libs/ReportUtils.ts | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 664c15642627..9f0e1aa832b5 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -774,7 +774,7 @@ function createOption( policyID: undefined, isOptimisticPersonalDetail: false, lastMessageText: '', - isUnsearchableViaParticipants: false, + isSearchableViaParticipants: false, }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); @@ -811,7 +811,7 @@ function createOption( result.isSelfDM = ReportUtils.isSelfDM(report); result.isTripRoom = ReportUtils.isTripRoom(report); result.policyName = ReportUtils.getPolicyName(report); - result.isUnsearchableViaParticipants = ReportUtils.isUnsearchableViaParticipants(report); + result.isSearchableViaParticipants = ReportUtils.isSearchableViaParticipants(report); const visibleParticipantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, true); @@ -1946,7 +1946,7 @@ function getOptions( } } - if (!ReportUtils.isUnsearchableViaParticipants(report)) { + if (ReportUtils.isSearchableViaParticipants(report)) { return; } } @@ -2513,7 +2513,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt // - Policy rooms // - Invoice rooms // - Trip rooms - if (!item.isUnsearchableViaParticipants) { + if (item.isSearchableViaParticipants) { values = values.concat(getParticipantsLoginsArray(item)); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 07543a420575..fd1409e81c02 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -431,7 +431,7 @@ type OptionData = { shouldShowAmountInput?: boolean; amountInputProps?: MoneyRequestAmountInputProps; tabIndex?: 0 | -1; - isUnsearchableViaParticipants?: boolean; + isSearchableViaParticipants?: boolean; } & Report; type OnyxDataTaskAssigneeChat = { @@ -975,10 +975,10 @@ function isChatRoom(report: OnyxEntry): boolean { } /** - * Whether the provided report is not searchable via participant + * Whether the provided report is searchable via participant */ -function isUnsearchableViaParticipants(report: OnyxEntry): boolean { - return isUserCreatedPolicyRoom(report) || isDefaultRoom(report) || isInvoiceRoom(report) || isTripRoom(report); +function isSearchableViaParticipants(report: OnyxEntry): boolean { + return !isChatReport(report) || isDM(report) || isGroupChat(report) || isPolicyExpenseChat(report) || isSystemChat(report) || isSelfDM(report); } /** @@ -5427,11 +5427,11 @@ function shouldReportBeInOptionList({ !isSystemChat(report) && !isGroupChat(report) && !isInvoiceRoom(report) && - // We omit sending back participants for chat rooms when searching for reports since they aren't needed to display the results and can get very large. + // We omit sending back participants for certain reports when searching for reports since they aren't needed to display the results and can get very large. // So we allow showing rooms with no participants when searching for reports. - // In any other circumstances we should never have default rooms with no participants in Onyx. + // In any other circumstances we should never have these reports with no participants in Onyx. !isSearchingForReports && - isUnsearchableViaParticipants(report)) + !isSearchableViaParticipants(report)) ) { return false; } @@ -7325,7 +7325,7 @@ export { createDraftWorkspaceAndNavigateToConfirmationScreen, isChatUsedForOnboarding, getChatUsedForOnboarding, - isUnsearchableViaParticipants, + isSearchableViaParticipants, findPolicyExpenseChatByPolicyID, }; From baad6f926ceed772bfe9ab9f2073ee09011e3a5a Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Wed, 26 Jun 2024 15:20:46 -0700 Subject: [PATCH 035/124] Add comment for confusing section --- src/libs/OptionsListUtils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 9f0e1aa832b5..9def94e98991 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1946,6 +1946,10 @@ function getOptions( } } + // Rooms that aren't searchable via participant won't have any participants returned with SearchForReports + // so we have to let their options pass through this check. + // Rooms that are searchable via participant should always have participants returned with SearchForReports, + // so we block any that don't have participants with this check. if (ReportUtils.isSearchableViaParticipants(report)) { return; } From e379688656633519aeba053e5649fa01a0205b90 Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Thu, 27 Jun 2024 06:45:05 +0700 Subject: [PATCH 036/124] Fix text input won't auto focus after back from get assistance page in workspace invite page Signed-off-by: Tsaqif --- src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts index 2a77b52e3116..bcad577fec78 100644 --- a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts +++ b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts @@ -10,6 +10,7 @@ const SCREENS_WITH_AUTOFOCUS: string[] = [ SCREENS.SETTINGS.PROFILE.PRONOUNS, SCREENS.NEW_TASK.DETAILS, SCREENS.MONEY_REQUEST.CREATE, + SCREENS.WORKSPACE.INVITE, ]; export default SCREENS_WITH_AUTOFOCUS; From a56370d1359855d30500008bd34ad6c36238eb88 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 27 Jun 2024 16:02:24 +0100 Subject: [PATCH 037/124] fix: suggestions not wrapping inside row --- src/components/AddressSearch/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index d37796f65524..2729edecc49e 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -380,10 +380,11 @@ function AddressSearch( const title = data.isPredefinedPlace ? data.name : data.structured_formatting.main_text; const subtitle = data.isPredefinedPlace ? data.description : data.structured_formatting.secondary_text; return ( - + // ScrollView is used here to allow whole screen scrolling, when the user grabs a row, on web + {!!title && {title}} {subtitle} - + ); }} onPress={(data, details) => { @@ -452,7 +453,7 @@ function AddressSearch( container: [styles.mh100], }} numberOfLines={2} - isRowScrollable={isRowScrollable} + isRowScrollable={false} listHoverColor={theme.border} listUnderlayColor={theme.buttonPressedBG} onLayout={(event: LayoutChangeEvent) => { From 0237f81c9ff35fc9e203334d291d3620c5b2d32c Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Thu, 27 Jun 2024 12:47:39 -0700 Subject: [PATCH 038/124] calculate isSearchingForReports from searchInputValue --- src/libs/OptionsListUtils.ts | 10 +++++++--- src/libs/ReportUtils.ts | 1 - 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 9def94e98991..df6b8b3ac024 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -176,7 +176,6 @@ type GetOptionsConfig = { recentlyUsedPolicyReportFieldOptions?: string[]; transactionViolations?: OnyxCollection; includeInvoiceRooms?: boolean; - isSearchingForReports?: boolean; }; type GetUserToInviteConfig = { @@ -1789,7 +1788,6 @@ function getOptions( policyReportFieldOptions = [], recentlyUsedPolicyReportFieldOptions = [], includeInvoiceRooms = false, - isSearchingForReports = false, }: GetOptionsConfig, ): Options { if (includeCategories) { @@ -1852,6 +1850,8 @@ function getOptions( const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase(); const topmostReportId = Navigation.getTopmostReportId() ?? '-1'; + const isSearchingForReports = !!searchInputValue.length; + // Filter out all the reports that shouldn't be displayed const filteredReportOptions = options.reports.filter((option) => { const report = option.item; @@ -2517,7 +2517,11 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt // - Policy rooms // - Invoice rooms // - Trip rooms - if (item.isSearchableViaParticipants) { + if (ReportUtils.isSearchableViaParticipants({ + reportID: item.reportID, + type: item.type, + chatType: item.chatType + })) { values = values.concat(getParticipantsLoginsArray(item)); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fd1409e81c02..a1f31b69d093 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5390,7 +5390,6 @@ function shouldReportBeInOptionList({ excludeEmptyChats, doesReportHaveViolations, includeSelfDM = false, - isSearchingForReports = false, }: { report: OnyxEntry; currentReportId: string; From a842879a3f98673bc7ec7e233262ee2900f038b5 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Thu, 27 Jun 2024 13:03:15 -0700 Subject: [PATCH 039/124] simplify logic and improve comment --- src/libs/OptionsListUtils.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index df6b8b3ac024..b0646f548a17 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1939,20 +1939,18 @@ function getOptions( return; } + // Rooms that aren't searchable via participant won't have any participants returned with SearchForReports + // so we don't need to check if their participants exist in order for them to qualify as a valid option. + if (isSearchingForReports && !ReportUtils.isSearchableViaParticipants(report)) { + return option; + } + if (!accountIDs || accountIDs.length === 0) { if (!isSearchingForReports) { if (!isChatRoom) { return; } } - - // Rooms that aren't searchable via participant won't have any participants returned with SearchForReports - // so we have to let their options pass through this check. - // Rooms that are searchable via participant should always have participants returned with SearchForReports, - // so we block any that don't have participants with this check. - if (ReportUtils.isSearchableViaParticipants(report)) { - return; - } } return option; From 128a3ce7c40ffa44c33c1ddbb54b573e88a14d6d Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Thu, 27 Jun 2024 13:04:27 -0700 Subject: [PATCH 040/124] update comment --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index b0646f548a17..f834ac5bb68e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1939,7 +1939,7 @@ function getOptions( return; } - // Rooms that aren't searchable via participant won't have any participants returned with SearchForReports + // Reports that aren't searchable via participant won't have any participants returned with SearchForReports // so we don't need to check if their participants exist in order for them to qualify as a valid option. if (isSearchingForReports && !ReportUtils.isSearchableViaParticipants(report)) { return option; From 8b49913d3af1be42a1e9cb17bc8286e1f90bf018 Mon Sep 17 00:00:00 2001 From: devguest Date: Thu, 27 Jun 2024 22:51:06 +0100 Subject: [PATCH 041/124] Replace capitalised "s" in the "Account settings" --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index d72681701814..34f323241feb 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -982,7 +982,7 @@ export default { }, returnToClassic: 'Switch to Expensify Classic', help: 'Help', - accountSettings: 'Account Settings', + accountSettings: 'Account settings', account: 'Account', general: 'General', }, From dfead132355c111260ee10b5361384caba6d8e3b Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Thu, 27 Jun 2024 18:50:11 -0700 Subject: [PATCH 042/124] style --- src/libs/OptionsListUtils.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f834ac5bb68e..8a9639078a34 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2515,11 +2515,13 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt // - Policy rooms // - Invoice rooms // - Trip rooms - if (ReportUtils.isSearchableViaParticipants({ - reportID: item.reportID, - type: item.type, - chatType: item.chatType - })) { + if ( + ReportUtils.isSearchableViaParticipants({ + reportID: item.reportID, + type: item.type, + chatType: item.chatType, + }) + ) { values = values.concat(getParticipantsLoginsArray(item)); } From 01c993f812d2a3ece567159ec2bf1d8bc28ae86a Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Thu, 27 Jun 2024 19:13:00 -0700 Subject: [PATCH 043/124] remove unused --- src/libs/OptionsListUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 8a9639078a34..1f7f73e9442a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2132,7 +2132,6 @@ function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = includeMoneyRequests: true, includeTasks: true, includeSelfDM: true, - isSearchingForReports: true, }); Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); From d42b35bc75f4a6585d48e1a44adc713f6fd1cbd3 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Thu, 27 Jun 2024 19:14:16 -0700 Subject: [PATCH 044/124] add back missing variable --- src/libs/ReportUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a1f31b69d093..fd1409e81c02 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5390,6 +5390,7 @@ function shouldReportBeInOptionList({ excludeEmptyChats, doesReportHaveViolations, includeSelfDM = false, + isSearchingForReports = false, }: { report: OnyxEntry; currentReportId: string; From fdadd5fa9e3f17d00f837b63bace009d9cdde6ab Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 28 Jun 2024 12:58:35 +0100 Subject: [PATCH 045/124] fix: scrolling issues --- src/components/AddressSearch/index.tsx | 16 +++++++--------- .../AddressSearch/isRowScrollable.native.ts | 8 -------- src/components/AddressSearch/isRowScrollable.ts | 6 ------ 3 files changed, 7 insertions(+), 23 deletions(-) delete mode 100644 src/components/AddressSearch/isRowScrollable.native.ts delete mode 100644 src/components/AddressSearch/isRowScrollable.ts diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 2729edecc49e..ca026ac21819 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -24,7 +24,6 @@ import CONST from '@src/CONST'; import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import CurrentLocationButton from './CurrentLocationButton'; import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer'; -import isRowScrollable from './isRowScrollable'; import type {AddressSearchProps, PredefinedPlace} from './types'; /** @@ -301,7 +300,7 @@ function AddressSearch( // eslint-disable-next-line react/jsx-no-useless-fragment <> {(predefinedPlaces?.length ?? 0) > 0 && ( - <> + {/* This will show current location button in list if there are some recent destinations */} {shouldShowCurrentLocationButton && ( )} {!value && {translate('common.recentDestinations')}} - + )} ); @@ -369,6 +368,7 @@ function AddressSearch( ref={containerRef} > + {!!title && {title}} {subtitle} - + ); }} onPress={(data, details) => { @@ -447,9 +446,9 @@ function AddressSearch( styles={{ textInputContainer: [styles.flexColumn], listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.borderLeft, styles.borderRight, !isFocused && styles.h0], - row: [styles.pv4, styles.ph3], + row: [styles.pv4, styles.ph3, styles.overflowAuto], description: [styles.googleSearchText], - separator: [styles.googleSearchSeparator], + separator: [styles.googleSearchSeparator, styles.overflowAuto], container: [styles.mh100], }} numberOfLines={2} @@ -475,7 +474,6 @@ function AddressSearch( } placeholder="" listViewDisplayed - disableScroll={!isRowScrollable} > setLocationErrorCode(null)} diff --git a/src/components/AddressSearch/isRowScrollable.native.ts b/src/components/AddressSearch/isRowScrollable.native.ts deleted file mode 100644 index aab4c3e3aa07..000000000000 --- a/src/components/AddressSearch/isRowScrollable.native.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Whether the address suggestions list is scrollable - * - * On native, this is set to false because the whole screen is scrollable - */ -const isRowScrollable = false; - -export default isRowScrollable; diff --git a/src/components/AddressSearch/isRowScrollable.ts b/src/components/AddressSearch/isRowScrollable.ts deleted file mode 100644 index 3627a5b218a7..000000000000 --- a/src/components/AddressSearch/isRowScrollable.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Whether the address suggestions list is scrollable - */ -const isRowScrollable = true; - -export default isRowScrollable; From ecf4bfe3b73eddd8edbef5328385066733760cae Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 10:59:06 -0700 Subject: [PATCH 046/124] remove unused logic --- src/libs/OptionsListUtils.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 1f7f73e9442a..a7de63aa7eeb 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1945,12 +1945,8 @@ function getOptions( return option; } - if (!accountIDs || accountIDs.length === 0) { - if (!isSearchingForReports) { - if (!isChatRoom) { - return; - } - } + if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) { + return; } return option; @@ -2603,7 +2599,7 @@ export { sortCategories, sortTags, getCategoryOptionTree, - hasEnabledTags, + hasEnabledTags formatMemberForList, formatSectionsFromSearchTerm, getShareLogOptions, From 1835257c6df8013555c7f6d421c588f2c367c765 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 11:05:55 -0700 Subject: [PATCH 047/124] improve comment --- src/libs/OptionsListUtils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index a7de63aa7eeb..b463ef8ebd72 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1941,6 +1941,10 @@ function getOptions( // Reports that aren't searchable via participant won't have any participants returned with SearchForReports // so we don't need to check if their participants exist in order for them to qualify as a valid option. + // This doesn't necessarily mean these reports are empty, we just do this because: + // - they don't need participants to be displayed as an option, and we'll load all the participant data again if the user selects the report from the option list + // - it improves query performance in SearchForReports + // - it drastically reduces response size if (isSearchingForReports && !ReportUtils.isSearchableViaParticipants(report)) { return option; } From edcb7ef509e98efdf0edaa7d8f41c98d100b88d4 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 11:06:19 -0700 Subject: [PATCH 048/124] remove unused comment --- src/libs/OptionsListUtils.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index b463ef8ebd72..d67bf29ed1a8 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2509,11 +2509,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt values = values.concat(getParticipantsLoginsArray(item)); } - // We don't want the following to be searchable by participant: - // - Default rooms - // - Policy rooms - // - Invoice rooms - // - Trip rooms if ( ReportUtils.isSearchableViaParticipants({ reportID: item.reportID, From 131ae678c7c523267334474d4767de6522eede50 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 11:53:55 -0700 Subject: [PATCH 049/124] Use isChatRoom to decide whether to display reports with empty participants --- src/libs/OptionsListUtils.ts | 16 ++-------------- src/libs/ReportUtils.ts | 2 +- src/pages/ChatFinderPage/index.tsx | 2 +- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index d67bf29ed1a8..932dc53b03df 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1850,8 +1850,6 @@ function getOptions( const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase(); const topmostReportId = Navigation.getTopmostReportId() ?? '-1'; - const isSearchingForReports = !!searchInputValue.length; - // Filter out all the reports that shouldn't be displayed const filteredReportOptions = options.reports.filter((option) => { const report = option.item; @@ -1872,7 +1870,7 @@ function getOptions( isInFocusMode: false, excludeEmptyChats: false, includeSelfDM, - isSearchingForReports, + isSearchingForReports: !!searchInputValue.length, }); }); @@ -1939,16 +1937,6 @@ function getOptions( return; } - // Reports that aren't searchable via participant won't have any participants returned with SearchForReports - // so we don't need to check if their participants exist in order for them to qualify as a valid option. - // This doesn't necessarily mean these reports are empty, we just do this because: - // - they don't need participants to be displayed as an option, and we'll load all the participant data again if the user selects the report from the option list - // - it improves query performance in SearchForReports - // - it drastically reduces response size - if (isSearchingForReports && !ReportUtils.isSearchableViaParticipants(report)) { - return option; - } - if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) { return; } @@ -2598,7 +2586,7 @@ export { sortCategories, sortTags, getCategoryOptionTree, - hasEnabledTags + hasEnabledTags, formatMemberForList, formatSectionsFromSearchTerm, getShareLogOptions, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fd1409e81c02..3e3af2d706d1 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -971,7 +971,7 @@ function isOpenInvoiceReport(report: OnyxEntry | EmptyObject): boolean { * Whether the provided report is a chat room */ function isChatRoom(report: OnyxEntry): boolean { - return isUserCreatedPolicyRoom(report) || isDefaultRoom(report) || isInvoiceRoom(report); + return isUserCreatedPolicyRoom(report) || isDefaultRoom(report) || isInvoiceRoom(report) || isTripRoom(report); } /** diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx index cb3d0b1eafd9..3b6e5a0d822c 100644 --- a/src/pages/ChatFinderPage/index.tsx +++ b/src/pages/ChatFinderPage/index.tsx @@ -93,7 +93,7 @@ function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPa headerMessage: '', }; } - const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); + const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValueInServer.trim(), betas ?? []); const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, !!optionList.userToInvite, ''); return {...optionList, headerMessage: header}; }, [areOptionsInitialized, betas, isScreenTransitionEnd, options]); From a133dde688cf38525c31ccf9917374eccefc5bd2 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 11:56:09 -0700 Subject: [PATCH 050/124] remove unused --- src/libs/OptionsListUtils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 932dc53b03df..f1402fabc88a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -808,9 +808,6 @@ function createOption( result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; result.policyID = report.policyID; result.isSelfDM = ReportUtils.isSelfDM(report); - result.isTripRoom = ReportUtils.isTripRoom(report); - result.policyName = ReportUtils.getPolicyName(report); - result.isSearchableViaParticipants = ReportUtils.isSearchableViaParticipants(report); const visibleParticipantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, true); From 8e0ca9009ab3c7d09a7b5f9f5d45051a45b0e800 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 11:58:23 -0700 Subject: [PATCH 051/124] remove unused --- src/libs/OptionsListUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f1402fabc88a..fb6d4a5f7de9 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -769,11 +769,9 @@ function createOption( isPolicyExpenseChat: false, isOwnPolicyExpenseChat: false, isExpenseReport: false, - isTripRoom: false, policyID: undefined, isOptimisticPersonalDetail: false, lastMessageText: '', - isSearchableViaParticipants: false, }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); From 09870fc75cadd1933275d7694bd2fb045729fd9e Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 11:59:32 -0700 Subject: [PATCH 052/124] remove unused --- src/libs/ReportUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 3e3af2d706d1..16196876920e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -412,7 +412,6 @@ type OptionData = { displayNamesWithTooltips?: DisplayNameWithTooltips | null; isDefaultRoom?: boolean; isInvoiceRoom?: boolean; - isTripRoom?: boolean; isExpenseReport?: boolean; isOptimisticPersonalDetail?: boolean; selected?: boolean; @@ -431,7 +430,6 @@ type OptionData = { shouldShowAmountInput?: boolean; amountInputProps?: MoneyRequestAmountInputProps; tabIndex?: 0 | -1; - isSearchableViaParticipants?: boolean; } & Report; type OnyxDataTaskAssigneeChat = { From 7c124837f1cb3272908ebceedfd47498787326a5 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 12:05:52 -0700 Subject: [PATCH 053/124] simplify logic --- 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 16196876920e..ca8f8b9c202b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -976,7 +976,7 @@ function isChatRoom(report: OnyxEntry): boolean { * Whether the provided report is searchable via participant */ function isSearchableViaParticipants(report: OnyxEntry): boolean { - return !isChatReport(report) || isDM(report) || isGroupChat(report) || isPolicyExpenseChat(report) || isSystemChat(report) || isSelfDM(report); + return !isChatReport(report) || !isChatRoom(report); } /** From cae1a80349767471783b3d17556a6d7399d19545 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 15:29:30 -0700 Subject: [PATCH 054/124] remove isSearchableViaParticipants --- src/libs/OptionsListUtils.ts | 9 ++------- src/libs/ReportUtils.ts | 12 ++---------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fb6d4a5f7de9..f02c1fe4bfff 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2492,13 +2492,8 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt values = values.concat(getParticipantsLoginsArray(item)); } - if ( - ReportUtils.isSearchableViaParticipants({ - reportID: item.reportID, - type: item.type, - chatType: item.chatType, - }) - ) { + const partialReport = {reportID: item.reportID, type: item.type, chatType: item.chatType}; + if (!ReportUtils.isChatReport(partialReport) || !ReportUtils.isChatRoom(partialReport)) { values = values.concat(getParticipantsLoginsArray(item)); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ca8f8b9c202b..93c2bd251d34 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -972,13 +972,6 @@ function isChatRoom(report: OnyxEntry): boolean { return isUserCreatedPolicyRoom(report) || isDefaultRoom(report) || isInvoiceRoom(report) || isTripRoom(report); } -/** - * Whether the provided report is searchable via participant - */ -function isSearchableViaParticipants(report: OnyxEntry): boolean { - return !isChatReport(report) || !isChatRoom(report); -} - /** * Whether the provided report is a public room */ @@ -5425,11 +5418,11 @@ function shouldReportBeInOptionList({ !isSystemChat(report) && !isGroupChat(report) && !isInvoiceRoom(report) && - // We omit sending back participants for certain reports when searching for reports since they aren't needed to display the results and can get very large. + // We omit sending back participants for chat rooms when searching for reports since they aren't needed to display the results and can get very large. // So we allow showing rooms with no participants when searching for reports. // In any other circumstances we should never have these reports with no participants in Onyx. !isSearchingForReports && - !isSearchableViaParticipants(report)) + !isChatRoom(report)) ) { return false; } @@ -7323,7 +7316,6 @@ export { createDraftWorkspaceAndNavigateToConfirmationScreen, isChatUsedForOnboarding, getChatUsedForOnboarding, - isSearchableViaParticipants, findPolicyExpenseChatByPolicyID, }; From a202b1eb81ca12c156328d6933bb36b379da955f Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 15:32:18 -0700 Subject: [PATCH 055/124] remove unused --- src/libs/ReportUtils.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 93c2bd251d34..75a32fe55916 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5381,7 +5381,6 @@ function shouldReportBeInOptionList({ excludeEmptyChats, doesReportHaveViolations, includeSelfDM = false, - isSearchingForReports = false, }: { report: OnyxEntry; currentReportId: string; @@ -5391,7 +5390,6 @@ function shouldReportBeInOptionList({ excludeEmptyChats: boolean; doesReportHaveViolations: boolean; includeSelfDM?: boolean; - isSearchingForReports?: boolean; }) { const isInDefaultMode = !isInFocusMode; // Exclude reports that have no data because there wouldn't be anything to show in the option item. @@ -5419,9 +5417,7 @@ function shouldReportBeInOptionList({ !isGroupChat(report) && !isInvoiceRoom(report) && // We omit sending back participants for chat rooms when searching for reports since they aren't needed to display the results and can get very large. - // So we allow showing rooms with no participants when searching for reports. - // In any other circumstances we should never have these reports with no participants in Onyx. - !isSearchingForReports && + // So we allow showing rooms with no participants–in any other circumstances we should never have these reports with no participants in Onyx. !isChatRoom(report)) ) { return false; From 88a6564bee46724abf1ea3fbd690061ba9239662 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 16:02:45 -0700 Subject: [PATCH 056/124] add missing hook dependency --- src/pages/ChatFinderPage/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx index 3b6e5a0d822c..75b11990742e 100644 --- a/src/pages/ChatFinderPage/index.tsx +++ b/src/pages/ChatFinderPage/index.tsx @@ -96,7 +96,7 @@ function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPa const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValueInServer.trim(), betas ?? []); const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, !!optionList.userToInvite, ''); return {...optionList, headerMessage: header}; - }, [areOptionsInitialized, betas, isScreenTransitionEnd, options]); + }, [areOptionsInitialized, betas, isScreenTransitionEnd, options, debouncedSearchValueInServer]); const filteredOptions = useMemo(() => { if (debouncedSearchValue.trim() === '') { From 802a3d0680a99df640715324992283850e3aa92d Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 28 Jun 2024 16:03:12 -0700 Subject: [PATCH 057/124] remove unused param --- src/libs/OptionsListUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f02c1fe4bfff..fadb47f5c72a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1865,7 +1865,6 @@ function getOptions( isInFocusMode: false, excludeEmptyChats: false, includeSelfDM, - isSearchingForReports: !!searchInputValue.length, }); }); From f6f13a4e274c05a9931787218838f6f8e6b8f666 Mon Sep 17 00:00:00 2001 From: truph01 Date: Mon, 1 Jul 2024 12:33:54 +0700 Subject: [PATCH 058/124] Fix: After inviting a member navigating back takes you back to members page --- src/pages/workspace/WorkspaceInviteMessagePage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 9bab92bc38b0..fc394890b5c7 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -114,8 +114,7 @@ function WorkspaceInviteMessagePage({ debouncedSaveDraft(null); SearchInputManager.searchInput = ''; // Pop the invite message page before navigating to the members page. - Navigation.goBack(); - Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(route.params.policyID)); + Navigation.dismissModal(); }; /** Opens privacy url as an external link */ From 4b55bd9e078aeb337a7da9132bdb19dc71f0c819 Mon Sep 17 00:00:00 2001 From: dominictb Date: Mon, 1 Jul 2024 18:36:04 +0700 Subject: [PATCH 059/124] fix: Design updates to Search rows on mobile --- .../Search/TransactionListItemRow.tsx | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index 0adc7ee21fd1..3d65638c43ca 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -110,6 +110,9 @@ function MerchantCell({transactionItem, showTooltip, isLargeScreenWidth}: Transa const {translate} = useLocalize(); const description = TransactionUtils.getDescription(transactionItem); let merchant = transactionItem.shouldShowMerchant ? transactionItem.formattedMerchant : description; + if (!isLargeScreenWidth) { + merchant = transactionItem.shouldShowMerchant ? (transactionItem.formattedMerchant ? transactionItem.formattedMerchant : description) : description; + } if (TransactionUtils.hasReceipt(transactionItem) && TransactionUtils.isReceiptBeingScanned(transactionItem) && transactionItem.shouldShowMerchant) { merchant = translate('iou.receiptStatusTitle'); @@ -159,18 +162,11 @@ function TypeCell({transactionItem, isLargeScreenWidth}: TransactionCellProps) { function CategoryCell({isLargeScreenWidth, showTooltip, transactionItem}: TransactionCellProps) { const styles = useThemeStyles(); - return isLargeScreenWidth ? ( + return ( - ) : ( - ); } @@ -234,24 +230,21 @@ function TransactionListItemRow({item, showTooltip, onButtonPress, showItemHeade isLargeScreenWidth={false} showTooltip={false} /> - + - - - - + {item.category && ( + + + + )} From 814baa71317ee985cd51b1dd751a3a438a4cc9cb Mon Sep 17 00:00:00 2001 From: dominictb Date: Mon, 1 Jul 2024 18:53:51 +0700 Subject: [PATCH 060/124] fix: lint --- .../SelectionList/Search/TransactionListItemRow.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index 3d65638c43ca..81f5a95fb1fb 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -109,10 +109,11 @@ function MerchantCell({transactionItem, showTooltip, isLargeScreenWidth}: Transa const styles = useThemeStyles(); const {translate} = useLocalize(); const description = TransactionUtils.getDescription(transactionItem); - let merchant = transactionItem.shouldShowMerchant ? transactionItem.formattedMerchant : description; - if (!isLargeScreenWidth) { - merchant = transactionItem.shouldShowMerchant ? (transactionItem.formattedMerchant ? transactionItem.formattedMerchant : description) : description; + let merchantOrDescriptionToDisplay = transactionItem.formattedMerchant; + if (!merchantOrDescriptionToDisplay && !isLargeScreenWidth) { + merchantOrDescriptionToDisplay = description; } + let merchant = transactionItem.shouldShowMerchant ? merchantOrDescriptionToDisplay : description; if (TransactionUtils.hasReceipt(transactionItem) && TransactionUtils.isReceiptBeingScanned(transactionItem) && transactionItem.shouldShowMerchant) { merchant = translate('iou.receiptStatusTitle'); From d686ba9bc92dd4085bce4836b0ae8938d8079ac1 Mon Sep 17 00:00:00 2001 From: truph01 Date: Mon, 1 Jul 2024 19:12:01 +0700 Subject: [PATCH 061/124] Fix: Remove comment --- src/pages/workspace/WorkspaceInviteMessagePage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index fc394890b5c7..753f2e4e9728 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -113,7 +113,6 @@ function WorkspaceInviteMessagePage({ Member.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, route.params.policyID); debouncedSaveDraft(null); SearchInputManager.searchInput = ''; - // Pop the invite message page before navigating to the members page. Navigation.dismissModal(); }; From 9ca7cea01fa5cf2df13b94cd3ee3c248269d14e7 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 1 Jul 2024 11:43:22 -0700 Subject: [PATCH 062/124] remove duplicate checks --- src/libs/ReportUtils.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 75a32fe55916..dc5e7ccd148e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5406,19 +5406,16 @@ function shouldReportBeInOptionList({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing report?.isHidden || (participantAccountIDs.length === 0 && + // We omit sending back participants for chat rooms when searching for reports since they aren't needed to display the results and can get very large. + // So we allow showing rooms with no participants–in any other circumstances we should never have these reports with no participants in Onyx. + !isChatRoom(report) && !isChatThread(report) && - !isPublicRoom(report) && - !isUserCreatedPolicyRoom(report) && !isArchivedRoom(report) && !isMoneyRequestReport(report) && !isTaskReport(report) && !isSelfDM(report) && !isSystemChat(report) && - !isGroupChat(report) && - !isInvoiceRoom(report) && - // We omit sending back participants for chat rooms when searching for reports since they aren't needed to display the results and can get very large. - // So we allow showing rooms with no participants–in any other circumstances we should never have these reports with no participants in Onyx. - !isChatRoom(report)) + !isGroupChat(report)) ) { return false; } From d1c6c0072fe71278e2b68922d5d280bde34e3473 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 1 Jul 2024 12:03:06 -0700 Subject: [PATCH 063/124] condense logic and add comment --- src/libs/OptionsListUtils.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fadb47f5c72a..f7083d8c2426 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2473,11 +2473,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt values.push(item.login.replace(emailRegex, '')); } - if (!item.isChatRoom) { - const participantNames = getParticipantNames(item.participantsList ?? []); - values = values.concat(Array.from(participantNames)); - } - if (item.isThread) { if (item.alternateText) { values.push(item.alternateText); @@ -2491,8 +2486,12 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt values = values.concat(getParticipantsLoginsArray(item)); } + // We don't want chat rooms to be searchable via participants, so we only add participant logins/display + // if the report in question isn't a chat room. const partialReport = {reportID: item.reportID, type: item.type, chatType: item.chatType}; if (!ReportUtils.isChatReport(partialReport) || !ReportUtils.isChatRoom(partialReport)) { + const participantNames = getParticipantNames(item.participantsList ?? []); + values = values.concat(Array.from(participantNames)); values = values.concat(getParticipantsLoginsArray(item)); } From f5dc4f700e3b230179ed8e061f3e51a9750d5e48 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 1 Jul 2024 13:43:11 -0700 Subject: [PATCH 064/124] remove unused --- src/pages/ChatFinderPage/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx index 75b11990742e..cb3d0b1eafd9 100644 --- a/src/pages/ChatFinderPage/index.tsx +++ b/src/pages/ChatFinderPage/index.tsx @@ -93,10 +93,10 @@ function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPa headerMessage: '', }; } - const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValueInServer.trim(), betas ?? []); + const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, !!optionList.userToInvite, ''); return {...optionList, headerMessage: header}; - }, [areOptionsInitialized, betas, isScreenTransitionEnd, options, debouncedSearchValueInServer]); + }, [areOptionsInitialized, betas, isScreenTransitionEnd, options]); const filteredOptions = useMemo(() => { if (debouncedSearchValue.trim() === '') { From a72642a34263e287a0a228dbb4272bd0fe570f42 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 2 Jul 2024 16:54:05 +0800 Subject: [PATCH 065/124] make merchant optional in invoice --- src/components/MoneyRequestConfirmationList.tsx | 2 +- src/pages/iou/request/step/IOURequestStepMerchant.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 8bfcbbeb779e..88f3b56538d9 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -284,7 +284,7 @@ function MoneyRequestConfirmationList({ }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); const isMerchantEmpty = useMemo(() => !iouMerchant || TransactionUtils.isMerchantMissing(transaction), [transaction, iouMerchant]); - const isMerchantRequired = (isPolicyExpenseChat || isTypeInvoice) && (!isScanRequest || isEditingSplitBill) && shouldShowMerchant; + const isMerchantRequired = isPolicyExpenseChat && (!isScanRequest || isEditingSplitBill) && shouldShowMerchant; const isCategoryRequired = !!policy?.requiresCategory; diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index f723c3280400..b9816c7daf7b 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -60,11 +60,10 @@ function IOURequestStepMerchant({ // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value const isEditingSplitBill = iouType === CONST.IOU.TYPE.SPLIT && isEditing; - const isTypeInvoice = iouType === CONST.IOU.TYPE.INVOICE; const merchant = ReportUtils.getTransactionDetails(isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction)?.merchant; const isEmptyMerchant = merchant === '' || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const isMerchantRequired = ReportUtils.isReportInGroupPolicy(report) || isTypeInvoice || transaction?.participants?.some((participant) => !!participant.isPolicyExpenseChat); + const isMerchantRequired = ReportUtils.isExpenseRequest(report) || transaction?.participants?.some((participant) => !!participant.isPolicyExpenseChat); const navigateBack = () => { Navigation.goBack(backTo); From 36b4b234d0e51b1ab99fb364426a7f5e73705960 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 2 Jul 2024 15:37:08 +0200 Subject: [PATCH 066/124] Implement Parser --- src/libs/OnyxAwareParser.ts | 52 -------------------------------- src/libs/Parser.ts | 59 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 52 deletions(-) delete mode 100644 src/libs/OnyxAwareParser.ts create mode 100644 src/libs/Parser.ts diff --git a/src/libs/OnyxAwareParser.ts b/src/libs/OnyxAwareParser.ts deleted file mode 100644 index c058775341c2..000000000000 --- a/src/libs/OnyxAwareParser.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {ExpensiMark} from 'expensify-common'; -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; - -const parser = new ExpensiMark(); - -const reportIDToNameMap: Record = {}; -const accountIDToNameMap: Record = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - callback: (report) => { - if (!report) { - return; - } - - reportIDToNameMap[report.reportID] = report.reportName ?? report.displayName ?? report.reportID; - }, -}); - -Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (personalDetailsList) => { - Object.values(personalDetailsList ?? {}).forEach((personalDetails) => { - if (!personalDetails) { - return; - } - - accountIDToNameMap[personalDetails.accountID] = personalDetails.login ?? String(personalDetails.accountID); - }); - }, -}); - -function parseHtmlToMarkdown( - html: string, - reportIDToName?: Record, - accountIDToName?: Record, - cacheVideoAttributes?: (videoSource: string, videoAttrs: string) => void, -): string { - return parser.htmlToMarkdown(html, {reportIDToName: reportIDToName ?? reportIDToNameMap, accountIDToName: accountIDToName ?? accountIDToNameMap, cacheVideoAttributes}); -} - -function parseHtmlToText( - html: string, - reportIDToName?: Record, - accountIDToName?: Record, - cacheVideoAttributes?: (videoSource: string, videoAttrs: string) => void, -): string { - return parser.htmlToText(html, {reportIDToName: reportIDToName ?? reportIDToNameMap, accountIDToName: accountIDToName ?? accountIDToNameMap, cacheVideoAttributes}); -} - -export {parseHtmlToMarkdown, parseHtmlToText}; diff --git a/src/libs/Parser.ts b/src/libs/Parser.ts new file mode 100644 index 000000000000..746b9670a295 --- /dev/null +++ b/src/libs/Parser.ts @@ -0,0 +1,59 @@ +import {ExpensiMark} from 'expensify-common'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; + +const reportIDToNameMap: Record = {}; +const accountIDToNameMap: Record = {}; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + callback: (report) => { + if (!report) { + return; + } + + reportIDToNameMap[report.reportID] = report.reportName ?? report.displayName ?? report.reportID; + }, +}); + +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (personalDetailsList) => { + Object.values(personalDetailsList ?? {}).forEach((personalDetails) => { + if (!personalDetails) { + return; + } + + accountIDToNameMap[personalDetails.accountID] = personalDetails.login ?? String(personalDetails.accountID); + }); + }, +}); + +type Extras = { + reportIDToName?: Record; + accountIDToName?: Record; + cacheVideoAttributes?: (vidSource: string, attrs: string) => void; + videoAttributeCache?: Record; +}; + +class ExpensiMarkWithContext extends ExpensiMark { + htmlToMarkdown(htmlString: string, extras?: Extras): string { + return super.htmlToMarkdown(htmlString, { + reportIDToName: extras?.reportIDToName ?? reportIDToNameMap, + accountIDToName: extras?.accountIDToName ?? accountIDToNameMap, + cacheVideoAttributes: extras?.cacheVideoAttributes, + }); + } + + htmlToText(htmlString: string, extras?: Extras): string { + return super.htmlToText(htmlString, { + reportIDToName: extras?.reportIDToName ?? reportIDToNameMap, + accountIDToName: extras?.accountIDToName ?? accountIDToNameMap, + cacheVideoAttributes: extras?.cacheVideoAttributes, + }); + } +} + +const Parser = new ExpensiMarkWithContext(); + +export default Parser; From 29470645763e01a4c8a320166b368361a74ab698 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 2 Jul 2024 15:37:30 +0200 Subject: [PATCH 067/124] Disallow importing ExpeniMark --- .eslintrc.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 22bb0158bc8e..ae3cedaec690 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,7 +8,7 @@ const restrictedImportPaths = [ '', "For 'useWindowDimensions', please use '@src/hooks/useWindowDimensions' instead.", "For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from '@components/Pressable' instead.", - "For 'StatusBar', please use '@src/libs/StatusBar' instead.", + "For 'StatusBar', please use '@libs/StatusBar' instead.", "For 'Text', please use '@components/Text' instead.", "For 'ScrollView', please use '@components/ScrollView' instead.", ].join('\n'), @@ -59,8 +59,12 @@ const restrictedImportPaths = [ }, { name: 'expensify-common', - importNames: ['Device'], - message: "Do not import Device directly, it's known to make VSCode's IntelliSense crash. Please import the desired module from `expensify-common/dist/Device` instead.", + importNames: ['Device', 'ExpensiMark'], + message: [ + '', + "For 'Device', do not import it directly, it's known to make VSCode's IntelliSense crash. Please import the desired module from `expensify-common/dist/Device` instead.", + "For 'ExpensiMark', please use '@libs/Parser' instead.", + ].join('\n'), }, ]; From 1912a3610776ef570304711db50c7b47b2b793ee Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 2 Jul 2024 15:37:57 +0200 Subject: [PATCH 068/124] Ignore ExpeniMark import --- src/libs/Parser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Parser.ts b/src/libs/Parser.ts index 746b9670a295..f979b3b5c563 100644 --- a/src/libs/Parser.ts +++ b/src/libs/Parser.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line no-restricted-imports import {ExpensiMark} from 'expensify-common'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; From 00b1c98d3fe9131c5d7cd63427246bfaf701325d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 2 Jul 2024 15:38:25 +0200 Subject: [PATCH 069/124] Change usage of OnyxAwareParser --- src/components/LHNOptionsList/OptionRowLHN.tsx | 4 ++-- src/hooks/useCopySelectionHelper.ts | 6 +++--- src/hooks/useHtmlPaste/index.ts | 4 ++-- src/libs/ReportActionsUtils.ts | 6 +++--- src/libs/ReportUtils.ts | 12 ++++++------ src/libs/actions/Report.ts | 6 +++--- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 6 +++--- src/pages/RoomDescriptionPage.tsx | 4 ++-- .../home/report/ContextMenu/ContextMenuActions.tsx | 8 ++++---- .../ComposerWithSuggestions.tsx | 4 ++-- .../home/report/ReportActionItemMessageEdit.tsx | 4 ++-- src/pages/tasks/NewTaskDescriptionPage.tsx | 4 ++-- src/pages/tasks/NewTaskDetailsPage.tsx | 6 +++--- src/pages/tasks/TaskDescriptionPage.tsx | 6 +++--- src/pages/workspace/WorkspaceInviteMessagePage.tsx | 4 ++-- .../workspace/WorkspaceProfileDescriptionPage.tsx | 4 ++-- src/pages/workspace/taxes/NamePage.tsx | 4 ++-- 17 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index c7797a37fd12..ffb31b3db7ec 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -20,8 +20,8 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import DomUtils from '@libs/DomUtils'; -import {parseHtmlToText} from '@libs/OnyxAwareParser'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import Parser from '@libs/Parser'; import Performance from '@libs/Performance'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportUtils from '@libs/ReportUtils'; @@ -242,7 +242,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti numberOfLines={1} accessibilityLabel={translate('accessibilityHints.lastChatMessagePreview')} > - {parseHtmlToText(optionItem.alternateText)} + {Parser.htmlToText(optionItem.alternateText)} ) : null} diff --git a/src/hooks/useCopySelectionHelper.ts b/src/hooks/useCopySelectionHelper.ts index ed379bfcf2e6..9bcb9b8b0139 100644 --- a/src/hooks/useCopySelectionHelper.ts +++ b/src/hooks/useCopySelectionHelper.ts @@ -1,7 +1,7 @@ import {useEffect} from 'react'; import Clipboard from '@libs/Clipboard'; import KeyboardShortcut from '@libs/KeyboardShortcut'; -import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import SelectionScraper from '@libs/SelectionScraper'; import CONST from '@src/CONST'; @@ -11,10 +11,10 @@ function copySelectionToClipboard() { return; } if (!Clipboard.canSetHtml()) { - Clipboard.setString(parseHtmlToMarkdown(selection)); + Clipboard.setString(Parser.htmlToMarkdown(selection)); return; } - Clipboard.setHtml(selection, parseHtmlToText(selection)); + Clipboard.setHtml(selection, Parser.htmlToText(selection)); } export default function useCopySelectionHelper() { diff --git a/src/hooks/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts index 4705a170c3bd..a17bc0134333 100644 --- a/src/hooks/useHtmlPaste/index.ts +++ b/src/hooks/useHtmlPaste/index.ts @@ -1,6 +1,6 @@ import {useNavigation} from '@react-navigation/native'; import {useCallback, useEffect} from 'react'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import type UseHtmlPaste from './types'; const insertByCommand = (text: string) => { @@ -71,7 +71,7 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi */ const handlePastedHTML = useCallback( (html: string) => { - paste(parseHtmlToMarkdown(html)); + paste(Parser.htmlToMarkdown(html)); }, [paste], ); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index c8599d785b22..f47ae594d1a5 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -20,7 +20,7 @@ import isReportMessageAttachment from './isReportMessageAttachment'; import * as Localize from './Localize'; import Log from './Log'; import type {MessageElementBase, MessageTextElement} from './MessageElement'; -import {parseHtmlToText} from './OnyxAwareParser'; +import {Parser.htmlToText} from './OnyxAwareParser'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; @@ -1142,11 +1142,11 @@ function getReportActionText(reportAction: PartialReportAction): string { // Sometime html can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const text = (message?.html || message?.text) ?? ''; - return text ? parseHtmlToText(text) : ''; + return text ? Parser.htmlToText(text) : ''; } function getTextFromHtml(html?: string): string { - return html ? parseHtmlToText(html) : ''; + return html ? Parser.htmlToText(html) : ''; } function getMemberChangeMessageFragment(reportAction: OnyxEntry): Message { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index cf47864a779e..1fe4e6b3ea42 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -63,7 +63,7 @@ import ModifiedExpenseMessage from './ModifiedExpenseMessage'; import linkingConfig from './Navigation/linkingConfig'; import Navigation from './Navigation/Navigation'; import * as NumberUtils from './NumberUtils'; -import {parseHtmlToText} from './OnyxAwareParser'; +import {Parser.htmlToText} from './OnyxAwareParser'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PhoneNumber from './PhoneNumber'; @@ -3288,7 +3288,7 @@ function parseReportActionHtmlToText(reportAction: OnyxEntry, repo const logins = PersonalDetailsUtils.getLoginsByAccountIDs(accountIDs); accountIDs.forEach((id, index) => (accountIDToName[id] = logins[index])); - const textMessage = Str.removeSMSDomain(parseHtmlToText(html, reportIDToName, accountIDToName)); + const textMessage = Str.removeSMSDomain(Parser.htmlToText(html, reportIDToName, accountIDToName)); parsedReportActionMessageCache[key] = textMessage; return textMessage; @@ -3660,7 +3660,7 @@ function getReportDescriptionText(report: Report): string { return ''; } - return parseHtmlToText(report.description); + return Parser.htmlToText(report.description); } function getPolicyDescriptionText(policy: OnyxEntry): string { @@ -3668,7 +3668,7 @@ function getPolicyDescriptionText(policy: OnyxEntry): string { return ''; } - return parseHtmlToText(policy.description); + return Parser.htmlToText(policy.description); } function buildOptimisticAddCommentReportAction( @@ -3690,10 +3690,10 @@ function buildOptimisticAddCommentReportAction( textForNewComment = CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML; } else if (isTextOnly) { htmlForNewComment = commentText; - textForNewComment = parseHtmlToText(htmlForNewComment); + textForNewComment = Parser.htmlToText(htmlForNewComment); } else { htmlForNewComment = `${commentText}${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`; - textForNewComment = `${parseHtmlToText(commentText)}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`; + textForNewComment = `${Parser.htmlToText(commentText)}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`; } const isAttachment = !text && file !== undefined; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d05f77f9c7ac..7e0e583e6ec0 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -61,7 +61,7 @@ import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {NetworkStatus} from '@libs/NetworkConnection'; import LocalNotification from '@libs/Notification/LocalNotification'; -import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; @@ -1495,7 +1495,7 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry ReportActions.getDraftPrivateNote(report.reportID).trim() || parseHtmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(), + () => ReportActions.getDraftPrivateNote(report.reportID).trim() || Parser.htmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(), ); /** @@ -93,7 +93,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report, session}: Pri const originalNote = report?.privateNotes?.[Number(route.params.accountID)]?.note ?? ''; let editedNote = ''; if (privateNote.trim() !== originalNote.trim()) { - editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), parseHtmlToMarkdown(originalNote).trim()); + editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), Parser.htmlToMarkdown(originalNote).trim()); ReportActions.updatePrivateNotes(report.reportID, Number(route.params.accountID), editedNote); } diff --git a/src/pages/RoomDescriptionPage.tsx b/src/pages/RoomDescriptionPage.tsx index 9f8586292895..1d64ca9e1129 100644 --- a/src/pages/RoomDescriptionPage.tsx +++ b/src/pages/RoomDescriptionPage.tsx @@ -12,7 +12,7 @@ import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import variables from '@styles/variables'; @@ -32,7 +32,7 @@ type RoomDescriptionPageProps = { function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) { const styles = useThemeStyles(); - const [description, setDescription] = useState(() => parseHtmlToMarkdown(report?.description ?? '')); + const [description, setDescription] = useState(() => Parser.htmlToMarkdown(report?.description ?? '')); const reportDescriptionInputRef = useRef(null); const focusTimeoutRef = useRef | null>(null); const {translate} = useLocalize(); diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index bf634b4ac8ae..45045912ed50 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -18,7 +18,7 @@ import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import * as Localize from '@libs/Localize'; import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import Navigation from '@libs/Navigation/Navigation'; -import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -42,11 +42,11 @@ function getActionHtml(reportAction: OnyxInputOrEntry): string { /** Sets the HTML string to Clipboard */ function setClipboardMessage(content: string) { if (!Clipboard.canSetHtml()) { - Clipboard.setString(parseHtmlToMarkdown(content)); + Clipboard.setString(Parser.htmlToMarkdown(content)); } else { const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; const isAnchorTag = anchorRegex.test(content); - const plainText = isAnchorTag ? parseHtmlToMarkdown(content) : parseHtmlToText(content); + const plainText = isAnchorTag ? Parser.htmlToMarkdown(content) : Parser.htmlToText(content); Clipboard.setHtml(content, plainText); } } @@ -238,7 +238,7 @@ const ContextMenuActions: ContextMenuAction[] = [ } const editAction = () => { if (!draftMessage) { - Report.saveReportActionDraft(reportID, reportAction, parseHtmlToMarkdown(getActionHtml(reportAction))); + Report.saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); } else { Report.deleteReportActionDraft(reportID, reportAction); } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index c744ad589306..81d5fc58ae12 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -38,7 +38,7 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import getPlatform from '@libs/getPlatform'; import * as KeyDownListener from '@libs/KeyboardShortcut/KeyDownPressListener'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -543,7 +543,7 @@ function ComposerWithSuggestions( event.preventDefault(); if (lastReportAction) { const message = Array.isArray(lastReportAction?.message) ? lastReportAction?.message?.at(-1) ?? null : lastReportAction?.message ?? null; - Report.saveReportActionDraft(reportID, lastReportAction, parseHtmlToMarkdown(message?.html ?? '')); + Report.saveReportActionDraft(reportID, lastReportAction, Parser.htmlToMarkdown(message?.html ?? '')); } } }, diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index d3c8ca3af8de..2d87d84dbd6b 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -26,8 +26,8 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import type {Selection} from '@libs/focusComposerWithDelay/types'; import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; import onyxSubscribe from '@libs/onyxSubscribe'; +import Parser from '@libs/Parser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -113,7 +113,7 @@ function ReportActionItemMessageEdit( useEffect(() => { draftMessageVideoAttributeCache.clear(); - const originalMessage = parseHtmlToMarkdown(ReportActionsUtils.getReportActionHtml(action), undefined, undefined, (videoSource, attrs) => { + const originalMessage = Parser.htmlToMarkdown(ReportActionsUtils.getReportActionHtml(action), undefined, undefined, (videoSource, attrs) => { draftMessageVideoAttributeCache.set(videoSource, attrs); }); if (ReportActionsUtils.isDeletedAction(action) || !!(action.message && draftMessage === originalMessage) || !!(prevDraftMessage === draftMessage || isCommentPendingSaved.current)) { diff --git a/src/pages/tasks/NewTaskDescriptionPage.tsx b/src/pages/tasks/NewTaskDescriptionPage.tsx index e9b632a0cee9..a4ff9d0f07ac 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.tsx +++ b/src/pages/tasks/NewTaskDescriptionPage.tsx @@ -16,7 +16,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {NewTaskNavigatorParamList} from '@libs/Navigation/types'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import variables from '@styles/variables'; @@ -80,7 +80,7 @@ function NewTaskDescriptionPage({task}: NewTaskDescriptionPageProps) { { setTaskTitle(task?.title ?? ''); - setTaskDescription(parseHtmlToMarkdown(parser.replace(task?.description ?? ''))); + setTaskDescription(Parser.htmlToMarkdown(parser.replace(task?.description ?? ''))); }, [task]); const validate = (values: FormOnyxValues): FormInputErrors => { @@ -136,7 +136,7 @@ function NewTaskDetailsPage({task}: NewTaskDetailsPageProps) { autoGrowHeight maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight} shouldSubmitForm - defaultValue={parseHtmlToMarkdown(parser.replace(taskDescription))} + defaultValue={Parser.htmlToMarkdown(parser.replace(taskDescription))} value={taskDescription} onValueChange={setTaskDescription} isMarkdownEnabled diff --git a/src/pages/tasks/TaskDescriptionPage.tsx b/src/pages/tasks/TaskDescriptionPage.tsx index 66bd7d9e9a82..92f5b2394308 100644 --- a/src/pages/tasks/TaskDescriptionPage.tsx +++ b/src/pages/tasks/TaskDescriptionPage.tsx @@ -15,7 +15,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; @@ -48,7 +48,7 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti const submit = useCallback( (values: FormOnyxValues) => { - if (values.description !== parseHtmlToMarkdown(report?.description ?? '') && !isEmptyObject(report)) { + if (values.description !== Parser.htmlToMarkdown(report?.description ?? '') && !isEmptyObject(report)) { // Set the description of the report in the store and then call EditTask API // to update the description of the report on the server Task.editTask(report, {description: values.description}); @@ -111,7 +111,7 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti name={INPUT_IDS.DESCRIPTION} label={translate('newTaskPage.descriptionOptional')} accessibilityLabel={translate('newTaskPage.descriptionOptional')} - defaultValue={parseHtmlToMarkdown(report?.description ?? '')} + defaultValue={Parser.htmlToMarkdown(report?.description ?? '')} ref={(element: AnimatedTextInputRef) => { if (!element) { return; diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 9bab92bc38b0..63528de3fa30 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -22,8 +22,8 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import Parser from '@libs/Parser'; import * as PolicyUtils from '@libs/PolicyUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import type {SettingsNavigatorParamList} from '@navigation/types'; @@ -96,7 +96,7 @@ function WorkspaceInviteMessagePage({ useEffect(() => { if (!isEmptyObject(invitedEmailsToAccountIDsDraft)) { - setWelcomeNote(parseHtmlToMarkdown(getDefaultWelcomeNote())); + setWelcomeNote(Parser.htmlToMarkdown(getDefaultWelcomeNote())); return; } Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID), true); diff --git a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx index 54dcfd62ac44..e31a6fdb0a3d 100644 --- a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx +++ b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx @@ -12,7 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import variables from '@styles/variables'; import * as Policy from '@userActions/Policy/Policy'; @@ -30,7 +30,7 @@ function WorkspaceProfileDescriptionPage({policy}: Props) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [description, setDescription] = useState(() => - parseHtmlToMarkdown( + Parser.htmlToMarkdown( // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing policy?.description || diff --git a/src/pages/workspace/taxes/NamePage.tsx b/src/pages/workspace/taxes/NamePage.tsx index 3d589e7339a8..0fb495a04574 100644 --- a/src/pages/workspace/taxes/NamePage.tsx +++ b/src/pages/workspace/taxes/NamePage.tsx @@ -13,7 +13,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {renamePolicyTax, validateTaxName} from '@libs/actions/TaxRate'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import * as PolicyUtils from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; @@ -38,7 +38,7 @@ function NamePage({ const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); const {inputCallbackRef} = useAutoFocusInput(); - const [name, setName] = useState(() => parseHtmlToMarkdown(currentTaxRate?.name ?? '')); + const [name, setName] = useState(() => Parser.htmlToMarkdown(currentTaxRate?.name ?? '')); const goBack = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_TAX_EDIT.getRoute(policyID ?? '-1', taxID)), [policyID, taxID]); From 01e3d0190992fffa4874abdc63d5b4e705f5c5c2 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 2 Jul 2024 15:54:29 +0200 Subject: [PATCH 070/124] set the logger in PArser lib --- src/libs/Log.ts | 3 +-- src/libs/Parser.ts | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/Log.ts b/src/libs/Log.ts index 64271dee2265..83965807263a 100644 --- a/src/libs/Log.ts +++ b/src/libs/Log.ts @@ -2,7 +2,7 @@ // action would likely cause confusion about which one to use. But most other API methods should happen inside an action file. /* eslint-disable rulesdir/no-api-in-views */ -import {ExpensiMark, Logger} from 'expensify-common'; +import {Logger} from 'expensify-common'; import Onyx from 'react-native-onyx'; import type {Merge} from 'type-fest'; import CONST from '@src/CONST'; @@ -80,6 +80,5 @@ const Log = new Logger({ isDebug: true, }); timeout = setTimeout(() => Log.info('Flushing logs older than 10 minutes', true, {}, true), 10 * 60 * 1000); -ExpensiMark.setLogger(Log); export default Log; diff --git a/src/libs/Parser.ts b/src/libs/Parser.ts index f979b3b5c563..3f9d3c48b49c 100644 --- a/src/libs/Parser.ts +++ b/src/libs/Parser.ts @@ -2,6 +2,7 @@ import {ExpensiMark} from 'expensify-common'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import Log from './Log'; const reportIDToNameMap: Record = {}; const accountIDToNameMap: Record = {}; @@ -55,6 +56,7 @@ class ExpensiMarkWithContext extends ExpensiMark { } } +ExpensiMarkWithContext.setLogger(Log); const Parser = new ExpensiMarkWithContext(); export default Parser; From 69bafc75b14f942295f3b64ae729c644c6dd7d3b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 2 Jul 2024 15:54:54 +0200 Subject: [PATCH 071/124] Remove all ExpeniMark imports and use PArser instead --- src/components/MenuItem.tsx | 8 +++----- src/libs/ReportActionsUtils.ts | 2 +- src/libs/ReportUtils.ts | 9 ++++----- src/libs/actions/Policy/Member.ts | 4 ++-- src/libs/actions/Report.ts | 12 +++++------- .../home/report/ReportActionItemMessageEdit.tsx | 4 ++-- src/pages/tasks/NewTaskDescriptionPage.tsx | 5 +---- src/pages/tasks/NewTaskDetailsPage.tsx | 7 ++----- src/pages/workspace/WorkspaceInviteMessagePage.tsx | 5 +---- .../workspace/WorkspaceProfileDescriptionPage.tsx | 5 +---- src/pages/workspace/WorkspaceProfilePage.tsx | 6 ++---- 11 files changed, 24 insertions(+), 43 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 9fd18524158d..357ef60d5161 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,4 +1,3 @@ -import {ExpensiMark} from 'expensify-common'; import type {ImageContentFit} from 'expo-image'; import type {ReactElement, ReactNode} from 'react'; import React, {forwardRef, useContext, useMemo} from 'react'; @@ -14,6 +13,7 @@ import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; +import Parser from '@libs/Parser'; import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; @@ -429,16 +429,14 @@ function MenuItem( if (!title || !shouldParseTitle) { return ''; } - const parser = new ExpensiMark(); - return parser.replace(title, {shouldEscapeText}); + return Parser.replace(title, {shouldEscapeText}); }, [title, shouldParseTitle, shouldEscapeText]); const helperHtml = useMemo(() => { if (!helperText || !shouldParseHelperText) { return ''; } - const parser = new ExpensiMark(); - return parser.replace(helperText, {shouldEscapeText}); + return Parser.replace(helperText, {shouldEscapeText}); }, [helperText, shouldParseHelperText, shouldEscapeText]); const processedTitle = useMemo(() => { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f47ae594d1a5..680fef414732 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -20,7 +20,7 @@ import isReportMessageAttachment from './isReportMessageAttachment'; import * as Localize from './Localize'; import Log from './Log'; import type {MessageElementBase, MessageTextElement} from './MessageElement'; -import {Parser.htmlToText} from './OnyxAwareParser'; +import Parser from './Parser'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1fe4e6b3ea42..3553d3e7cbc5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1,5 +1,5 @@ import {format} from 'date-fns'; -import {ExpensiMark, Str} from 'expensify-common'; +import {Str} from 'expensify-common'; import {isEmpty} from 'lodash'; import lodashEscape from 'lodash/escape'; import lodashFindLastIndex from 'lodash/findLastIndex'; @@ -63,7 +63,7 @@ import ModifiedExpenseMessage from './ModifiedExpenseMessage'; import linkingConfig from './Navigation/linkingConfig'; import Navigation from './Navigation/Navigation'; import * as NumberUtils from './NumberUtils'; -import {Parser.htmlToText} from './OnyxAwareParser'; +import Parser from './Parser'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PhoneNumber from './PhoneNumber'; @@ -3288,7 +3288,7 @@ function parseReportActionHtmlToText(reportAction: OnyxEntry, repo const logins = PersonalDetailsUtils.getLoginsByAccountIDs(accountIDs); accountIDs.forEach((id, index) => (accountIDToName[id] = logins[index])); - const textMessage = Str.removeSMSDomain(Parser.htmlToText(html, reportIDToName, accountIDToName)); + const textMessage = Str.removeSMSDomain(Parser.htmlToText(html, {reportIDToName, accountIDToName})); parsedReportActionMessageCache[key] = textMessage; return textMessage; @@ -3640,7 +3640,6 @@ function getParsedComment(text: string, parsingDetails?: ParsingDetails): string isGroupPolicyReport = isReportInGroupPolicy(currentReport); } - const parser = new ExpensiMark(); const textWithMention = text.replace(CONST.REGEX.SHORT_MENTION, (match) => { if (!Str.isValidMention(match)) { return match; @@ -3651,7 +3650,7 @@ function getParsedComment(text: string, parsingDetails?: ParsingDetails): string }); return text.length <= CONST.MAX_MARKUP_LENGTH - ? parser.replace(textWithMention, {shouldEscapeText: parsingDetails?.shouldEscapeText, disabledRules: isGroupPolicyReport ? [] : ['reportMentions']}) + ? Parser.replace(textWithMention, {shouldEscapeText: parsingDetails?.shouldEscapeText, disabledRules: isGroupPolicyReport ? [] : ['reportMentions']}) : lodashEscape(text); } diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index f8472bd43098..218bf3a93c69 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -1,4 +1,3 @@ -import {ExpensiMark} from 'expensify-common'; import type {NullishDeep, OnyxCollection, OnyxCollectionInputValue, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; @@ -12,6 +11,7 @@ import type { import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; +import Parser from '@libs/Parser'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -627,7 +627,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount const params: AddMembersToWorkspaceParams = { employees: JSON.stringify(logins.map((login) => ({email: login}))), - welcomeNote: new ExpensiMark().replace(welcomeNote), + welcomeNote: Parser.replace(welcomeNote), policyID, }; if (!isEmptyObject(membersChats.reportCreationData)) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 7e0e583e6ec0..54039fc9dadb 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1,5 +1,5 @@ import {format as timezoneFormat, utcToZonedTime} from 'date-fns-tz'; -import {ExpensiMark, Str} from 'expensify-common'; +import {Str} from 'expensify-common'; import isEmpty from 'lodash/isEmpty'; import {DeviceEventEmitter, InteractionManager, Linking} from 'react-native'; import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; @@ -1470,21 +1470,19 @@ function removeLinksFromHtml(html: string, links: string[]): string { * @param videoAttributeCache cache of video attributes ([videoSource]: videoAttributes) */ function handleUserDeletedLinksInHtml(newCommentText: string, originalCommentMarkdown: string, videoAttributeCache?: Record): string { - const parser = new ExpensiMark(); if (newCommentText.length > CONST.MAX_MARKUP_LENGTH) { return newCommentText; } - const htmlForNewComment = parser.replace(newCommentText, { + const htmlForNewComment = Parser.replace(newCommentText, { extras: {videoAttributeCache}, }); - const removedLinks = parser.getRemovedMarkdownLinks(originalCommentMarkdown, newCommentText); + const removedLinks = Parser.getRemovedMarkdownLinks(originalCommentMarkdown, newCommentText); return removeLinksFromHtml(htmlForNewComment, removedLinks); } /** Saves a new message for a comment. Marks the comment as edited, which will be reflected in the UI. */ function editReportComment(reportID: string, originalReportAction: OnyxEntry, textForNewComment: string, videoAttributeCache?: Record) { - const parser = new ExpensiMark(); const originalReportID = ReportUtils.getOriginalReportID(reportID, originalReportAction); if (!originalReportID || !originalReportAction) { @@ -1509,8 +1507,8 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry rule.name).filter((name) => name !== 'autolink')}; - parsedOriginalCommentHTML = parser.replace(originalCommentMarkdown, autolinkFilter); + const autolinkFilter = {filterRules: Parser.rules.map((rule) => rule.name).filter((name) => name !== 'autolink')}; + parsedOriginalCommentHTML = Parser.replace(originalCommentMarkdown, autolinkFilter); } // Delete the comment if it's empty diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 2d87d84dbd6b..2766de157dd3 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -113,8 +113,8 @@ function ReportActionItemMessageEdit( useEffect(() => { draftMessageVideoAttributeCache.clear(); - const originalMessage = Parser.htmlToMarkdown(ReportActionsUtils.getReportActionHtml(action), undefined, undefined, (videoSource, attrs) => { - draftMessageVideoAttributeCache.set(videoSource, attrs); + const originalMessage = Parser.htmlToMarkdown(ReportActionsUtils.getReportActionHtml(action), { + cacheVideoAttributes: (videoSource, attrs) => draftMessageVideoAttributeCache.set(videoSource, attrs), }); if (ReportActionsUtils.isDeletedAction(action) || !!(action.message && draftMessage === originalMessage) || !!(prevDraftMessage === draftMessage || isCommentPendingSaved.current)) { return; diff --git a/src/pages/tasks/NewTaskDescriptionPage.tsx b/src/pages/tasks/NewTaskDescriptionPage.tsx index a4ff9d0f07ac..f5aaf9ea8ffd 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.tsx +++ b/src/pages/tasks/NewTaskDescriptionPage.tsx @@ -1,5 +1,4 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import {ExpensiMark} from 'expensify-common'; import React from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -35,8 +34,6 @@ type NewTaskDescriptionPageOnyxProps = { type NewTaskDescriptionPageProps = NewTaskDescriptionPageOnyxProps & StackScreenProps; -const parser = new ExpensiMark(); - function NewTaskDescriptionPage({task}: NewTaskDescriptionPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -80,7 +77,7 @@ function NewTaskDescriptionPage({task}: NewTaskDescriptionPageProps) { ; -const parser = new ExpensiMark(); - function NewTaskDetailsPage({task}: NewTaskDetailsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -50,7 +47,7 @@ function NewTaskDetailsPage({task}: NewTaskDetailsPageProps) { useEffect(() => { setTaskTitle(task?.title ?? ''); - setTaskDescription(Parser.htmlToMarkdown(parser.replace(task?.description ?? ''))); + setTaskDescription(Parser.htmlToMarkdown(Parser.replace(task?.description ?? ''))); }, [task]); const validate = (values: FormOnyxValues): FormInputErrors => { @@ -136,7 +133,7 @@ function NewTaskDetailsPage({task}: NewTaskDetailsPageProps) { autoGrowHeight maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight} shouldSubmitForm - defaultValue={Parser.htmlToMarkdown(parser.replace(taskDescription))} + defaultValue={Parser.htmlToMarkdown(Parser.replace(taskDescription))} value={taskDescription} onValueChange={setTaskDescription} isMarkdownEnabled diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 63528de3fa30..f7742ecd1361 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -1,5 +1,4 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import {ExpensiMark} from 'expensify-common'; import lodashDebounce from 'lodash/debounce'; import React, {useEffect, useMemo, useState} from 'react'; import {Keyboard, View} from 'react-native'; @@ -59,8 +58,6 @@ type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInviteMessagePageOnyxProps & StackScreenProps; -const parser = new ExpensiMark(); - function WorkspaceInviteMessagePage({ workspaceInviteMessageDraft, invitedEmailsToAccountIDsDraft, @@ -88,7 +85,7 @@ function WorkspaceInviteMessagePage({ // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing policy?.description || - parser.replace( + Parser.replace( translate('workspace.common.welcomeNote', { workspaceName: policy?.name ?? '', }), diff --git a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx index e31a6fdb0a3d..527fdf28f8d1 100644 --- a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx +++ b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx @@ -1,4 +1,3 @@ -import {ExpensiMark} from 'expensify-common'; import React, {useCallback, useState} from 'react'; import {Keyboard, View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; @@ -24,8 +23,6 @@ import type {WithPolicyProps} from './withPolicy'; type Props = WithPolicyProps; -const parser = new ExpensiMark(); - function WorkspaceProfileDescriptionPage({policy}: Props) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -34,7 +31,7 @@ function WorkspaceProfileDescriptionPage({policy}: Props) { // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing policy?.description || - parser.replace( + Parser.replace( translate('workspace.common.welcomeNote', { workspaceName: policy?.name ?? '', }), diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 6a86a83b5d11..9163afdf24f6 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -1,4 +1,3 @@ -import {ExpensiMark} from 'expensify-common'; import React, {useCallback, useState} from 'react'; import type {ImageStyle, StyleProp} from 'react-native'; import {Image, StyleSheet, View} from 'react-native'; @@ -21,6 +20,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; +import Parser from '@libs/Parser'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; @@ -42,8 +42,6 @@ type WorkSpaceProfilePageOnyxProps = { type WorkSpaceProfilePageProps = WithPolicyProps & WorkSpaceProfilePageOnyxProps; -const parser = new ExpensiMark(); - function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfilePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -73,7 +71,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing policy?.description || - parser.replace( + Parser.replace( translate('workspace.common.welcomeNote', { workspaceName: policy?.name ?? '', }), From a4d0160daa425280b827bf94339256614f622f9a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 2 Jul 2024 15:55:24 +0200 Subject: [PATCH 072/124] Add eslint disable for storybook ExpeniMark import --- src/stories/Composer.stories.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stories/Composer.stories.tsx b/src/stories/Composer.stories.tsx index 7244479b4c58..805f2b4c7448 100644 --- a/src/stories/Composer.stories.tsx +++ b/src/stories/Composer.stories.tsx @@ -1,4 +1,5 @@ import type {Meta} from '@storybook/react'; +// eslint-disable-next-line no-restricted-imports import {ExpensiMark} from 'expensify-common'; import React, {useState} from 'react'; import {Image, View} from 'react-native'; From 785a45ffbde399af248e6c94ca5e34847b7d5a6e Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 2 Jul 2024 18:04:22 +0100 Subject: [PATCH 073/124] fix: predefined places being filtered in offline mode --- src/components/AddressSearch/index.tsx | 28 ++------------------------ 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index ca026ac21819..de6c8515f9af 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -24,24 +24,7 @@ import CONST from '@src/CONST'; import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import CurrentLocationButton from './CurrentLocationButton'; import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer'; -import type {AddressSearchProps, PredefinedPlace} from './types'; - -/** - * Check if the place matches the search by the place name or description. - * @param search The search string for a place - * @param place The place to check for a match on the search - * @returns true if search is related to place, otherwise it returns false. - */ -function isPlaceMatchForSearch(search: string, place: PredefinedPlace): boolean { - if (!search) { - return true; - } - if (!place) { - return false; - } - const fullSearchSentence = `${place.name ?? ''} ${place.description}`; - return search.split(' ').every((searchTerm) => !searchTerm || fullSearchSentence.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase())); -} +import type {AddressSearchProps} from './types'; // The error that's being thrown below will be ignored until we fork the // react-native-google-places-autocomplete repo and replace the @@ -322,13 +305,6 @@ function AddressSearch( }; }, []); - const filteredPredefinedPlaces = useMemo(() => { - if (!isOffline || !searchValue) { - return predefinedPlaces ?? []; - } - return predefinedPlaces?.filter((predefinedPlace) => isPlaceMatchForSearch(searchValue, predefinedPlace)) ?? []; - }, [isOffline, predefinedPlaces, searchValue]); - const listEmptyComponent = useCallback( () => (!isTyping ? null : {translate('common.noResultsFound')}), [isTyping, styles, translate], @@ -372,7 +348,7 @@ function AddressSearch( fetchDetails suppressDefaultStyles enablePoweredByContainer={false} - predefinedPlaces={filteredPredefinedPlaces} + predefinedPlaces={predefinedPlaces ?? []} listEmptyComponent={listEmptyComponent} listLoaderComponent={listLoader} renderHeaderComponent={renderHeaderComponent} From fc426932fd28d1cb63476eaaefc392e1edab3156 Mon Sep 17 00:00:00 2001 From: dominictb Date: Wed, 3 Jul 2024 15:53:51 +0700 Subject: [PATCH 074/124] fix: update isLoading logic on workspace catogory page Signed-off-by: dominictb --- src/pages/workspace/categories/WorkspaceCategoriesPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 37b9680647da..5d55527da4c5 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -254,7 +254,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { ); }; - const isLoading = !isOffline && policyCategories === null; + const isLoading = !isOffline && policyCategories === undefined; const hasVisibleCategories = categoryList.some((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); From 55feafb2c173693b11c2143c8b5d6112dfc53066 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 3 Jul 2024 11:20:52 +0200 Subject: [PATCH 075/124] render mapview when access token is set in a library --- src/components/MapView/MapView.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 283f7c396edb..7f5547e5e5a8 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -37,6 +37,7 @@ const MapView = forwardRef( const currentPosition = userLocation ?? initialLocation; const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); const shouldInitializeCurrentPosition = useRef(true); + const [isAccessTokenSet, setIsAccessTokenSet] = useState(false); // Determines if map can be panned to user's detected // location without bothering the user. It will return @@ -138,7 +139,12 @@ const MapView = forwardRef( }, [navigation]); useEffect(() => { - setAccessToken(accessToken); + setAccessToken(accessToken).then((token) => { + if(!token) { + return; + } + setIsAccessTokenSet(true); + }); }, [accessToken]); const setMapIdle = (e: MapState) => { @@ -198,7 +204,7 @@ const MapView = forwardRef( const initCenterCoordinate = useMemo(() => (interactive ? centerCoordinate : undefined), [interactive, centerCoordinate]); const initBounds = useMemo(() => (interactive ? undefined : waypointsBounds), [interactive, waypointsBounds]); - return !isOffline && !!accessToken && !!defaultSettings ? ( + return !isOffline && isAccessTokenSet && !!defaultSettings ? ( Date: Wed, 3 Jul 2024 11:30:45 +0200 Subject: [PATCH 076/124] fix lint --- src/components/MapView/MapView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 7f5547e5e5a8..553be816cf3f 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -140,7 +140,7 @@ const MapView = forwardRef( useEffect(() => { setAccessToken(accessToken).then((token) => { - if(!token) { + if (!token) { return; } setIsAccessTokenSet(true); From a90fe8ffd00e85aa971511f197e7f7c5a19cf781 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 4 Jul 2024 03:23:55 +0530 Subject: [PATCH 077/124] fix: Web - Profile - Hovering over the zoom tool does not display a tooltip. Signed-off-by: Krishna Gupta --- src/components/AvatarCropModal/Slider.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx index 9a9da65befa0..67aa89c9c550 100644 --- a/src/components/AvatarCropModal/Slider.tsx +++ b/src/components/AvatarCropModal/Slider.tsx @@ -7,6 +7,7 @@ import type {SharedValue} from 'react-native-reanimated'; import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as Browser from '@libs/Browser'; import ControlSelection from '@libs/ControlSelection'; type SliderProps = { @@ -62,7 +63,7 @@ function Slider({sliderValue, gestureCallbacks}: SliderProps) { shiftVertical={-2} > {/* pointerEventsNone is a workaround to make sure the pan gesture works correctly on mobile safari */} - + )} From 505e31a47337b2cc2dbc361cfc54ec4359dee2f4 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Wed, 3 Jul 2024 16:22:35 -0700 Subject: [PATCH 078/124] consolidate logic --- src/libs/OptionsListUtils.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 9bf0ef46ec13..f65a1accb763 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2501,14 +2501,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt if (item.subtitle) { values.push(item.subtitle); } - } else { - values = values.concat(getParticipantsLoginsArray(item)); - } - - // We don't want chat rooms to be searchable via participants, so we only add participant logins/display - // if the report in question isn't a chat room. - const partialReport = {reportID: item.reportID, type: item.type, chatType: item.chatType}; - if (!ReportUtils.isChatReport(partialReport) || !ReportUtils.isChatRoom(partialReport)) { + } else if (!item.isChatRoom) { const participantNames = getParticipantNames(item.participantsList ?? []); values = values.concat(Array.from(participantNames)); values = values.concat(getParticipantsLoginsArray(item)); From 86735ab68ac55181fb3edb616e8de47185590451 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Wed, 3 Jul 2024 16:25:48 -0700 Subject: [PATCH 079/124] consolidate logic --- src/libs/OptionsListUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f65a1accb763..763ab0ccd48b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2501,7 +2501,9 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt if (item.subtitle) { values.push(item.subtitle); } - } else if (!item.isChatRoom) { + } + + if (!item.isChatRoom) { const participantNames = getParticipantNames(item.participantsList ?? []); values = values.concat(Array.from(participantNames)); values = values.concat(getParticipantsLoginsArray(item)); From c1b0ddbbc16e1f21b16d75f14f9ca4ceb842a1e4 Mon Sep 17 00:00:00 2001 From: dominictb Date: Thu, 4 Jul 2024 10:52:15 +0700 Subject: [PATCH 080/124] Fix: Category styles --- .../Search/TransactionListItemRow.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index ca27f05dbda4..3d7374fc2709 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -172,7 +172,21 @@ function CategoryCell({isLargeScreenWidth, showTooltip, transactionItem}: Transa ); } From aba4a1b91bde9823bf340a46c22cda3e21becf08 Mon Sep 17 00:00:00 2001 From: dominictb Date: Thu, 4 Jul 2024 11:36:16 +0700 Subject: [PATCH 081/124] Fix: Update passing isLargeScreenWidth logic --- .../Search/TransactionListItemRow.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index 3d7374fc2709..5ec944410c0f 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -270,7 +270,7 @@ function TransactionListItemRow({ {item.category && ( @@ -281,19 +281,19 @@ function TransactionListItemRow({ @@ -317,7 +317,7 @@ function TransactionListItemRow({ @@ -325,14 +325,14 @@ function TransactionListItemRow({ @@ -350,7 +350,7 @@ function TransactionListItemRow({ {item.shouldShowCategory && ( @@ -359,7 +359,7 @@ function TransactionListItemRow({ {item.shouldShowTag && ( @@ -369,7 +369,7 @@ function TransactionListItemRow({ @@ -379,14 +379,14 @@ function TransactionListItemRow({ From ead55919064307bfaf92981761e69c159c13da90 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:20:46 +0530 Subject: [PATCH 082/124] Update --- src/libs/TransactionUtils.ts | 2 +- src/pages/iou/request/step/IOURequestStepDistanceRate.tsx | 2 +- src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx | 3 ++- tests/unit/CurrencyUtilsTest.ts | 8 ++++++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index b41f627bf23f..9f2c727b94ba 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -715,7 +715,7 @@ function hasWarningTypeViolation(transactionID: string, transactionViolations: O /** * Calculates tax amount from the given expense amount and tax percentage */ -function calculateTaxAmount(percentage: string, amount: number, currency: string | undefined = undefined) { +function calculateTaxAmount(percentage: string, amount: number, currency: string) { const divisor = Number(percentage.slice(0, -1)) / 100 + 1; const taxAmount = (amount - amount / divisor) / 100; const decimals = getCurrencyDecimals(currency); diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 9399a2a65d9f..0d83bada93bb 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -81,7 +81,7 @@ function IOURequestStepDistanceRate({ const taxRateExternalID = policyCustomUnitRate?.attributes?.taxRateExternalID ?? '-1'; const taxableAmount = DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, TransactionUtils.getDistance(transaction)); const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxRateExternalID) ?? ''; - const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount)); + const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, rates[customUnitRateID].currency)); IOU.setMoneyRequestTaxAmount(transactionID, taxAmount); IOU.setMoneyRequestTaxRate(transactionID, taxRateExternalID); } diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx index 22ca585ac4b1..6025a573e417 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx @@ -17,6 +17,7 @@ import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; +import {getCurrency, transformedTaxRates} from "@libs/TransactionUtils"; type IOURequestStepTaxRatePageOnyxProps = { policy: OnyxEntry; @@ -38,7 +39,7 @@ function getTaxAmount(policy: OnyxEntry, transaction: OnyxEntry TransactionUtils.getTaxValue(policy, transaction, taxCode); const taxPercentage = getTaxValue(selectedTaxCode); if (taxPercentage) { - return TransactionUtils.calculateTaxAmount(taxPercentage, amount); + return TransactionUtils.calculateTaxAmount(taxPercentage, amount, getCurrency(transaction)); } } diff --git a/tests/unit/CurrencyUtilsTest.ts b/tests/unit/CurrencyUtilsTest.ts index d6c18ad82471..20b03ea07c81 100644 --- a/tests/unit/CurrencyUtilsTest.ts +++ b/tests/unit/CurrencyUtilsTest.ts @@ -117,6 +117,10 @@ describe('CurrencyUtils', () => { }); }); + /** + * VND uses 0 decimals, so this test is needed. + * https://github.com/Expensify/App/pull/43948 + */ describe('convertToFrontendAmountAsInteger VND', () => { test.each([ [2500, 25], @@ -143,6 +147,10 @@ describe('CurrencyUtils', () => { }); }); + /** + * VND uses 0 decimals, so this test is needed. + * https://github.com/Expensify/App/pull/43948 + */ describe('convertToFrontendAmountAsString VND', () => { test.each([ [2500, '25'], From 21530f8b9693fded2358d7df37cd73536e183ee7 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:40:53 +0530 Subject: [PATCH 083/124] Update IOURequestStepTaxRatePage.tsx --- src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx index 6025a573e417..d349545d61db 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx @@ -13,11 +13,11 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type {Policy, PolicyCategories, PolicyTagList, Transaction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {getCurrency} from "@libs/TransactionUtils"; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; -import {getCurrency, transformedTaxRates} from "@libs/TransactionUtils"; type IOURequestStepTaxRatePageOnyxProps = { policy: OnyxEntry; From 2fb7b2662043a00269271ebfa4ab1380135171eb Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:26:31 +0530 Subject: [PATCH 084/124] Add some default values --- src/pages/iou/request/step/IOURequestStepAmount.tsx | 2 +- src/pages/iou/request/step/IOURequestStepDistanceRate.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index f234b3b71718..a64af40a32a5 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -294,7 +294,7 @@ function IOURequestStepAmount({ const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, currentTransaction, currency) ?? ''; const taxCode = (currency !== transactionCurrency ? defaultTaxCode : transactionTaxCode) ?? defaultTaxCode; const taxPercentage = TransactionUtils.getTaxValue(policy, currentTransaction, taxCode) ?? ''; - const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, newAmount, currency)); + const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, newAmount, currency ?? CONST.CURRENCY.USD)); if (isSplitBill) { IOU.setDraftSplitTransaction(transactionID, {amount: newAmount, currency, taxCode, taxAmount}); diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 0d83bada93bb..4f70d3e4fee9 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -81,7 +81,7 @@ function IOURequestStepDistanceRate({ const taxRateExternalID = policyCustomUnitRate?.attributes?.taxRateExternalID ?? '-1'; const taxableAmount = DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, TransactionUtils.getDistance(transaction)); const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxRateExternalID) ?? ''; - const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, rates[customUnitRateID].currency)); + const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, rates[customUnitRateID].currency ?? CONST.CURRENCY.USD)); IOU.setMoneyRequestTaxAmount(transactionID, taxAmount); IOU.setMoneyRequestTaxRate(transactionID, taxRateExternalID); } From 6747545d35b2a020293671d564087bfaa9bb47e1 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:28:24 +0530 Subject: [PATCH 085/124] Lint fix --- src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx index d349545d61db..a14c3a3a50be 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx @@ -7,13 +7,13 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {TaxRatesOption} from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import {getCurrency} from '@libs/TransactionUtils'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type {Policy, PolicyCategories, PolicyTagList, Transaction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {getCurrency} from "@libs/TransactionUtils"; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; From 0b5bbe3bb00ccf8b966f0b3fb2f0d60400792cdb Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 4 Jul 2024 19:47:05 +0100 Subject: [PATCH 086/124] Revert "fix: predefined places being filtered in offline mode" This reverts commit 785a45ffbde399af248e6c94ca5e34847b7d5a6e. --- src/components/AddressSearch/index.tsx | 28 ++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index de6c8515f9af..ca026ac21819 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -24,7 +24,24 @@ import CONST from '@src/CONST'; import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import CurrentLocationButton from './CurrentLocationButton'; import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer'; -import type {AddressSearchProps} from './types'; +import type {AddressSearchProps, PredefinedPlace} from './types'; + +/** + * Check if the place matches the search by the place name or description. + * @param search The search string for a place + * @param place The place to check for a match on the search + * @returns true if search is related to place, otherwise it returns false. + */ +function isPlaceMatchForSearch(search: string, place: PredefinedPlace): boolean { + if (!search) { + return true; + } + if (!place) { + return false; + } + const fullSearchSentence = `${place.name ?? ''} ${place.description}`; + return search.split(' ').every((searchTerm) => !searchTerm || fullSearchSentence.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase())); +} // The error that's being thrown below will be ignored until we fork the // react-native-google-places-autocomplete repo and replace the @@ -305,6 +322,13 @@ function AddressSearch( }; }, []); + const filteredPredefinedPlaces = useMemo(() => { + if (!isOffline || !searchValue) { + return predefinedPlaces ?? []; + } + return predefinedPlaces?.filter((predefinedPlace) => isPlaceMatchForSearch(searchValue, predefinedPlace)) ?? []; + }, [isOffline, predefinedPlaces, searchValue]); + const listEmptyComponent = useCallback( () => (!isTyping ? null : {translate('common.noResultsFound')}), [isTyping, styles, translate], @@ -348,7 +372,7 @@ function AddressSearch( fetchDetails suppressDefaultStyles enablePoweredByContainer={false} - predefinedPlaces={predefinedPlaces ?? []} + predefinedPlaces={filteredPredefinedPlaces} listEmptyComponent={listEmptyComponent} listLoaderComponent={listLoader} renderHeaderComponent={renderHeaderComponent} From 363ab6ff24ca60945268d2a8e67b069a333e0bb0 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 4 Jul 2024 19:51:31 +0100 Subject: [PATCH 087/124] fix: address search not showing recent waypoints while offline --- src/components/AddressSearch/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index ca026ac21819..2679a550f72f 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -90,7 +90,7 @@ function AddressSearch( const [isTyping, setIsTyping] = useState(false); const [isFocused, setIsFocused] = useState(false); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const [searchValue, setSearchValue] = useState(value || defaultValue || ''); + const [searchValue, setSearchValue] = useState(''); const [locationErrorCode, setLocationErrorCode] = useState(null); const [isFetchingCurrentLocation, setIsFetchingCurrentLocation] = useState(false); const shouldTriggerGeolocationCallbacks = useRef(true); @@ -323,11 +323,11 @@ function AddressSearch( }, []); const filteredPredefinedPlaces = useMemo(() => { - if (!isOffline || !searchValue) { + if (!searchValue) { return predefinedPlaces ?? []; } return predefinedPlaces?.filter((predefinedPlace) => isPlaceMatchForSearch(searchValue, predefinedPlace)) ?? []; - }, [isOffline, predefinedPlaces, searchValue]); + }, [predefinedPlaces, searchValue]); const listEmptyComponent = useCallback( () => (!isTyping ? null : {translate('common.noResultsFound')}), From f05fff30fce929ec48a8af340dc3cd078456f8c5 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:21:20 +0530 Subject: [PATCH 088/124] Typecheck fix --- src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx index d54cd6129e12..4b3b5cc13eb1 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx @@ -50,7 +50,7 @@ function getTaxAmount(transaction: OnyxEntry, policy: OnyxEntry Date: Fri, 5 Jul 2024 16:09:51 +0700 Subject: [PATCH 089/124] remove view button in mobile and display one line merchant --- src/components/SelectionList/Search/ActionCell.tsx | 3 +++ .../SelectionList/Search/TransactionListItemRow.tsx | 5 +++-- src/libs/StringUtils.ts | 11 ++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index 9e0599d839df..5af3d84bf32f 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -22,6 +22,9 @@ function ActionCell({onButtonPress, action = CONST.SEARCH.ACTION_TYPES.VIEW, isL const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); + if (!isLargeScreenWidth) { + return null; + } if (action === CONST.SEARCH.ACTION_TYPES.PAID || action === CONST.SEARCH.ACTION_TYPES.DONE) { const buttonTextKey = action === CONST.SEARCH.ACTION_TYPES.PAID ? 'iou.settledExpensify' : 'common.done'; diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index 5ec944410c0f..29e009a406c3 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -14,6 +14,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; +import StringUtils from '@libs/StringUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import variables from '@styles/variables'; @@ -123,11 +124,11 @@ function MerchantCell({transactionItem, showTooltip, isLargeScreenWidth}: Transa if (TransactionUtils.hasReceipt(transactionItem) && TransactionUtils.isReceiptBeingScanned(transactionItem) && transactionItem.shouldShowMerchant) { merchant = translate('iou.receiptStatusTitle'); } - + const merchantToDisplay = StringUtils.getFirstLine(merchant); return ( ); diff --git a/src/libs/StringUtils.ts b/src/libs/StringUtils.ts index 393ecddb4c36..fffd54506fcb 100644 --- a/src/libs/StringUtils.ts +++ b/src/libs/StringUtils.ts @@ -105,4 +105,13 @@ function lineBreaksToSpaces(text = '') { return text.replace(CONST.REGEX.LINE_BREAK, ' '); } -export default {sanitizeString, isEmptyString, removeInvisibleCharacters, normalizeAccents, normalizeCRLF, getAcronym, lineBreaksToSpaces}; +/** + * Get the first line of the string + */ +function getFirstLine(text = '') { + // Split the input string by newline characters and return the first element of the resulting array + const lines = text.split('\n'); + return lines[0]; +} + +export default {sanitizeString, isEmptyString, removeInvisibleCharacters, normalizeAccents, normalizeCRLF, getAcronym, lineBreaksToSpaces, getFirstLine}; From f8df4cf5a0068173a02201500c5672981e85fa15 Mon Sep 17 00:00:00 2001 From: dominictb Date: Fri, 5 Jul 2024 16:20:33 +0700 Subject: [PATCH 090/124] fix lint --- .../Search/TransactionListItemRow.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index 29e009a406c3..23f9234819c3 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -318,7 +318,7 @@ function TransactionListItemRow({ @@ -326,14 +326,14 @@ function TransactionListItemRow({ @@ -351,7 +351,7 @@ function TransactionListItemRow({ {item.shouldShowCategory && ( @@ -360,7 +360,7 @@ function TransactionListItemRow({ {item.shouldShowTag && ( @@ -370,7 +370,7 @@ function TransactionListItemRow({ @@ -380,14 +380,14 @@ function TransactionListItemRow({ From 6ea62861993168bddb97ddec4361ddf21c980c7f Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:39:15 +0530 Subject: [PATCH 091/124] Update --- tests/unit/CurrencyUtilsTest.ts | 84 +++++++++++++-------------------- 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/tests/unit/CurrencyUtilsTest.ts b/tests/unit/CurrencyUtilsTest.ts index 20b03ea07c81..b4d09104a74c 100644 --- a/tests/unit/CurrencyUtilsTest.ts +++ b/tests/unit/CurrencyUtilsTest.ts @@ -107,62 +107,44 @@ describe('CurrencyUtils', () => { describe('convertToFrontendAmountAsInteger', () => { test.each([ - [2500, 25], - [2550, 25.5], - [25, 0.25], - [2500, 25], - [2500.5, 25], // The backend should never send a decimal .5 value - ])('Correctly converts %s to amount in units handled in frontend as an integer', (amount, expectedResult) => { - expect(CurrencyUtils.convertToFrontendAmountAsInteger(amount, CONST.CURRENCY.USD)).toBe(expectedResult); - }); - }); - - /** - * VND uses 0 decimals, so this test is needed. - * https://github.com/Expensify/App/pull/43948 - */ - describe('convertToFrontendAmountAsInteger VND', () => { - test.each([ - [2500, 25], - [2550, 26], - [25, 0], - [2586, 26], - [2500.5, 25], // The backend should never send a decimal .5 value - ])('Correctly converts %s to amount in units handled in frontend as an integer', (amount, expectedResult) => { - expect(CurrencyUtils.convertToFrontendAmountAsInteger(amount, 'VND')).toBe(expectedResult); + [2500, 25, 'USD'], + [2550, 25.5, 'USD'], + [25, 0.25, 'USD'], + [2500, 25, 'USD'], + [2500.5, 25, 'USD'], // The backend should never send a decimal .5 value + // VND uses 0 decimals. + // https://github.com/Expensify/App/pull/43948 + [2500, 25, 'VND'], + [2550, 26, 'VND'], + [25, 0, 'VND'], + [2586, 26, 'VND'], + [2500.5, 25, 'VND'], // The backend should never send a decimal .5 value + ])('Correctly converts %s to amount in units handled in frontend as an integer', (amount, expectedResult, currency) => { + expect(CurrencyUtils.convertToFrontendAmountAsInteger(amount, currency)).toBe(expectedResult); }); }); describe('convertToFrontendAmountAsString', () => { test.each([ - [2500, '25.00'], - [2550, '25.50'], - [25, '0.25'], - [2500.5, '25.00'], - [null, ''], - [undefined, ''], - [0, '0.00'], - ])('Correctly converts %s to amount in units handled in frontend as a string', (input, expectedResult) => { - expect(CurrencyUtils.convertToFrontendAmountAsString(input, CONST.CURRENCY.USD)).toBe(expectedResult); - }); - }); - - /** - * VND uses 0 decimals, so this test is needed. - * https://github.com/Expensify/App/pull/43948 - */ - describe('convertToFrontendAmountAsString VND', () => { - test.each([ - [2500, '25'], - [2550, '26'], - [25, '0'], - [2500.5, '25'], - [null, ''], - [undefined, ''], - [0, '0'], - [2586, '26'], - ])('Correctly converts %s to amount in units handled in frontend as a string', (input, expectedResult) => { - expect(CurrencyUtils.convertToFrontendAmountAsString(input, 'VND')).toBe(expectedResult); + [2500, '25.00', 'USD'], + [2550, '25.50', 'USD'], + [25, '0.25', 'USD'], + [2500.5, '25.00', 'USD'], + [null, '', 'USD'], + [undefined, '', 'USD'], + [0, '0.00', 'USD'], + // VND uses 0 decimals. + // https://github.com/Expensify/App/pull/43948 + [2500, '25', 'VND'], + [2550, '26', 'VND'], + [25, '0', 'VND'], + [2500.5, '25', 'VND'], + [null, '', 'VND'], + [undefined, '', 'VND'], + [0, '0', 'VND'], + [2586, '26', 'VND'], + ])('Correctly converts %s to amount in units handled in frontend as a string', (input, expectedResult, currency) => { + expect(CurrencyUtils.convertToFrontendAmountAsString(input, currency ?? CONST.CURRENCY.USD)).toBe(expectedResult); }); }); From c2d7b70ac955922d0f2f7eb43f498a986a47b042 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 5 Jul 2024 21:10:25 +0530 Subject: [PATCH 092/124] Implemented remaining pages of NetSuite Advanced --- src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 29 ++++ src/SCREENS.ts | 7 + src/languages/en.ts | 7 + src/languages/es.ts | 7 + src/libs/API/types.ts | 17 +++ .../ModalStackNavigators/index.tsx | 13 ++ .../FULL_SCREEN_TO_RHP_MAPPING.ts | 7 + src/libs/Navigation/linkingConfig/config.ts | 21 +++ src/libs/Navigation/types.ts | 22 +++ src/libs/PolicyUtils.ts | 49 ++++++- .../actions/connections/NetSuiteCommands.ts | 128 +++++++++++++++++- .../advanced/NetSuiteAdvancedPage.tsx | 21 +-- .../NetSuiteApprovalAccountSelectPage.tsx | 90 ++++++++++++ .../NetSuiteCollectionAccountSelectPage.tsx | 89 ++++++++++++ .../advanced/NetSuiteCustomFormIDPage.tsx | 83 ++++++++++++ ...teExpenseReportApprovalLevelSelectPage.tsx | 74 ++++++++++ ...iteJournalEntryApprovalLevelSelectPage.tsx | 77 +++++++++++ ...NetSuiteReimbursementAccountSelectPage.tsx | 89 ++++++++++++ ...SuiteVendorBillApprovalLevelSelectPage.tsx | 81 +++++++++++ src/types/form/NetSuiteCustomFormIDForm.ts | 16 +++ src/types/form/index.ts | 1 + src/types/onyx/Policy.ts | 26 ++-- 23 files changed, 936 insertions(+), 21 deletions(-) create mode 100644 src/pages/workspace/accounting/netsuite/advanced/NetSuiteApprovalAccountSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/advanced/NetSuiteCollectionAccountSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/advanced/NetSuiteExpenseReportApprovalLevelSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/advanced/NetSuiteJournalEntryApprovalLevelSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/advanced/NetSuiteReimbursementAccountSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/advanced/NetSuiteVendorBillApprovalLevelSelectPage.tsx create mode 100644 src/types/form/NetSuiteCustomFormIDForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 906f8ef7095e..5d6b5492d15c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -562,6 +562,8 @@ const ONYXKEYS = { SAGE_INTACCT_CREDENTIALS_FORM_DRAFT: 'sageIntacctCredentialsFormDraft', NETSUITE_TOKEN_INPUT_FORM: 'netsuiteTokenInputForm', NETSUITE_TOKEN_INPUT_FORM_DRAFT: 'netsuiteTokenInputFormDraft', + NETSUITE_CUSTOM_FORM_ID_FORM: 'netsuiteCustomFormIDForm', + NETSUITE_CUSTOM_FORM_ID_FORM_DRAFT: 'netsuiteCustomFormIDFormDraft', }, } as const; @@ -626,6 +628,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; [ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm; [ONYXKEYS.FORMS.NETSUITE_TOKEN_INPUT_FORM]: FormTypes.NetSuiteTokenInputForm; + [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FORM_ID_FORM]: FormTypes.NetSuiteCustomFormIDForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 82e49e31a166..149af6b16828 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1070,6 +1070,35 @@ const ROUTES = { route: 'settings/workspaces/:policyID/connections/netsuite/advanced/', getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/` as const, }, + POLICY_ACCOUNTING_NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/reimbursement-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/reimbursement-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_COLLECTION_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/collection-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/collection-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/expense-report-approval-level/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/expense-report-approval-level/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/vendor-bill-approval-level/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/vendor-bill-approval-level/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/journal-entry-approval-level/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/journal-entry-approval-level/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_APPROVAL_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/approval-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/approval-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/custom-form-id/:expenseType', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/advanced/custom-form-id/${expenseType}` as const, + }, POLICY_ACCOUNTING_SAGE_INTACCT_PREREQUISITES: { route: 'settings/workspaces/:policyID/accounting/sage-intacct/prerequisites', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/prerequisites` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 875de4363028..418d0559c380 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -299,6 +299,13 @@ const SCREENS = { NETSUITE_TAX_POSTING_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Tax_Posting_Account_Select', NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Provincial_Tax_Posting_Account_Select', NETSUITE_ADVANCED: 'Policy_Accounting_NetSuite_Advanced', + NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Reimbursement_Account_Select', + NETSUITE_COLLECTION_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Collection_Account_Select', + NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT: 'Policy_Accounting_NetSuite_Expense_Report_Approval_Level_Select', + NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT: 'Policy_Accounting_NetSuite_Vendor_Bill_Approval_Level_Select', + NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT: 'Policy_Accounting_NetSuite_Journal_Entry_Approval_Level_Select', + NETSUITE_APPROVAL_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Approval_Account_Select', + NETSUITE_CUSTOM_FORM_ID: 'Policy_Accounting_NetSuite_Custom_Form_ID', SAGE_INTACCT_PREREQUISITES: 'Policy_Accounting_Sage_Intacct_Prerequisites', ENTER_SAGE_INTACCT_CREDENTIALS: 'Policy_Enter_Sage_Intacct_Credentials', EXISTING_SAGE_INTACCT_CONNECTIONS: 'Policy_Existing_Sage_Intacct_Connections', diff --git a/src/languages/en.ts b/src/languages/en.ts index 1607e9b858d5..477db7b95af6 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2293,8 +2293,12 @@ export default { autoSyncDescription: 'Expensify will automatically sync with NetSuite every day.', reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the NetSuite account below.', reimbursementsAccount: 'Reimbursements account', + reimbursementsAccountDescription: "Choose the bank account you'll use for reimbursements, and we'll create the associated payment in NetSuite.", collectionsAccount: 'Collections account', + collectionsAccountDescription: 'Once an invoice is marked as paid in Expensify and exported to NetSuite, it’ll appear against the account below.', approvalAccount: 'A/P approval account', + approvalAccountDescription: + 'Choose the account that transactions will be approved against in NetSuite. If you’re syncing reimbursed reports, this is also the account that bill payments will be created against.', defaultApprovalAccount: 'NetSuite default', inviteEmployees: 'Invite employees and set approvals', inviteEmployeesDescription: @@ -2308,6 +2312,7 @@ export default { customFormIDNonReimbursable: 'Non-reimbursable expense', exportReportsTo: { label: 'Expense report approval level', + description: 'Once an expense report is approved in Expensify and exported to NetSuite, you can set an additional level of approval in NetSuite prior to posting.', values: { [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_APPROVED_NONE]: 'NetSuite default preference', [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_SUPERVISOR_APPROVED]: 'Only supervisor approved', @@ -2317,6 +2322,7 @@ export default { }, exportVendorBillsTo: { label: 'Vendor bill approval level', + description: 'Once a vendor bill is approved in Expensify and exported to NetSuite, you can set an additional level of approval in NetSuite prior to posting.', values: { [CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL.VENDOR_BILLS_APPROVED_NONE]: 'NetSuite default preference', [CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL.VENDOR_BILLS_APPROVAL_PENDING]: 'Pending approval', @@ -2325,6 +2331,7 @@ export default { }, exportJournalsTo: { label: 'Journal entry approval level', + description: 'Once a journal entry is approved in Expensify and exported to NetSuite, you can set an additional level of approval in NetSuite prior to posting.', values: { [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVED_NONE]: 'NetSuite default preference', [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVAL_PENDING]: 'Pending approval', diff --git a/src/languages/es.ts b/src/languages/es.ts index c940fe78e7cf..6dae9b8068a3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2334,8 +2334,12 @@ export default { reimbursedReportsDescription: 'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de NetSuite indicadas a continuación.', reimbursementsAccount: 'Cuenta de reembolsos', + reimbursementsAccountDescription: "Choose the bank account you'll use for reimbursements, and we'll create the associated payment in NetSuite.", collectionsAccount: 'Cuenta de cobros', + collectionsAccountDescription: 'Once an invoice is marked as paid in Expensify and exported to NetSuite, it’ll appear against the account below.', approvalAccount: 'Cuenta de aprobación de cuentas por pagar', + approvalAccountDescription: + 'Choose the account that transactions will be approved against in NetSuite. If you’re syncing reimbursed reports, this is also the account that bill payments will be created against.', defaultApprovalAccount: 'Preferencia predeterminada de NetSuite', inviteEmployees: 'Invitar empleados y establecer aprobaciones', inviteEmployeesDescription: @@ -2349,6 +2353,7 @@ export default { customFormIDNonReimbursable: 'Gasto no reembolsable', exportReportsTo: { label: 'Nivel de aprobación del informe de gastos', + description: 'Once an expense report is approved in Expensify and exported to NetSuite, you can set an additional level of approval in NetSuite prior to posting.', values: { [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_APPROVED_NONE]: 'Preferencia predeterminada de NetSuite', [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_SUPERVISOR_APPROVED]: 'Solo aprobado por el supervisor', @@ -2358,6 +2363,7 @@ export default { }, exportVendorBillsTo: { label: 'Nivel de aprobación de facturas de proveedores', + description: 'Once a vendor bill is approved in Expensify and exported to NetSuite, you can set an additional level of approval in NetSuite prior to posting.', values: { [CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL.VENDOR_BILLS_APPROVED_NONE]: 'Preferencia predeterminada de NetSuite', [CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL.VENDOR_BILLS_APPROVAL_PENDING]: 'Aprobación pendiente', @@ -2366,6 +2372,7 @@ export default { }, exportJournalsTo: { label: 'Nivel de aprobación de asientos contables', + description: 'Once a journal entry is approved in Expensify and exported to NetSuite, you can set an additional level of approval in NetSuite prior to posting.', values: { [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVED_NONE]: 'Preferencia predeterminada de NetSuite', [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVAL_PENDING]: 'Aprobación pendiente', diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 1bf1efc8af24..c5a93cf9f6d8 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1,5 +1,6 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; +import type {NetSuiteCustomFormIDOptions} from '@src/types/onyx/Policy'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import type * as Parameters from './parameters'; import type SignInUserParams from './parameters/SignInUserParams'; @@ -263,6 +264,14 @@ const WRITE_COMMANDS = { UPDATE_NETSUITE_AUTO_CREATE_ENTITIES: 'UpdateNetSuiteAutoCreateEntities', UPDATE_NETSUITE_ENABLE_NEW_CATEGORIES: 'UpdateNetSuiteEnableNewCategories', UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_ENABLED: 'UpdateNetSuiteCustomFormIDOptionsEnabled', + UPDATE_NETSUITE_REIMBURSEMENT_ACCOUNT_ID: 'UpdateNetSuiteReimbursementAccountID', + UPDATE_NETSUITE_COLLECTION_ACCOUNT: 'UpdateNetSuiteCollectionAccount', + UPDATE_NETSUITE_EXPORT_REPORTS_TO: 'UpdateNetSuiteExportReportsTo', + UPDATE_NETSUITE_VENDOR_BILLS_TO: 'UpdateNetSuiteExportVendorBillsTo', + UPDATE_NETSUITE_JOURNALS_TO: 'UpdateNetSuiteExportJournalsTo', + UPDATE_NETSUITE_APPROVAL_ACCOUNT: 'UpdateNetSuiteApprovalAccount', + UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE: 'UpdateNetSuiteCustomFormIDOptionsReimbursable', + UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE: 'UpdateNetSuiteCustomFormIDOptionsNonReimbursable', REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE: 'RequestExpensifyCardLimitIncrease', CONNECT_POLICY_TO_SAGE_INTACCT: 'ConnectPolicyToSageIntacct', CONNECT_POLICY_TO_NETSUITE: 'ConnectPolicyToNetSuite', @@ -540,6 +549,14 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_NETSUITE_AUTO_CREATE_ENTITIES]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>; [WRITE_COMMANDS.UPDATE_NETSUITE_ENABLE_NEW_CATEGORIES]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>; [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_ENABLED]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>; + [WRITE_COMMANDS.UPDATE_NETSUITE_REIMBURSEMENT_ACCOUNT_ID]: Parameters.UpdateNetSuiteGenericTypeParams<'bankAccountID', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_COLLECTION_ACCOUNT]: Parameters.UpdateNetSuiteGenericTypeParams<'bankAccountID', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_REPORTS_TO]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; + [WRITE_COMMANDS.UPDATE_NETSUITE_VENDOR_BILLS_TO]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; + [WRITE_COMMANDS.UPDATE_NETSUITE_JOURNALS_TO]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; + [WRITE_COMMANDS.UPDATE_NETSUITE_APPROVAL_ACCOUNT]: Parameters.UpdateNetSuiteGenericTypeParams<'value', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE]: Parameters.UpdateNetSuiteGenericTypeParams<'reimbursable', NetSuiteCustomFormIDOptions>; + [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE]: Parameters.UpdateNetSuiteGenericTypeParams<'nonreimbursable', NetSuiteCustomFormIDOptions>; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index fe631e4cd0b1..1d13d5d21dc1 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -352,6 +352,19 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_ADVANCED]: () => require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteReimbursementAccountSelectPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_COLLECTION_ACCOUNT_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteCollectionAccountSelectPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteExpenseReportApprovalLevelSelectPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteVendorBillApprovalLevelSelectPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteJournalEntryApprovalLevelSelectPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_APPROVAL_ACCOUNT_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteApprovalAccountSelectPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_CUSTOM_FORM_ID]: () => require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage').default, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES]: () => require('../../../../pages/workspace/accounting/intacct/IntacctPrerequisitesPage').default, [SCREENS.WORKSPACE.ACCOUNTING.ENTER_SAGE_INTACCT_CREDENTIALS]: () => diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 78732767f4c1..e5ea2d9fa2e4 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -75,6 +75,13 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_TAX_POSTING_ACCOUNT_SELECT, SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT, SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_ADVANCED, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_COLLECTION_ACCOUNT_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_APPROVAL_ACCOUNT_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_CUSTOM_FORM_ID, SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES, SCREENS.WORKSPACE.ACCOUNTING.ENTER_SAGE_INTACCT_CREDENTIALS, SCREENS.WORKSPACE.ACCOUNTING.EXISTING_SAGE_INTACCT_CONNECTIONS, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index eb2646a048c7..ff83a64d580e 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -401,6 +401,27 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_ADVANCED]: { path: ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.route, }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_COLLECTION_ACCOUNT_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_COLLECTION_ACCOUNT_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_APPROVAL_ACCOUNT_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_APPROVAL_ACCOUNT_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_CUSTOM_FORM_ID]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID.route, + }, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PREREQUISITES.route}, [SCREENS.WORKSPACE.ACCOUNTING.ENTER_SAGE_INTACCT_CREDENTIALS]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ENTER_CREDENTIALS.route}, [SCREENS.WORKSPACE.ACCOUNTING.EXISTING_SAGE_INTACCT_CONNECTIONS]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXISTING_CONNECTIONS.route}, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 183e21fc67b4..746b37bbe26a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -479,6 +479,28 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_ADVANCED]: { policyID: string; }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_COLLECTION_ACCOUNT_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_APPROVAL_ACCOUNT_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_CUSTOM_FORM_ID]: { + policyID: string; + expenseType: ValueOf; + }; [SCREENS.GET_ASSISTANCE]: { backTo: Routes; }; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 4c071317907b..392ac1156c92 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -8,9 +8,10 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx'; -import type {ConnectionLastSync, Connections, CustomUnit, NetSuiteConnection, PolicyFeatureName, Rate, Tenant} from '@src/types/onyx/Policy'; +import type {ConnectionLastSync, Connections, CustomUnit, NetSuiteAccount, NetSuiteConnection, PolicyFeatureName, Rate, Tenant} from '@src/types/onyx/Policy'; import type PolicyEmployee from '@src/types/onyx/PolicyEmployee'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import * as Localize from './Localize'; import Navigation from './Navigation/Navigation'; import * as NetworkStore from './Network/NetworkStore'; import {getAccountIDsByLogins, getLoginsByAccountIDs, getPersonalDetailByEmail} from './PersonalDetailsUtils'; @@ -538,6 +539,49 @@ function canUseProvincialTaxNetSuite(subsidiaryCountry?: string) { return subsidiaryCountry === '_canada'; } +function getNetSuiteReimbursableAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] { + const payableAccounts = policy?.connections?.netsuite.options.data.payableList ?? []; + let accountsToConsider = (payableAccounts ?? []).filter(({type}) => type === '_bank'); + accountsToConsider = accountsToConsider.concat((payableAccounts ?? []).filter(({type}) => type === '_creditCard')); + + return accountsToConsider.map(({id, name}) => ({ + value: id, + text: name, + keyForList: id, + isSelected: selectedBankAccountId === id, + })); +} + +function getNetSuiteCollectionAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] { + const payableAccounts = policy?.connections?.netsuite.options.data.payableList ?? []; + const accountsToConsider = (payableAccounts ?? []).filter(({type}) => type === '_bank'); + + return accountsToConsider.map(({id, name}) => ({ + value: id, + text: name, + keyForList: id, + isSelected: selectedBankAccountId === id, + })); +} + +function getNetSuiteApprovalAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] { + const payableAccounts = policy?.connections?.netsuite.options.data.payableList ?? []; + const defaultApprovalAccount: NetSuiteAccount = { + id: CONST.NETSUITE_APPROVAL_ACCOUNT_DEFAULT, + name: Localize.translateLocal('workspace.netsuite.advancedConfig.defaultApprovalAccount'), + type: '_accountsPayable', + }; + let accountsToConsider = [defaultApprovalAccount]; + accountsToConsider = accountsToConsider.concat((payableAccounts ?? []).filter(({type}) => type === '_accountsPayable')); + + return accountsToConsider.map(({id, name}) => ({ + value: id, + text: name, + keyForList: id, + isSelected: selectedBankAccountId === id, + })); +} + function getCustomersOrJobsLabelNetSuite(policy: Policy | undefined, translate: LocaleContextProps['translate']): string | undefined { const importMapping = policy?.connections?.netsuite?.options?.config?.syncOptions?.mapping; if (!importMapping?.customers && !importMapping?.jobs) { @@ -670,6 +714,9 @@ export { getNetSuiteVendorOptions, canUseTaxNetSuite, canUseProvincialTaxNetSuite, + getNetSuiteReimbursableAccountOptions, + getNetSuiteCollectionAccountOptions, + getNetSuiteApprovalAccountOptions, getNetSuitePayableAccountOptions, getNetSuiteReceivableAccountOptions, getNetSuiteInvoiceItemOptions, diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts index 20f7fcd6e483..f77180026a4f 100644 --- a/src/libs/actions/connections/NetSuiteCommands.ts +++ b/src/libs/actions/connections/NetSuiteCommands.ts @@ -7,7 +7,7 @@ import {WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Connections} from '@src/types/onyx/Policy'; +import type {Connections, NetSuiteCustomFormID} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; type SubsidiaryParam = { @@ -729,7 +729,125 @@ function updateNetSuiteCustomFormIDOptionsEnabled(policyID: string, value: boole API.write(WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_ENABLED, parameters, onyxData); } +function updateNetSuiteReimbursementAccountID(policyID: string, bankAccountID: string, oldBankAccountID?: string) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.REIMBURSEMENT_ACCOUNT_ID, bankAccountID, oldBankAccountID); + + const parameters = { + policyID, + bankAccountID, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_REIMBURSEMENT_ACCOUNT_ID, parameters, onyxData); +} + +function updateNetSuiteCollectionAccount(policyID: string, bankAccountID: string, oldBankAccountID?: string) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.REIMBURSEMENT_ACCOUNT_ID, bankAccountID, oldBankAccountID); + + const parameters = { + policyID, + bankAccountID, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_COLLECTION_ACCOUNT, parameters, onyxData); +} + +function updateNetSuiteExportReportsTo( + policyID: string, + approvalLevel: ValueOf, + oldApprovalLevel: ValueOf, +) { + const onyxData = updateNetSuiteSyncOptionsOnyxData(policyID, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_REPORTS_TO, approvalLevel, oldApprovalLevel); + + const parameters = { + policyID, + value: approvalLevel, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_REPORTS_TO, parameters, onyxData); +} + +function updateNetSuiteExportVendorBillsTo( + policyID: string, + approvalLevel: ValueOf, + oldApprovalLevel: ValueOf, +) { + const onyxData = updateNetSuiteSyncOptionsOnyxData(policyID, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_VENDOR_BILLS_TO, approvalLevel, oldApprovalLevel); + + const parameters = { + policyID, + value: approvalLevel, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_VENDOR_BILLS_TO, parameters, onyxData); +} + +function updateNetSuiteExportJournalsTo( + policyID: string, + approvalLevel: ValueOf, + oldApprovalLevel: ValueOf, +) { + const onyxData = updateNetSuiteSyncOptionsOnyxData(policyID, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_JOURNALS_TO, approvalLevel, oldApprovalLevel); + + const parameters = { + policyID, + value: approvalLevel, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_JOURNALS_TO, parameters, onyxData); +} + +function updateNetSuiteApprovalAccount(policyID: string, value: string, oldValue: string) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.APPROVAL_ACCOUNT, value, oldValue); + + const parameters = { + policyID, + value, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_APPROVAL_ACCOUNT, parameters, onyxData); +} + +function getNetSuiteCustomFormIDOptionsParamsAndCommand(policyID: string, isReimbursable: boolean, expenseType: ValueOf, value: string) { + if (isReimbursable) { + return { + command: WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE, + parameters: { + policyID, + reimbursable: { + [expenseType]: value, + }, + }, + }; + } + return { + command: WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE, + parameters: { + policyID, + nonreimbursable: { + [expenseType]: value, + }, + }, + }; +} + +function updateNetSuiteCustomFormIDOptions( + policyID: string, + value: string, + isReimbursable: boolean, + exportDestination: ValueOf, + oldCustomFormID?: NetSuiteCustomFormID, +) { + const customFormIDKey = isReimbursable ? 'reimbursable' : 'nonReimbursable'; + const data = { + [customFormIDKey]: { + [CONST.NETSUITE_MAP_EXPORT_DESTINATION[exportDestination]]: value, + }, + }; + const oldData = { + [customFormIDKey]: oldCustomFormID?.[customFormIDKey] ?? null, + }; + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_OPTIONS, data, oldData); + + const {command, parameters} = getNetSuiteCustomFormIDOptionsParamsAndCommand(policyID, isReimbursable, CONST.NETSUITE_MAP_EXPORT_DESTINATION[exportDestination], value); + API.write(command, parameters, onyxData); +} + export { + connectPolicyToNetSuite, updateNetSuiteSubsidiary, updateNetSuiteSyncTaxConfiguration, updateNetSuiteExporter, @@ -755,5 +873,11 @@ export { updateNetSuiteAutoCreateEntities, updateNetSuiteEnableNewCategories, updateNetSuiteCustomFormIDOptionsEnabled, - connectPolicyToNetSuite, + updateNetSuiteReimbursementAccountID, + updateNetSuiteCollectionAccount, + updateNetSuiteExportReportsTo, + updateNetSuiteExportVendorBillsTo, + updateNetSuiteExportJournalsTo, + updateNetSuiteApprovalAccount, + updateNetSuiteCustomFormIDOptions, }; diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx index 741395555f4a..c1836ce2d1a7 100644 --- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx @@ -7,12 +7,14 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections/NetSuiteCommands'; import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; import type {DividerLineItem, MenuItem, ToggleItem} from '@pages/workspace/accounting/netsuite/types'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); @@ -72,26 +74,29 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { { type: 'menuitem', description: translate('workspace.netsuite.advancedConfig.reimbursementsAccount'), - onPress: () => {}, // TODO: This will be implemented in a future PR + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT.getRoute(policyID)), brickRoadIndicator: config?.errorFields?.reimbursementAccountID ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: selectedReimbursementAccount ? selectedReimbursementAccount.name : undefined, pendingAction: config?.pendingFields?.reimbursementAccountID, errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.REIMBURSEMENT_ACCOUNT_ID), onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.REIMBURSEMENT_ACCOUNT_ID), + shouldHide: config?.reimbursableExpensesExportDestination === CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY, }, { type: 'menuitem', description: translate('workspace.netsuite.advancedConfig.collectionsAccount'), - onPress: () => {}, // TODO: This will be implemented in a future PR + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_COLLECTION_ACCOUNT_SELECT.getRoute(policyID)), brickRoadIndicator: config?.errorFields?.collectionAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: selectedCollectionAccount ? selectedCollectionAccount.name : undefined, pendingAction: config?.pendingFields?.collectionAccount, errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.COLLECTION_ACCOUNT), onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.COLLECTION_ACCOUNT), + shouldHide: config?.reimbursableExpensesExportDestination === CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY, }, { type: 'divider', key: 'divider2', + shouldHide: config?.reimbursableExpensesExportDestination === CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY, }, { type: 'toggle', @@ -137,7 +142,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { { type: 'menuitem', description: translate('workspace.netsuite.advancedConfig.exportReportsTo.label'), - onPress: () => {}, // TODO: This will be implemented in a future PR + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT.getRoute(policyID)), brickRoadIndicator: config?.errorFields?.exportReportsTo ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: config?.syncOptions.exportReportsTo ? translate(`workspace.netsuite.advancedConfig.exportReportsTo.values.${config.syncOptions.exportReportsTo}`) : undefined, pendingAction: config?.syncOptions.pendingFields?.exportReportsTo, @@ -148,7 +153,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { { type: 'menuitem', description: translate('workspace.netsuite.advancedConfig.exportVendorBillsTo.label'), - onPress: () => {}, // TODO: This will be implemented in a future PR + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT.getRoute(policyID)), brickRoadIndicator: config?.errorFields?.exportVendorBillsTo ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: config?.syncOptions.exportVendorBillsTo ? translate(`workspace.netsuite.advancedConfig.exportVendorBillsTo.values.${config.syncOptions.exportVendorBillsTo}`) : undefined, pendingAction: config?.syncOptions.pendingFields?.exportVendorBillsTo, @@ -161,7 +166,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { { type: 'menuitem', description: translate('workspace.netsuite.advancedConfig.exportJournalsTo.label'), - onPress: () => {}, // TODO: This will be implemented in a future PR + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT.getRoute(policyID)), brickRoadIndicator: config?.errorFields?.exportJournalsTo ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: config?.syncOptions.exportJournalsTo ? translate(`workspace.netsuite.advancedConfig.exportJournalsTo.values.${config.syncOptions.exportJournalsTo}`) : undefined, pendingAction: config?.syncOptions.pendingFields?.exportJournalsTo, @@ -174,7 +179,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { { type: 'menuitem', description: translate('workspace.netsuite.advancedConfig.approvalAccount'), - onPress: () => {}, // TODO: This will be implemented in a future PR + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_APPROVAL_ACCOUNT_SELECT.getRoute(policyID)), brickRoadIndicator: config?.errorFields?.approvalAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: selectedApprovalAccount ? selectedApprovalAccount.name : undefined, pendingAction: config?.pendingFields?.approvalAccount, @@ -200,7 +205,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { { type: 'menuitem', description: translate('workspace.netsuite.advancedConfig.customFormIDReimbursable'), - onPress: () => {}, // TODO: This will be implemented in a future PR + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE)), brickRoadIndicator: config?.errorFields?.customFormIDOptions ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: config?.customFormIDOptions?.reimbursable[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.reimbursableExpensesExportDestination]], pendingAction: config?.pendingFields?.customFormIDOptions, @@ -211,7 +216,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { { type: 'menuitem', description: translate('workspace.netsuite.advancedConfig.customFormIDNonReimbursable'), - onPress: () => {}, // TODO: This will be implemented in a future PR + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.NON_REIMBURSABLE)), brickRoadIndicator: config?.errorFields?.customFormIDOptions ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: config?.customFormIDOptions?.reimbursable[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.nonreimbursableExpensesExportDestination]], pendingAction: config?.pendingFields?.customFormIDOptions, diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteApprovalAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteApprovalAccountSelectPage.tsx new file mode 100644 index 000000000000..ed823d698d2f --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteApprovalAccountSelectPage.tsx @@ -0,0 +1,90 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@libs/Navigation/Navigation'; +import {getNetSuiteApprovalAccountOptions} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function NetSuiteApprovalAccountSelectPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + + const config = policy?.connections?.netsuite.options.config; + const netsuiteApprovalAccountOptions = useMemo( + () => getNetSuiteApprovalAccountOptions(policy ?? undefined, config?.approvalAccount), + // The default option will be language dependent, so we need to recompute the options when the language changes + // eslint-disable-next-line react-hooks/exhaustive-deps + [config?.approvalAccount, policy, translate], + ); + + const initiallyFocusedOptionKey = useMemo(() => netsuiteApprovalAccountOptions?.find((mode) => mode.isSelected)?.keyForList, [netsuiteApprovalAccountOptions]); + + const updateCollectionAccount = useCallback( + ({value}: SelectorType) => { + if (config?.approvalAccount !== value) { + Connections.updateNetSuiteApprovalAccount(policyID, value, config?.approvalAccount ?? CONST.NETSUITE_APPROVAL_ACCOUNT_DEFAULT); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID)); + }, + [policyID, config?.approvalAccount], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + + const headerContent = useMemo( + () => ( + + {translate('workspace.netsuite.advancedConfig.approvalAccountDescription')} + + ), + [translate, styles.pb5, styles.ph5], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID))} + title="workspace.netsuite.advancedConfig.approvalAccount" + listEmptyContent={listEmptyContent} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + /> + ); +} + +NetSuiteApprovalAccountSelectPage.displayName = 'NetSuiteApprovalAccountSelectPage'; + +export default withPolicyConnections(NetSuiteApprovalAccountSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCollectionAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCollectionAccountSelectPage.tsx new file mode 100644 index 000000000000..c1851a84f384 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCollectionAccountSelectPage.tsx @@ -0,0 +1,89 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@libs/Navigation/Navigation'; +import {getNetSuiteCollectionAccountOptions} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function NetSuiteCollectionAccountSelectPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + + const config = policy?.connections?.netsuite.options.config; + const netsuiteCollectionAccountOptions = useMemo( + () => getNetSuiteCollectionAccountOptions(policy ?? undefined, config?.collectionAccount), + [config?.collectionAccount, policy], + ); + + const initiallyFocusedOptionKey = useMemo(() => netsuiteCollectionAccountOptions?.find((mode) => mode.isSelected)?.keyForList, [netsuiteCollectionAccountOptions]); + + const updateCollectionAccount = useCallback( + ({value}: SelectorType) => { + if (config?.collectionAccount !== value) { + Connections.updateNetSuiteCollectionAccount(policyID, value, config?.collectionAccount); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID)); + }, + [policyID, config?.collectionAccount], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + + const headerContent = useMemo( + () => ( + + {translate('workspace.netsuite.advancedConfig.collectionsAccountDescription')} + + ), + [translate, styles.pb5, styles.ph5], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID))} + title="workspace.netsuite.advancedConfig.collectionsAccount" + listEmptyContent={listEmptyContent} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldBeBlocked={config?.reimbursableExpensesExportDestination === CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY} + /> + ); +} + +NetSuiteCollectionAccountSelectPage.displayName = 'NetSuiteCollectionAccountSelectPage'; + +export default withPolicyConnections(NetSuiteCollectionAccountSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx new file mode 100644 index 000000000000..ce89a2daac76 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx @@ -0,0 +1,83 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import ConnectionLayout from '@components/ConnectionLayout'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@libs/Navigation/Navigation'; +import type {ExpenseRouteParams} from '@pages/workspace/accounting/netsuite/types'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; + +function NetSuiteCustomFormIDPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? '-1'; + const route = useRoute(); + const params = route.params as ExpenseRouteParams; + const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE; + + const config = policy?.connections?.netsuite?.options.config; + + const exportDestination = + (isReimbursable ? config?.reimbursableExpensesExportDestination : config?.nonreimbursableExpensesExportDestination) ?? CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT; + const customFormIDKey = isReimbursable ? 'reimbursable' : 'nonReimbursable'; + + const updateCustomFormID = useCallback( + (formValues: FormOnyxValues) => { + if (config?.customFormIDOptions?.[customFormIDKey]?.[CONST.NETSUITE_MAP_EXPORT_DESTINATION[exportDestination]] !== formValues[params.expenseType]) { + Connections.updateNetSuiteCustomFormIDOptions(policyID, formValues[params.expenseType], isReimbursable, exportDestination, config?.customFormIDOptions); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID)); + }, + [config?.customFormIDOptions, customFormIDKey, exportDestination, isReimbursable, params.expenseType, policyID], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID))} + headerTitle={`workspace.netsuite.advancedConfig.${isReimbursable ? 'customFormIDReimbursable' : 'customFormIDNonReimbursable'}`} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + contentContainerStyle={styles.pb2} + titleStyle={styles.ph5} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + > + + + + + + + ); +} + +NetSuiteCustomFormIDPage.displayName = 'NetSuiteCustomFormIDPage'; + +export default withPolicyConnections(NetSuiteCustomFormIDPage); diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteExpenseReportApprovalLevelSelectPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteExpenseReportApprovalLevelSelectPage.tsx new file mode 100644 index 000000000000..ed70da6f1608 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteExpenseReportApprovalLevelSelectPage.tsx @@ -0,0 +1,74 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +function NetSuiteExpenseReportApprovalLevelSelectPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const policyID = policy?.id ?? '-1'; + const styles = useThemeStyles(); + const config = policy?.connections?.netsuite.options.config; + const data: MenuListItem[] = Object.values(CONST.NETSUITE_REPORTS_APPROVAL_LEVEL).map((approvalType) => ({ + value: approvalType, + text: translate(`workspace.netsuite.advancedConfig.exportReportsTo.values.${approvalType}`), + keyForList: approvalType, + isSelected: config?.syncOptions.exportReportsTo === approvalType, + })); + + const headerContent = useMemo( + () => ( + + {translate('workspace.netsuite.advancedConfig.exportReportsTo.description')} + + ), + [translate, styles.pb5, styles.ph5], + ); + + const selectExpenseReportApprovalLevel = useCallback( + (row: MenuListItem) => { + if (row.value !== config?.syncOptions.exportReportsTo) { + Connections.updateNetSuiteExportReportsTo(policyID, row.value, config?.syncOptions.exportReportsTo ?? CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_APPROVED_NONE); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID)); + }, + [config?.syncOptions.exportReportsTo, policyID], + ); + + return ( + selectExpenseReportApprovalLevel(selection as MenuListItem)} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldBeBlocked={config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT} + /> + ); +} + +NetSuiteExpenseReportApprovalLevelSelectPage.displayName = 'NetSuiteExpenseReportApprovalLevelSelectPage'; + +export default withPolicyConnections(NetSuiteExpenseReportApprovalLevelSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteJournalEntryApprovalLevelSelectPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteJournalEntryApprovalLevelSelectPage.tsx new file mode 100644 index 000000000000..ee1d1108ffd6 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteJournalEntryApprovalLevelSelectPage.tsx @@ -0,0 +1,77 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +function NetSuiteJournalEntryApprovalLevelSelectPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const policyID = policy?.id ?? '-1'; + const styles = useThemeStyles(); + const config = policy?.connections?.netsuite.options.config; + const data: MenuListItem[] = Object.values(CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL).map((approvalType) => ({ + value: approvalType, + text: translate(`workspace.netsuite.advancedConfig.exportJournalsTo.values.${approvalType}`), + keyForList: approvalType, + isSelected: config?.syncOptions.exportJournalsTo === approvalType, + })); + + const headerContent = useMemo( + () => ( + + {translate('workspace.netsuite.advancedConfig.exportJournalsTo.description')} + + ), + [translate, styles.pb5, styles.ph5], + ); + + const selectJournalApprovalLevel = useCallback( + (row: MenuListItem) => { + if (row.value !== config?.syncOptions.exportJournalsTo) { + Connections.updateNetSuiteExportJournalsTo(policyID, row.value, config?.syncOptions.exportJournalsTo ?? CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVED_NONE); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID)); + }, + [config?.syncOptions.exportJournalsTo, policyID], + ); + + return ( + selectJournalApprovalLevel(selection as MenuListItem)} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldBeBlocked={ + config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY && + config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY + } + /> + ); +} + +NetSuiteJournalEntryApprovalLevelSelectPage.displayName = 'NetSuiteJournalEntryApprovalLevelSelectPage'; + +export default withPolicyConnections(NetSuiteJournalEntryApprovalLevelSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteReimbursementAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteReimbursementAccountSelectPage.tsx new file mode 100644 index 000000000000..4a9898fb187e --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteReimbursementAccountSelectPage.tsx @@ -0,0 +1,89 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@libs/Navigation/Navigation'; +import {getNetSuiteReimbursableAccountOptions} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function NetSuiteReimbursementAccountSelectPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + + const config = policy?.connections?.netsuite.options.config; + const netsuiteReimbursableAccountOptions = useMemo( + () => getNetSuiteReimbursableAccountOptions(policy ?? undefined, config?.reimbursementAccountID), + [config?.reimbursementAccountID, policy], + ); + + const initiallyFocusedOptionKey = useMemo(() => netsuiteReimbursableAccountOptions?.find((mode) => mode.isSelected)?.keyForList, [netsuiteReimbursableAccountOptions]); + + const updateReimbursementAccount = useCallback( + ({value}: SelectorType) => { + if (config?.reimbursementAccountID !== value) { + Connections.updateNetSuiteReimbursementAccountID(policyID, value, config?.reimbursementAccountID); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID)); + }, + [policyID, config?.reimbursementAccountID], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + + const headerContent = useMemo( + () => ( + + {translate('workspace.netsuite.advancedConfig.reimbursementsAccountDescription')} + + ), + [translate, styles.pb5, styles.ph5], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID))} + title="workspace.netsuite.advancedConfig.reimbursementsAccount" + listEmptyContent={listEmptyContent} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldBeBlocked={config?.reimbursableExpensesExportDestination === CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY} + /> + ); +} + +NetSuiteReimbursementAccountSelectPage.displayName = 'NetSuiteReimbursementAccountSelectPage'; + +export default withPolicyConnections(NetSuiteReimbursementAccountSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteVendorBillApprovalLevelSelectPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteVendorBillApprovalLevelSelectPage.tsx new file mode 100644 index 000000000000..ecbaacd46bad --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteVendorBillApprovalLevelSelectPage.tsx @@ -0,0 +1,81 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +function NetSuiteVendorBillApprovalLevelSelectPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const policyID = policy?.id ?? '-1'; + const styles = useThemeStyles(); + const config = policy?.connections?.netsuite.options.config; + const data: MenuListItem[] = Object.values(CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL).map((approvalType) => ({ + value: approvalType, + text: translate(`workspace.netsuite.advancedConfig.exportVendorBillsTo.values.${approvalType}`), + keyForList: approvalType, + isSelected: config?.syncOptions.exportVendorBillsTo === approvalType, + })); + + const headerContent = useMemo( + () => ( + + {translate('workspace.netsuite.advancedConfig.exportVendorBillsTo.description')} + + ), + [translate, styles.pb5, styles.ph5], + ); + + const selectVendorBillApprovalLevel = useCallback( + (row: MenuListItem) => { + if (row.value !== config?.syncOptions.exportVendorBillsTo) { + Connections.updateNetSuiteExportVendorBillsTo( + policyID, + row.value, + config?.syncOptions.exportVendorBillsTo ?? CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL.VENDOR_BILLS_APPROVED_NONE, + ); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID)); + }, + [config?.syncOptions.exportVendorBillsTo, policyID], + ); + + return ( + selectVendorBillApprovalLevel(selection as MenuListItem)} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldBeBlocked={ + config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL && + config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL + } + /> + ); +} + +NetSuiteVendorBillApprovalLevelSelectPage.displayName = 'NetSuiteVendorBillApprovalLevelSelectPage'; + +export default withPolicyConnections(NetSuiteVendorBillApprovalLevelSelectPage); diff --git a/src/types/form/NetSuiteCustomFormIDForm.ts b/src/types/form/NetSuiteCustomFormIDForm.ts new file mode 100644 index 000000000000..eb1ea8625075 --- /dev/null +++ b/src/types/form/NetSuiteCustomFormIDForm.ts @@ -0,0 +1,16 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; +import type Form from './Form'; + +type InputID = ValueOf; + +type NetSuiteCustomFormIDForm = Form< + InputID, + { + [CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE]: string; + [CONST.NETSUITE_EXPENSE_TYPE.NON_REIMBURSABLE]: string; + } +>; + +// eslint-disable-next-line import/prefer-default-export +export type {NetSuiteCustomFormIDForm}; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index b3503099b4b3..44c0cb81cb91 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -57,4 +57,5 @@ export type {SubscriptionSizeForm} from './SubscriptionSizeForm'; export type {WorkspaceReportFieldsForm} from './WorkspaceReportFieldsForm'; export type {SageIntactCredentialsForm} from './SageIntactCredentialsForm'; export type {NetSuiteTokenInputForm} from './NetSuiteTokenInputForm'; +export type {NetSuiteCustomFormIDForm} from './NetSuiteCustomFormIDForm'; export type {default as Form} from './Form'; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 098869d7bf8e..86560166cb4e 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -718,6 +718,18 @@ type NetSuiteCustomFormIDOptions = { journalEntry?: string; }; +/** The custom form ID object */ +type NetSuiteCustomFormID = { + /** The custom form selections for reimbursable transactions */ + reimbursable: NetSuiteCustomFormIDOptions; + + /** The custom form selections for non-reimbursable transactions */ + nonReimbursable: NetSuiteCustomFormIDOptions; + + /** Whether we'll use the custom form selections upon export to NetSuite */ + enabled: boolean; +}; + /** User configuration for the NetSuite accounting integration. */ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Invoice Item Preference */ @@ -880,16 +892,7 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ payableAcct: string; /** Configurations for customer to set custom forms for which reimbursable and non-reimbursable transactions will export to in NetSuite */ - customFormIDOptions?: { - /** The custom form selections for reimbursable transactions */ - reimbursable: NetSuiteCustomFormIDOptions; - - /** The custom form selections for non-reimbursable transactions */ - nonReimbursable: NetSuiteCustomFormIDOptions; - - /** Whether we'll use the custom form selections upon export to NetSuite */ - enabled: boolean; - }; + customFormIDOptions?: NetSuiteCustomFormID; /** The account to use for Invoices export to NetSuite */ collectionAccount?: string; @@ -1355,4 +1358,7 @@ export type { NetSuiteConnection, ConnectionLastSync, NetSuiteSubsidiary, + NetSuiteAccount, + NetSuiteCustomFormIDOptions, + NetSuiteCustomFormID, }; From 35eee592797473ce0c8a258ebb4dd881ab65d28d Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 5 Jul 2024 21:13:58 +0530 Subject: [PATCH 093/124] Bug fix --- .../accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx index c1836ce2d1a7..eb2c6489241f 100644 --- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx @@ -218,7 +218,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { description: translate('workspace.netsuite.advancedConfig.customFormIDNonReimbursable'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.NON_REIMBURSABLE)), brickRoadIndicator: config?.errorFields?.customFormIDOptions ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - title: config?.customFormIDOptions?.reimbursable[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.nonreimbursableExpensesExportDestination]], + title: config?.customFormIDOptions?.nonReimbursable[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.nonreimbursableExpensesExportDestination]], pendingAction: config?.pendingFields?.customFormIDOptions, errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_OPTIONS), onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_OPTIONS), From acfa474662a516c0e5374649c5d16a04f0334a99 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 5 Jul 2024 21:21:10 +0530 Subject: [PATCH 094/124] Small bugfix in API --- src/libs/API/types.ts | 5 ++--- src/libs/actions/connections/NetSuiteCommands.ts | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c5a93cf9f6d8..1b7e04aca254 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1,6 +1,5 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; -import type {NetSuiteCustomFormIDOptions} from '@src/types/onyx/Policy'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import type * as Parameters from './parameters'; import type SignInUserParams from './parameters/SignInUserParams'; @@ -555,8 +554,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_NETSUITE_VENDOR_BILLS_TO]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; [WRITE_COMMANDS.UPDATE_NETSUITE_JOURNALS_TO]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; [WRITE_COMMANDS.UPDATE_NETSUITE_APPROVAL_ACCOUNT]: Parameters.UpdateNetSuiteGenericTypeParams<'value', string>; - [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE]: Parameters.UpdateNetSuiteGenericTypeParams<'reimbursable', NetSuiteCustomFormIDOptions>; - [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE]: Parameters.UpdateNetSuiteGenericTypeParams<'nonreimbursable', NetSuiteCustomFormIDOptions>; + [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE]: Parameters.UpdateNetSuiteGenericTypeParams<'reimbursable', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE]: Parameters.UpdateNetSuiteGenericTypeParams<'nonreimbursable', string>; }; const READ_COMMANDS = { diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts index f77180026a4f..a91fd9b1ca65 100644 --- a/src/libs/actions/connections/NetSuiteCommands.ts +++ b/src/libs/actions/connections/NetSuiteCommands.ts @@ -807,9 +807,9 @@ function getNetSuiteCustomFormIDOptionsParamsAndCommand(policyID: string, isReim command: WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE, parameters: { policyID, - reimbursable: { + reimbursable: JSON.stringify({ [expenseType]: value, - }, + }), }, }; } @@ -817,9 +817,9 @@ function getNetSuiteCustomFormIDOptionsParamsAndCommand(policyID: string, isReim command: WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE, parameters: { policyID, - nonreimbursable: { + nonreimbursable: JSON.stringify({ [expenseType]: value, - }, + }), }, }; } From f7b2940479b78f2c16657769417863c3eb75cdef Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 5 Jul 2024 22:43:27 +0530 Subject: [PATCH 095/124] Small bugfix in Custom Form ID API --- .../UpdateNetSuiteCustomFormIDParams.ts | 10 ++++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 4 +-- .../actions/connections/NetSuiteCommands.ts | 32 ++++--------------- 4 files changed, 20 insertions(+), 27 deletions(-) create mode 100644 src/libs/API/parameters/UpdateNetSuiteCustomFormIDParams.ts diff --git a/src/libs/API/parameters/UpdateNetSuiteCustomFormIDParams.ts b/src/libs/API/parameters/UpdateNetSuiteCustomFormIDParams.ts new file mode 100644 index 000000000000..01e5db8c1089 --- /dev/null +++ b/src/libs/API/parameters/UpdateNetSuiteCustomFormIDParams.ts @@ -0,0 +1,10 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type UpdateNetSuiteCustomFormIDParams = { + policyID: string; + formID: string; + formType: ValueOf; +}; + +export default UpdateNetSuiteCustomFormIDParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 848af7e34634..dae5d022baa9 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -243,3 +243,4 @@ export type {default as CreateWorkspaceReportFieldParams} from './CreateWorkspac export type {default as OpenPolicyExpensifyCardsPageParams} from './OpenPolicyExpensifyCardsPageParams'; export type {default as RequestExpensifyCardLimitIncreaseParams} from './RequestExpensifyCardLimitIncreaseParams'; export type {default as UpdateNetSuiteGenericTypeParams} from './UpdateNetSuiteGenericTypeParams'; +export type {default as UpdateNetSuiteCustomFormIDParams} from './UpdateNetSuiteCustomFormIDParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 1b7e04aca254..3c6b0751398b 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -554,8 +554,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_NETSUITE_VENDOR_BILLS_TO]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; [WRITE_COMMANDS.UPDATE_NETSUITE_JOURNALS_TO]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; [WRITE_COMMANDS.UPDATE_NETSUITE_APPROVAL_ACCOUNT]: Parameters.UpdateNetSuiteGenericTypeParams<'value', string>; - [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE]: Parameters.UpdateNetSuiteGenericTypeParams<'reimbursable', string>; - [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE]: Parameters.UpdateNetSuiteGenericTypeParams<'nonreimbursable', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE]: Parameters.UpdateNetSuiteCustomFormIDParams; + [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE]: Parameters.UpdateNetSuiteCustomFormIDParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts index a91fd9b1ca65..1858a8b9baa0 100644 --- a/src/libs/actions/connections/NetSuiteCommands.ts +++ b/src/libs/actions/connections/NetSuiteCommands.ts @@ -801,29 +801,6 @@ function updateNetSuiteApprovalAccount(policyID: string, value: string, oldValue API.write(WRITE_COMMANDS.UPDATE_NETSUITE_APPROVAL_ACCOUNT, parameters, onyxData); } -function getNetSuiteCustomFormIDOptionsParamsAndCommand(policyID: string, isReimbursable: boolean, expenseType: ValueOf, value: string) { - if (isReimbursable) { - return { - command: WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE, - parameters: { - policyID, - reimbursable: JSON.stringify({ - [expenseType]: value, - }), - }, - }; - } - return { - command: WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE, - parameters: { - policyID, - nonreimbursable: JSON.stringify({ - [expenseType]: value, - }), - }, - }; -} - function updateNetSuiteCustomFormIDOptions( policyID: string, value: string, @@ -842,8 +819,13 @@ function updateNetSuiteCustomFormIDOptions( }; const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_OPTIONS, data, oldData); - const {command, parameters} = getNetSuiteCustomFormIDOptionsParamsAndCommand(policyID, isReimbursable, CONST.NETSUITE_MAP_EXPORT_DESTINATION[exportDestination], value); - API.write(command, parameters, onyxData); + const commandName = isReimbursable ? WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE : WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE; + const parameters = { + policyID, + formType: CONST.NETSUITE_MAP_EXPORT_DESTINATION[exportDestination], + formID: value, + }; + API.write(commandName, parameters, onyxData); } export { From bea76a844c47191e8597b7bc1b297ae34a701cf3 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 5 Jul 2024 22:54:58 +0530 Subject: [PATCH 096/124] Fixing style of form --- .../accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx index ce89a2daac76..5b62674f9c9b 100644 --- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx @@ -49,9 +49,10 @@ function NetSuiteCustomFormIDPage({policy}: WithPolicyConnectionsProps) { accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} - contentContainerStyle={styles.pb2} + contentContainerStyle={[styles.flex1]} titleStyle={styles.ph5} connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldIncludeSafeAreaPaddingBottom > Date: Fri, 5 Jul 2024 23:14:43 +0530 Subject: [PATCH 097/124] Fixing style --- src/components/ConnectionLayout.tsx | 6 +++++- .../netsuite/advanced/NetSuiteCustomFormIDPage.tsx | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/ConnectionLayout.tsx b/src/components/ConnectionLayout.tsx index dc8638f018d4..4bcfdc61077f 100644 --- a/src/components/ConnectionLayout.tsx +++ b/src/components/ConnectionLayout.tsx @@ -66,6 +66,9 @@ type ConnectionLayoutProps = { /** Handler for back button press */ onBackButtonPress?: () => void; + + /** Whether or not to block user from accessing the page */ + shouldBeBlocked?: boolean; }; type ConnectionLayoutContentProps = Pick; @@ -99,6 +102,7 @@ function ConnectionLayout({ titleAlreadyTranslated, reverseConnectionEmptyCheck = false, onBackButtonPress = () => Navigation.goBack(), + shouldBeBlocked = false, }: ConnectionLayoutProps) { const {translate} = useLocalize(); @@ -123,7 +127,7 @@ function ConnectionLayout({ policyID={policyID} accessVariants={accessVariants} featureName={featureName} - shouldBeBlocked={reverseConnectionEmptyCheck ? !isConnectionEmpty : isConnectionEmpty} + shouldBeBlocked={(reverseConnectionEmptyCheck ? !isConnectionEmpty : isConnectionEmpty) || shouldBeBlocked} > Date: Sat, 6 Jul 2024 14:52:53 +0530 Subject: [PATCH 098/124] Update --- tests/unit/CurrencyUtilsTest.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/unit/CurrencyUtilsTest.ts b/tests/unit/CurrencyUtilsTest.ts index b4d09104a74c..5322faff763f 100644 --- a/tests/unit/CurrencyUtilsTest.ts +++ b/tests/unit/CurrencyUtilsTest.ts @@ -112,8 +112,6 @@ describe('CurrencyUtils', () => { [25, 0.25, 'USD'], [2500, 25, 'USD'], [2500.5, 25, 'USD'], // The backend should never send a decimal .5 value - // VND uses 0 decimals. - // https://github.com/Expensify/App/pull/43948 [2500, 25, 'VND'], [2550, 26, 'VND'], [25, 0, 'VND'], @@ -133,8 +131,6 @@ describe('CurrencyUtils', () => { [null, '', 'USD'], [undefined, '', 'USD'], [0, '0.00', 'USD'], - // VND uses 0 decimals. - // https://github.com/Expensify/App/pull/43948 [2500, '25', 'VND'], [2550, '26', 'VND'], [25, '0', 'VND'], From 299a42e622f071ddfd3c06c00a3bf50e47eff230 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 6 Jul 2024 18:45:49 +0530 Subject: [PATCH 099/124] mWeb/Safari - Attachments - New tab doesn't open when clicking the download button from the overflow menu on a .doc file. Signed-off-by: Krishna Gupta --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 43ba70d0ffcf..8f130a5546d8 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -10,6 +10,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import * as Browser from '@libs/Browser'; import Clipboard from '@libs/Clipboard'; import EmailUtils from '@libs/EmailUtils'; import * as Environment from '@libs/Environment/Environment'; @@ -514,7 +515,9 @@ const ContextMenuActions: ContextMenuAction[] = [ const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? ''); const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; Download.setDownload(sourceID, true); - fileDownload(sourceURLWithAuth, originalFileName ?? '').then(() => Download.setDownload(sourceID, false)); + const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; + const isAnchorTag = anchorRegex.test(html); + fileDownload(sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && Browser.isMobileSafari()).then(() => Download.setDownload(sourceID, false)); if (closePopover) { hideContextMenu(true, ReportActionComposeFocusManager.focus); } From 7a4fdd9ff03883b029865058ed8f5aee0e9155b1 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sun, 7 Jul 2024 00:58:37 +0300 Subject: [PATCH 100/124] now clicking same link will keep you at same place --- docs/_includes/lhn-template.html | 18 +++++++++--------- docs/_sass/_main.scss | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/_includes/lhn-template.html b/docs/_includes/lhn-template.html index 80302f33f52e..32078c1a8de6 100644 --- a/docs/_includes/lhn-template.html +++ b/docs/_includes/lhn-template.html @@ -21,25 +21,25 @@ {% for platform in site.data.routes.platforms %} {% if platform.href == activePlatform %}
  • - - + {% for hub in platform.hubs %}
      {% if hub.href == activeHub %} - - +
        {% for section in hub.sections %}
      • {% if section.href == activeSection %} - - +
          {% for article in section.articles %} {% assign article_href = section.href | append: '/' | append: article.href %} diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index 29b02d8aeb00..f3bf1035ed56 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -749,6 +749,7 @@ button { width: 20px; height: 20px; cursor: pointer; + display: inline-block; } .homepage { From 72a58fdcd69d9707a4e8914816eb8fd906178b01 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sun, 7 Jul 2024 19:45:47 +0530 Subject: [PATCH 101/124] Addressing comments --- src/CONST.ts | 11 +++++++++++ src/libs/PolicyUtils.ts | 10 +++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 051ff1b71ae5..8803895c4072 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1456,6 +1456,17 @@ const CONST = { JOURNALS_APPROVED: 'JOURNALS_APPROVED', }, + NETSUITE_ACCOUNT_TYPE: { + ACCOUNTS_PAYABLE: '_accountsPayable', + ACCOUNTS_RECEIVABLE: '_accountsReceivable', + OTHER_CURRENT_LIABILITY: '_otherCurrentLiability', + CREDIT_CARD: '_creditCard', + BANK: '_bank', + OTHER_CURRENT_ASSET: '_otherCurrentAsset', + LONG_TERM_LIABILITY: '_longTermLiability', + EXPENSE: '_expense', + }, + NETSUITE_APPROVAL_ACCOUNT_DEFAULT: 'APPROVAL_ACCOUNT_DEFAULT', /** diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 392ac1156c92..396df0143e83 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -541,8 +541,8 @@ function canUseProvincialTaxNetSuite(subsidiaryCountry?: string) { function getNetSuiteReimbursableAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] { const payableAccounts = policy?.connections?.netsuite.options.data.payableList ?? []; - let accountsToConsider = (payableAccounts ?? []).filter(({type}) => type === '_bank'); - accountsToConsider = accountsToConsider.concat((payableAccounts ?? []).filter(({type}) => type === '_creditCard')); + let accountsToConsider = (payableAccounts ?? []).filter(({type}) => type === CONST.NETSUITE_ACCOUNT_TYPE.BANK); + accountsToConsider = accountsToConsider.concat((payableAccounts ?? []).filter(({type}) => type === CONST.NETSUITE_ACCOUNT_TYPE.CREDIT_CARD)); return accountsToConsider.map(({id, name}) => ({ value: id, @@ -554,7 +554,7 @@ function getNetSuiteReimbursableAccountOptions(policy: Policy | undefined, selec function getNetSuiteCollectionAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] { const payableAccounts = policy?.connections?.netsuite.options.data.payableList ?? []; - const accountsToConsider = (payableAccounts ?? []).filter(({type}) => type === '_bank'); + const accountsToConsider = (payableAccounts ?? []).filter(({type}) => type === CONST.NETSUITE_ACCOUNT_TYPE.BANK); return accountsToConsider.map(({id, name}) => ({ value: id, @@ -569,10 +569,10 @@ function getNetSuiteApprovalAccountOptions(policy: Policy | undefined, selectedB const defaultApprovalAccount: NetSuiteAccount = { id: CONST.NETSUITE_APPROVAL_ACCOUNT_DEFAULT, name: Localize.translateLocal('workspace.netsuite.advancedConfig.defaultApprovalAccount'), - type: '_accountsPayable', + type: CONST.NETSUITE_ACCOUNT_TYPE.ACCOUNTS_PAYABLE, }; let accountsToConsider = [defaultApprovalAccount]; - accountsToConsider = accountsToConsider.concat((payableAccounts ?? []).filter(({type}) => type === '_accountsPayable')); + accountsToConsider = accountsToConsider.concat((payableAccounts ?? []).filter(({type}) => type === CONST.NETSUITE_ACCOUNT_TYPE.ACCOUNTS_PAYABLE)); return accountsToConsider.map(({id, name}) => ({ value: id, From 7adc3bc7ff46fff8ffc2f2c71b2b8e78e0f99de3 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sun, 7 Jul 2024 21:28:49 +0530 Subject: [PATCH 102/124] Adding validation to custom form ID --- src/languages/en.ts | 3 +++ src/languages/es.ts | 3 +++ .../advanced/NetSuiteCustomFormIDPage.tsx | 18 +++++++++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 477db7b95af6..3ebfe44c7d41 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2338,6 +2338,9 @@ export default { [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVED]: 'Approved for posting', }, }, + error: { + customFormID: 'Please enter a valid numeric custom form ID.', + }, }, noAccountsFound: 'No accounts found', noAccountsFoundDescription: 'Add the account in NetSuite and sync the connection again.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 6dae9b8068a3..e3f72946519d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2379,6 +2379,9 @@ export default { [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVED]: 'Aprobado para publicación', }, }, + error: { + customFormID: 'Please enter a valid numeric custom form ID.', + }, }, noAccountsFound: 'No se han encontrado cuentas', noAccountsFoundDescription: 'Añade la cuenta en NetSuite y sincroniza la conexión de nuevo.', diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx index 7b0aa82f5c84..2400e6497d63 100644 --- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx @@ -4,12 +4,14 @@ import {View} from 'react-native'; import ConnectionLayout from '@components/ConnectionLayout'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type {FormOnyxValues} from '@components/Form/types'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as ValidationUtils from '@libs/ValidationUtils'; import type {ExpenseRouteParams} from '@pages/workspace/accounting/netsuite/types'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -31,6 +33,19 @@ function NetSuiteCustomFormIDPage({policy}: WithPolicyConnectionsProps) { (isReimbursable ? config?.reimbursableExpensesExportDestination : config?.nonreimbursableExpensesExportDestination) ?? CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT; const customFormIDKey = isReimbursable ? 'reimbursable' : 'nonReimbursable'; + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors: FormInputErrors = {}; + + if (values[params.expenseType] && !ValidationUtils.isNumeric(values[params.expenseType])) { + ErrorUtils.addErrorMessage(errors, params.expenseType, translate('workspace.netsuite.advancedConfig.error.customFormID')); + } + + return errors; + }, + [params.expenseType, translate], + ); + const updateCustomFormID = useCallback( (formValues: FormOnyxValues) => { if (config?.customFormIDOptions?.[customFormIDKey]?.[CONST.NETSUITE_MAP_EXPORT_DESTINATION[exportDestination]] !== formValues[params.expenseType]) { @@ -59,6 +74,7 @@ function NetSuiteCustomFormIDPage({policy}: WithPolicyConnectionsProps) { Date: Sun, 7 Jul 2024 21:43:34 +0530 Subject: [PATCH 103/124] Fixing API call --- src/libs/actions/connections/NetSuiteCommands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts index 1858a8b9baa0..d9a32bf43911 100644 --- a/src/libs/actions/connections/NetSuiteCommands.ts +++ b/src/libs/actions/connections/NetSuiteCommands.ts @@ -740,7 +740,7 @@ function updateNetSuiteReimbursementAccountID(policyID: string, bankAccountID: s } function updateNetSuiteCollectionAccount(policyID: string, bankAccountID: string, oldBankAccountID?: string) { - const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.REIMBURSEMENT_ACCOUNT_ID, bankAccountID, oldBankAccountID); + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.COLLECTION_ACCOUNT, bankAccountID, oldBankAccountID); const parameters = { policyID, From 2eac47edd8ffbacc48b788988ef6babb430d85b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Mon, 8 Jul 2024 09:59:32 +0100 Subject: [PATCH 104/124] Bump react-native-onyx to 2.0.56 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index abc223a50b38..9cffc45a6b65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,7 +102,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.55", + "react-native-onyx": "2.0.56", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -37277,9 +37277,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.55", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.55.tgz", - "integrity": "sha512-W0+hFY98uC3uije2JBFS1ON19iAe8u6Ls50T2Qrx9NMtzUFqEchMuR75L4F/kMvi/uwtQII+Cl02Pd52h/tdPg==", + "version": "2.0.56", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.56.tgz", + "integrity": "sha512-3rn1+J4tli9zPS9w5x6tOAUz01wVHkiTFgtHoIwjD7HdLUO/9nk6H8JX6Oqb9Vzq2XQOSavUFRepIHnGvzNtgg==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index bc7306ee3782..2c1da45ad496 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.55", + "react-native-onyx": "2.0.56", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", From 6c32800a015c42a4cb7153301ed052daac7849a9 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 8 Jul 2024 11:43:04 +0200 Subject: [PATCH 105/124] Change ExpensiMark usage to Parser --- .../NetSuiteTokenInput/substeps/NetSuiteTokenInputForm.tsx | 6 ++---- .../substeps/NetSuiteTokenSetupContent.tsx | 6 ++---- .../import/NetSuiteImportCustomersOrProjectsPage.tsx | 6 ++---- .../netsuite/import/NetSuiteImportMappingPage.tsx | 6 ++---- src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx | 5 ++--- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/substeps/NetSuiteTokenInputForm.tsx b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/substeps/NetSuiteTokenInputForm.tsx index 08e2ab9460ec..3747bf1b6e4e 100644 --- a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/substeps/NetSuiteTokenInputForm.tsx +++ b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/substeps/NetSuiteTokenInputForm.tsx @@ -1,4 +1,3 @@ -import {ExpensiMark} from 'expensify-common'; import React, {useCallback} from 'react'; import {View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; @@ -12,12 +11,11 @@ import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {connectPolicyToNetSuite} from '@libs/actions/connections/NetSuiteCommands'; import * as ErrorUtils from '@libs/ErrorUtils'; +import Parser from '@libs/Parser'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/NetSuiteTokenInputForm'; -const parser = new ExpensiMark(); - function NetSuiteTokenInputForm({onNext, policyID}: SubStepProps & {policyID: string}) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -76,7 +74,7 @@ function NetSuiteTokenInputForm({onNext, policyID}: SubStepProps & {policyID: st {formInput === INPUT_IDS.NETSUITE_ACCOUNT_ID && ( ${parser.replace( + html={`${Parser.replace( translate(`workspace.netsuite.tokenInput.formSteps.enterCredentials.${formInput}Description`), )}`} /> diff --git a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/substeps/NetSuiteTokenSetupContent.tsx b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/substeps/NetSuiteTokenSetupContent.tsx index f1b9ed258d7d..889e3101fb32 100644 --- a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/substeps/NetSuiteTokenSetupContent.tsx +++ b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/substeps/NetSuiteTokenSetupContent.tsx @@ -1,4 +1,3 @@ -import {ExpensiMark} from 'expensify-common'; import React from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; @@ -8,11 +7,10 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import Parser from '@libs/Parser'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -const parser = new ExpensiMark(); - function NetSuiteTokenSetupContent({onNext, screenIndex}: SubStepProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -27,7 +25,7 @@ function NetSuiteTokenSetupContent({onNext, screenIndex}: SubStepProps) { {translate(titleKey)} - ${parser.replace(translate(description))}`} /> + ${Parser.replace(translate(description))}`} />