From 5de34fba8a874c66abed081a6e94509d1bb2d3bf Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 14 Nov 2023 17:50:11 +0500 Subject: [PATCH 001/120] perf: add memoization This memoizes relevant functions and values to not re-render LHNOptionsList and ReportActionsList when there's some update in react tree which is not relevant --- src/components/LHNOptionsList/LHNOptionsList.js | 4 ++-- src/pages/home/report/ReportActionsList.js | 4 ++-- src/pages/home/report/ReportActionsView.js | 14 +++++++------- src/pages/home/sidebar/SidebarLinks.js | 8 +++++--- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index ef1954aeb948..ec031c041c0e 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; +import React, {memo, useCallback} from 'react'; import {FlatList, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -211,4 +211,4 @@ export default compose( key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, }, }), -)(LHNOptionsList); +)(memo(LHNOptionsList)); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 759e73aa90e5..51dce09610d4 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -1,7 +1,7 @@ import {useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '@components/InvertedFlatList'; @@ -443,4 +443,4 @@ ReportActionsList.propTypes = propTypes; ReportActionsList.defaultProps = defaultProps; ReportActionsList.displayName = 'ReportActionsList'; -export default compose(withWindowDimensions, withPersonalDetails(), withCurrentUserPersonalDetails)(ReportActionsList); +export default compose(withWindowDimensions, withPersonalDetails(), withCurrentUserPersonalDetails)(memo(ReportActionsList)); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 01ec967d76b1..761c6933ff3f 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -172,25 +172,25 @@ function ReportActionsView(props) { } }, [props.report, didSubscribeToReportTypingEvents, reportID]); + const oldestReportAction = useMemo(() => _.last(props.reportActions), [props.reportActions]); + /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadOlderChats = () => { + const loadOlderChats = useCallback(() => { // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. if (props.network.isOffline || props.isLoadingOlderReportActions) { return; } - const oldestReportAction = _.last(props.reportActions); - // Don't load more chats if we're already at the beginning of the chat history if (oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }; + }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction.actionName, oldestReportAction.reportActionID, reportID]); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -227,7 +227,7 @@ function ReportActionsView(props) { /** * Runs when the FlatList finishes laying out */ - const recordTimeToMeasureItemLayout = () => { + const recordTimeToMeasureItemLayout = useCallback(() => { if (didLayout.current) { return; } @@ -242,7 +242,7 @@ function ReportActionsView(props) { } else { Performance.markEnd(CONST.TIMING.SWITCH_REPORT); } - }; + }, [hasCachedActions]); // Comments have not loaded at all yet do nothing if (!_.size(props.reportActions)) { diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index ad981a190a70..e6dee6f213d4 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {InteractionManager, View} from 'react-native'; import _ from 'underscore'; import LogoComponent from '@assets/images/expensify-wordmark.svg'; @@ -145,6 +145,8 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority ); const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT; + const listStyle = useMemo(() => [isLoading ? styles.flexShrink1 : styles.flex1], [isLoading]); + const contentContainerStyles = useMemo(() => [styles.sidebarListContainer, {paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}], [insets]); return ( @@ -177,8 +179,8 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority Date: Wed, 15 Nov 2023 15:05:32 +0500 Subject: [PATCH 002/120] perf: add memoization --- src/components/OptionsList/BaseOptionsList.js | 22 ++++++++----------- src/components/OptionsList/index.js | 4 ++-- src/components/OptionsList/index.native.js | 6 ++--- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index e0acc2534fbf..cecf983ff989 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {forwardRef, memo, useEffect, useRef} from 'react'; +import React, {forwardRef, memo, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import OptionRow from '@components/OptionRow'; @@ -35,7 +35,7 @@ const defaultProps = { ...optionsListDefaultProps, }; -function BaseOptionsList({ +const BaseOptionsList = forwardRef(({ keyboardDismissMode, onScrollBeginDrag, onScroll, @@ -65,16 +65,18 @@ function BaseOptionsList({ onSelectRow, boldStyle, isDisabled, - innerRef, isRowMultilineSupported, isLoadingNewOptions, nestedScrollEnabled, bounces, -}) { + safeAreaPaddingBottomStyle, +}, innerRef) => { const flattenedData = useRef(); const previousSections = usePrevious(sections); const didLayout = useRef(false); + const listContentContainerStyle = useMemo(() => [contentContainerStyles, safeAreaPaddingBottomStyle], [contentContainerStyles, safeAreaPaddingBottomStyle]) + /** * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. * @@ -270,7 +272,7 @@ function BaseOptionsList({ scrollEnabled={nestedScrollEnabled} onScrollBeginDrag={onScrollBeginDrag} onScroll={onScroll} - contentContainerStyle={contentContainerStyles} + contentContainerStyle={listContentContainerStyle} showsVerticalScrollIndicator={showScrollIndicator} sections={sections} keyExtractor={extractKey} @@ -290,7 +292,7 @@ function BaseOptionsList({ )} ); -} +}); BaseOptionsList.propTypes = propTypes; BaseOptionsList.defaultProps = defaultProps; @@ -298,13 +300,7 @@ BaseOptionsList.displayName = 'BaseOptionsList'; // using memo to avoid unnecessary rerenders when parents component rerenders (thus causing this component to rerender because shallow comparison is used for some props). export default memo( - forwardRef((props, ref) => ( - - )), + BaseOptionsList, (prevProps, nextProps) => nextProps.focusedIndex === prevProps.focusedIndex && nextProps.selectedOptions.length === prevProps.selectedOptions.length && diff --git a/src/components/OptionsList/index.js b/src/components/OptionsList/index.js index 36b8e7fccf12..6046a6124ccc 100644 --- a/src/components/OptionsList/index.js +++ b/src/components/OptionsList/index.js @@ -1,4 +1,4 @@ -import React, {forwardRef, useCallback, useEffect, useRef} from 'react'; +import React, {forwardRef, memo, useCallback, useEffect, useRef} from 'react'; import {Keyboard} from 'react-native'; import _ from 'underscore'; import withWindowDimensions from '@components/withWindowDimensions'; @@ -64,4 +64,4 @@ const OptionsListWithRef = forwardRef((props, ref) => ( OptionsListWithRef.displayName = 'OptionsListWithRef'; -export default withWindowDimensions(OptionsListWithRef); +export default withWindowDimensions(memo(OptionsListWithRef)); diff --git a/src/components/OptionsList/index.native.js b/src/components/OptionsList/index.native.js index ab2db4f20967..8a70e1e060b1 100644 --- a/src/components/OptionsList/index.native.js +++ b/src/components/OptionsList/index.native.js @@ -1,4 +1,4 @@ -import React, {forwardRef} from 'react'; +import React, {forwardRef, memo} from 'react'; import {Keyboard} from 'react-native'; import BaseOptionsList from './BaseOptionsList'; import {defaultProps, propTypes} from './optionsListPropTypes'; @@ -8,7 +8,7 @@ const OptionsList = forwardRef((props, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} - onScrollBeginDrag={() => Keyboard.dismiss()} + onScrollBeginDrag={Keyboard.dismiss} /> )); @@ -16,4 +16,4 @@ OptionsList.propTypes = propTypes; OptionsList.defaultProps = defaultProps; OptionsList.displayName = 'OptionsList'; -export default OptionsList; +export default memo(OptionsList); From bbb216dd728eb135709a7892b793247c5cdb820d Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 15 Nov 2023 15:07:28 +0500 Subject: [PATCH 003/120] perf: add navigation listeners and remove inline functions --- .../OptionsSelector/BaseOptionsSelector.js | 109 ++++++++++-------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 8c480c27f20f..cdf2b83b6215 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {Component} from 'react'; -import {ScrollView, View} from 'react-native'; +import {InteractionManager, ScrollView, View} from 'react-native'; import _ from 'underscore'; import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; @@ -10,7 +10,7 @@ import FormHelpMessage from '@components/FormHelpMessage'; import OptionsList from '@components/OptionsList'; import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withNavigationFocus from '@components/withNavigationFocus'; +import withNavigation from '@components/withNavigation'; import compose from '@libs/compose'; import getPlatform from '@libs/getPlatform'; import KeyboardShortcut from '@libs/KeyboardShortcut'; @@ -32,9 +32,6 @@ const propTypes = { /** List styles for OptionsList */ listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - /** Whether navigation is focused */ - isFocused: PropTypes.bool.isRequired, - ...optionsSelectorPropTypes, ...withLocalizePropTypes, }; @@ -58,49 +55,59 @@ class BaseOptionsSelector extends Component { this.selectFocusedOption = this.selectFocusedOption.bind(this); this.addToSelection = this.addToSelection.bind(this); this.updateSearchValue = this.updateSearchValue.bind(this); + this.onLayout = this.onLayout.bind(this); + this.setListRef = this.setListRef.bind(this); this.relatedTarget = null; - const allOptions = this.flattenSections(); - const focusedIndex = this.getInitiallyFocusedIndex(allOptions); - + this.focusListener = null; + this.blurListener = null; + this.isFocused = false; this.state = { - allOptions, - focusedIndex, + allOptions: [], + focusedIndex: 0, shouldDisableRowSelection: false, errorMessage: '', }; } componentDidMount() { - this.subscribeToKeyboardShortcut(); + this.focusListener = this.props.navigation.addListener('focus', () => { + this.subscribeToKeyboardShortcut(); + + // Screen coming back into focus, for example + // when doing Cmd+Shift+K, then Cmd+K, then Cmd+Shift+K. + // Only applies to platforms that support keyboard shortcuts + if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform()) && this.props.autoFocus && this.textInput) { + this.focusTimeout = setTimeout(() => { + this.textInput.focus(); + }, CONST.ANIMATED_TRANSITION); + } - if (this.props.isFocused && this.props.autoFocus && this.textInput) { - this.focusTimeout = setTimeout(() => { - this.textInput.focus(); - }, CONST.ANIMATED_TRANSITION); - } + this.isFocused = true; + }); + this.blurListener = this.props.navigation.addListener('blur', () => { + this.unSubscribeFromKeyboardShortcut(); + this.isFocused = false; + }); this.scrollToIndex(this.props.selectedOptions.length ? 0 : this.state.focusedIndex, false); + + /** + * Execute the following code after all interactions have been completed. + * Which means once we are sure that all navigation animations are done, + * we will execute the callback passed to `runAfterInteractions`. + */ + this.interactionTask = InteractionManager.runAfterInteractions(() => { + const allOptions = this.flattenSections(); + const focusedIndex = this.getInitiallyFocusedIndex(allOptions); + this.setState({ + allOptions, + focusedIndex, + }); + }); } componentDidUpdate(prevProps) { - if (prevProps.isFocused !== this.props.isFocused) { - if (this.props.isFocused) { - this.subscribeToKeyboardShortcut(); - } else { - this.unSubscribeFromKeyboardShortcut(); - } - } - - // Screen coming back into focus, for example - // when doing Cmd+Shift+K, then Cmd+K, then Cmd+Shift+K. - // Only applies to platforms that support keyboard shortcuts - if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform()) && !prevProps.isFocused && this.props.isFocused && this.props.autoFocus && this.textInput) { - setTimeout(() => { - this.textInput.focus(); - }, CONST.ANIMATED_TRANSITION); - } - if (_.isEqual(this.props.sections, prevProps.sections)) { return; } @@ -139,11 +146,22 @@ class BaseOptionsSelector extends Component { } componentWillUnmount() { + this.interactionTask.cancel(); + this.focusListener(); + this.blurListener(); if (this.focusTimeout) { clearTimeout(this.focusTimeout); } + } - this.unSubscribeFromKeyboardShortcut(); + onLayout() { + if (this.props.selectedOptions.length === 0) { + this.scrollToIndex(this.state.focusedIndex, false); + } + + if (this.props.onLayout) { + this.props.onLayout(); + } } /** @@ -172,6 +190,10 @@ class BaseOptionsSelector extends Component { return defaultIndex; } + setListRef(ref) { + this.list = ref; + } + updateSearchValue(value) { this.setState({ errorMessage: value.length > this.props.maxLength ? this.props.translate('common.error.characterLimitExceedCounter', {length: value.length, limit: this.props.maxLength}) : '', @@ -226,7 +248,7 @@ class BaseOptionsSelector extends Component { selectFocusedOption() { const focusedOption = this.state.allOptions[this.state.focusedIndex]; - if (!focusedOption || !this.props.isFocused) { + if (!focusedOption || !this.isFocused) { return; } @@ -400,7 +422,7 @@ class BaseOptionsSelector extends Component { ); const optionsList = ( (this.list = el)} + ref={this.setListRef} optionHoveredStyle={this.props.optionHoveredStyle} onSelectRow={this.props.onSelectRow ? this.selectRow : undefined} sections={this.props.sections} @@ -417,16 +439,9 @@ class BaseOptionsSelector extends Component { isDisabled={this.props.isDisabled} shouldHaveOptionSeparator={this.props.shouldHaveOptionSeparator} highlightSelectedOptions={this.props.highlightSelectedOptions} - onLayout={() => { - if (this.props.selectedOptions.length === 0) { - this.scrollToIndex(this.state.focusedIndex, false); - } - - if (this.props.onLayout) { - this.props.onLayout(); - } - }} - contentContainerStyles={[safeAreaPaddingBottomStyle, ...this.props.contentContainerStyles]} + onLayout={this.onLayout} + safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} + contentContainerStyles={this.props.contentContainerStyles} sectionHeaderStyle={this.props.sectionHeaderStyle} listContainerStyles={this.props.listContainerStyles} listStyles={this.props.listStyles} @@ -518,4 +533,4 @@ class BaseOptionsSelector extends Component { BaseOptionsSelector.defaultProps = defaultProps; BaseOptionsSelector.propTypes = propTypes; -export default compose(withLocalize, withNavigationFocus)(BaseOptionsSelector); +export default compose(withLocalize, withNavigation)(BaseOptionsSelector); From 15147c6e7c6a09ca0e54a3c3351cfbebe9e18d6c Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 15 Nov 2023 15:09:45 +0500 Subject: [PATCH 004/120] refactor: use personalDetails from utils and add Interaction Manager --- src/libs/PersonalDetailsUtils.js | 11 ++++++- src/pages/SearchPage.js | 49 ++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index c99adc32a56a..3a1038700537 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -177,4 +177,13 @@ function getFormattedAddress(privatePersonalDetails) { return formattedAddress.trim().replace(/,$/, ''); } -export {getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, getLoginsByAccountIDs, getNewPersonalDetailsOnyxData, getFormattedAddress}; +/** + * get personal details + * + * @returns {Object} + */ +function getPersonalDetails() { + return allPersonalDetails || {}; +} + +export {getPersonalDetails, getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, getLoginsByAccountIDs, getNewPersonalDetailsOnyxData, getFormattedAddress}; diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 3e7731efc7b2..7d9f9818c309 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, {Component} from 'react'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -14,13 +14,13 @@ import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import styles from '@styles/styles'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import personalDetailsPropType from './personalDetailsPropType'; import reportPropTypes from './reportPropTypes'; const propTypes = { @@ -29,9 +29,6 @@ const propTypes = { /** Beta features list */ betas: PropTypes.arrayOf(PropTypes.string), - /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), @@ -49,7 +46,6 @@ const propTypes = { const defaultProps = { betas: [], - personalDetails: {}, reports: {}, network: {}, isSearchingForReports: false, @@ -76,12 +72,16 @@ class SearchPage extends Component { } componentDidUpdate(prevProps) { - if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { + if (_.isEqual(prevProps.reports, this.props.reports)) { return; } this.updateOptions(); } + componentWillUnmount() { + this.interactionTask.cancel(); + } + onChangeText(searchValue = '') { if (searchValue.length) { Report.searchInServer(searchValue); @@ -134,16 +134,26 @@ class SearchPage extends Component { } updateOptions() { - const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions( - this.props.reports, - this.props.personalDetails, - this.state.searchValue.trim(), - this.props.betas, - ); - this.setState({ - userToInvite, - recentReports, - personalDetails, + if (this.interactionTask) { + this.interactionTask.cancel(); + } + + /** + * Execute the callback after all interactions are done, which means + * after all animations have finished. + */ + this.interactionTask = InteractionManager.runAfterInteractions(() => { + const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions( + this.props.reports, + PersonalDetailsUtils.getPersonalDetails(), + this.state.searchValue.trim(), + this.props.betas, + ); + this.setState({ + userToInvite, + recentReports, + personalDetails, + }); }); } @@ -173,7 +183,7 @@ class SearchPage extends Component { render() { const sections = this.getSections(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.state.personalDetails); const headerMessage = OptionsListUtils.getHeaderMessage( this.state.recentReports.length + this.state.personalDetails.length !== 0, Boolean(this.state.userToInvite), @@ -228,9 +238,6 @@ export default compose( reports: { key: ONYXKEYS.COLLECTION.REPORT, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, betas: { key: ONYXKEYS.BETAS, }, From 15e62f92aa9eb07d2ed3ac9ff9b71717395fca47 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 15 Nov 2023 17:31:22 +0500 Subject: [PATCH 005/120] fix: linting --- src/components/OptionsList/BaseOptionsList.js | 489 +++++++++--------- 1 file changed, 247 insertions(+), 242 deletions(-) diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index cecf983ff989..d303c6f58073 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -35,264 +35,269 @@ const defaultProps = { ...optionsListDefaultProps, }; -const BaseOptionsList = forwardRef(({ - keyboardDismissMode, - onScrollBeginDrag, - onScroll, - listStyles, - focusedIndex, - selectedOptions, - headerMessage, - isLoading, - sections, - onLayout, - hideSectionHeaders, - shouldHaveOptionSeparator, - showTitleTooltip, - optionHoveredStyle, - contentContainerStyles, - sectionHeaderStyle, - showScrollIndicator, - listContainerStyles, - shouldDisableRowInnerPadding, - shouldPreventDefaultFocusOnSelectRow, - disableFocusOptions, - canSelectMultipleOptions, - shouldShowMultipleOptionSelectorAsButton, - multipleOptionSelectorButtonText, - onAddToSelection, - highlightSelectedOptions, - onSelectRow, - boldStyle, - isDisabled, - isRowMultilineSupported, - isLoadingNewOptions, - nestedScrollEnabled, - bounces, - safeAreaPaddingBottomStyle, -}, innerRef) => { - const flattenedData = useRef(); - const previousSections = usePrevious(sections); - const didLayout = useRef(false); - - const listContentContainerStyle = useMemo(() => [contentContainerStyles, safeAreaPaddingBottomStyle], [contentContainerStyles, safeAreaPaddingBottomStyle]) - - /** - * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. - * - * @returns {Array} - */ - const buildFlatSectionArray = () => { - let offset = 0; - - // Start with just an empty list header - const flatArray = [{length: 0, offset}]; - - // Build the flat array - for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { - const section = sections[sectionIndex]; - - // Add the section header - const sectionHeaderHeight = section.title && !hideSectionHeaders ? variables.optionsListSectionHeaderHeight : 0; - flatArray.push({length: sectionHeaderHeight, offset}); - offset += sectionHeaderHeight; - - // Add section items - for (let i = 0; i < section.data.length; i++) { - let fullOptionHeight = variables.optionRowHeight; - if (i > 0 && shouldHaveOptionSeparator) { - fullOptionHeight += variables.borderTopWidth; +const BaseOptionsList = forwardRef( + ( + { + keyboardDismissMode, + onScrollBeginDrag, + onScroll, + listStyles, + focusedIndex, + selectedOptions, + headerMessage, + isLoading, + sections, + onLayout, + hideSectionHeaders, + shouldHaveOptionSeparator, + showTitleTooltip, + optionHoveredStyle, + contentContainerStyles, + sectionHeaderStyle, + showScrollIndicator, + listContainerStyles, + shouldDisableRowInnerPadding, + shouldPreventDefaultFocusOnSelectRow, + disableFocusOptions, + canSelectMultipleOptions, + shouldShowMultipleOptionSelectorAsButton, + multipleOptionSelectorButtonText, + onAddToSelection, + highlightSelectedOptions, + onSelectRow, + boldStyle, + isDisabled, + isRowMultilineSupported, + isLoadingNewOptions, + nestedScrollEnabled, + bounces, + safeAreaPaddingBottomStyle, + }, + innerRef, + ) => { + const flattenedData = useRef(); + const previousSections = usePrevious(sections); + const didLayout = useRef(false); + + const listContentContainerStyle = useMemo(() => [contentContainerStyles, safeAreaPaddingBottomStyle], [contentContainerStyles, safeAreaPaddingBottomStyle]); + + /** + * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. + * + * @returns {Array} + */ + const buildFlatSectionArray = () => { + let offset = 0; + + // Start with just an empty list header + const flatArray = [{length: 0, offset}]; + + // Build the flat array + for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { + const section = sections[sectionIndex]; + + // Add the section header + const sectionHeaderHeight = section.title && !hideSectionHeaders ? variables.optionsListSectionHeaderHeight : 0; + flatArray.push({length: sectionHeaderHeight, offset}); + offset += sectionHeaderHeight; + + // Add section items + for (let i = 0; i < section.data.length; i++) { + let fullOptionHeight = variables.optionRowHeight; + if (i > 0 && shouldHaveOptionSeparator) { + fullOptionHeight += variables.borderTopWidth; + } + flatArray.push({length: fullOptionHeight, offset}); + offset += fullOptionHeight; } - flatArray.push({length: fullOptionHeight, offset}); - offset += fullOptionHeight; + + // Add the section footer + flatArray.push({length: 0, offset}); } - // Add the section footer + // Then add the list footer flatArray.push({length: 0, offset}); - } - - // Then add the list footer - flatArray.push({length: 0, offset}); - return flatArray; - }; - - useEffect(() => { - if (_.isEqual(sections, previousSections)) { - return; - } - flattenedData.current = buildFlatSectionArray(); - }); - - const onViewableItemsChanged = () => { - if (didLayout.current || !onLayout) { - return; - } - - didLayout.current = true; - onLayout(); - }; - - /** - * This function is used to compute the layout of any given item in our list. - * We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList. - * - * @param {Array} data - This is the same as the data we pass into the component - * @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: - * - * 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those. - * 2. Each section includes a header, even if we don't provide/render one. - * - * For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this: - * - * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] - * - * @returns {Object} - */ - const getItemLayout = (data, flatDataArrayIndex) => { - if (!_.has(flattenedData.current, flatDataArrayIndex)) { + return flatArray; + }; + + useEffect(() => { + if (_.isEqual(sections, previousSections)) { + return; + } flattenedData.current = buildFlatSectionArray(); - } + }); + + const onViewableItemsChanged = () => { + if (didLayout.current || !onLayout) { + return; + } - const targetItem = flattenedData.current[flatDataArrayIndex]; - return { - length: targetItem.length, - offset: targetItem.offset, - index: flatDataArrayIndex, + didLayout.current = true; + onLayout(); }; - }; - - /** - * Returns the key used by the list - * @param {Object} option - * @return {String} - */ - const extractKey = (option) => option.keyForList; - - /** - * Function which renders a row in the list - * - * @param {Object} params - * @param {Object} params.item - * @param {Number} params.index - * @param {Object} params.section - * - * @return {Component} - */ - const renderItem = ({item, index, section}) => { - const isItemDisabled = isDisabled || section.isDisabled || !!item.isDisabled; - const isSelected = _.some(selectedOptions, (option) => { - if (option.accountID && option.accountID === item.accountID) { - return true; + + /** + * This function is used to compute the layout of any given item in our list. + * We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList. + * + * @param {Array} data - This is the same as the data we pass into the component + * @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: + * + * 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those. + * 2. Each section includes a header, even if we don't provide/render one. + * + * For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this: + * + * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] + * + * @returns {Object} + */ + const getItemLayout = (data, flatDataArrayIndex) => { + if (!_.has(flattenedData.current, flatDataArrayIndex)) { + flattenedData.current = buildFlatSectionArray(); } - if (option.reportID && option.reportID === item.reportID) { - return true; + const targetItem = flattenedData.current[flatDataArrayIndex]; + return { + length: targetItem.length, + offset: targetItem.offset, + index: flatDataArrayIndex, + }; + }; + + /** + * Returns the key used by the list + * @param {Object} option + * @return {String} + */ + const extractKey = (option) => option.keyForList; + + /** + * Function which renders a row in the list + * + * @param {Object} params + * @param {Object} params.item + * @param {Number} params.index + * @param {Object} params.section + * + * @return {Component} + */ + const renderItem = ({item, index, section}) => { + const isItemDisabled = isDisabled || section.isDisabled || !!item.isDisabled; + const isSelected = _.some(selectedOptions, (option) => { + if (option.accountID && option.accountID === item.accountID) { + return true; + } + + if (option.reportID && option.reportID === item.reportID) { + return true; + } + + if (_.isEmpty(option.name)) { + return false; + } + + return option.name === item.searchText; + }); + + return ( + 0 && shouldHaveOptionSeparator} + shouldDisableRowInnerPadding={shouldDisableRowInnerPadding} + shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} + isMultilineSupported={isRowMultilineSupported} + /> + ); + }; + + /** + * Function which renders a section header component + * + * @param {Object} params + * @param {Object} params.section + * @param {String} params.section.title + * @param {Boolean} params.section.shouldShow + * + * @return {Component} + */ + const renderSectionHeader = ({section: {title, shouldShow}}) => { + if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) { + return ; } - if (_.isEmpty(option.name)) { - return false; + if (title && shouldShow && !hideSectionHeaders) { + return ( + // Note: The `optionsListSectionHeader` style provides an explicit height to section headers. + // We do this so that we can reference the height in `getItemLayout` – + // we need to know the heights of all list items up-front in order to synchronously compute the layout of any given list item. + // So be aware that if you adjust the content of the section header (for example, change the font size), you may need to adjust this explicit height as well. + + {title} + + ); } - return option.name === item.searchText; - }); + return ; + }; return ( - 0 && shouldHaveOptionSeparator} - shouldDisableRowInnerPadding={shouldDisableRowInnerPadding} - shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} - isMultilineSupported={isRowMultilineSupported} - /> + + {isLoading ? ( + + ) : ( + <> + {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */} + {/* This is misleading because we might be in the process of loading fresh options from the server. */} + {!isLoadingNewOptions && headerMessage ? ( + + {headerMessage} + + ) : null} + + + )} + ); - }; - - /** - * Function which renders a section header component - * - * @param {Object} params - * @param {Object} params.section - * @param {String} params.section.title - * @param {Boolean} params.section.shouldShow - * - * @return {Component} - */ - const renderSectionHeader = ({section: {title, shouldShow}}) => { - if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) { - return ; - } - - if (title && shouldShow && !hideSectionHeaders) { - return ( - // Note: The `optionsListSectionHeader` style provides an explicit height to section headers. - // We do this so that we can reference the height in `getItemLayout` – - // we need to know the heights of all list items up-front in order to synchronously compute the layout of any given list item. - // So be aware that if you adjust the content of the section header (for example, change the font size), you may need to adjust this explicit height as well. - - {title} - - ); - } - - return ; - }; - - return ( - - {isLoading ? ( - - ) : ( - <> - {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */} - {/* This is misleading because we might be in the process of loading fresh options from the server. */} - {!isLoadingNewOptions && headerMessage ? ( - - {headerMessage} - - ) : null} - - - )} - - ); -}); + }, +); BaseOptionsList.propTypes = propTypes; BaseOptionsList.defaultProps = defaultProps; From aab633b9bb74d783f42d96c30e2d0242567e5f01 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 16 Nov 2023 12:14:34 +0500 Subject: [PATCH 006/120] refactor: focus text input --- .../OptionsSelector/BaseOptionsSelector.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index cdf2b83b6215..682743ec7f01 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -72,12 +72,11 @@ class BaseOptionsSelector extends Component { componentDidMount() { this.focusListener = this.props.navigation.addListener('focus', () => { - this.subscribeToKeyboardShortcut(); + if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform())) { + this.subscribeToKeyboardShortcut(); + } - // Screen coming back into focus, for example - // when doing Cmd+Shift+K, then Cmd+K, then Cmd+Shift+K. - // Only applies to platforms that support keyboard shortcuts - if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform()) && this.props.autoFocus && this.textInput) { + if (this.props.autoFocus && this.textInput) { this.focusTimeout = setTimeout(() => { this.textInput.focus(); }, CONST.ANIMATED_TRANSITION); @@ -87,7 +86,9 @@ class BaseOptionsSelector extends Component { }); this.blurListener = this.props.navigation.addListener('blur', () => { - this.unSubscribeFromKeyboardShortcut(); + if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform())) { + this.unSubscribeFromKeyboardShortcut(); + } this.isFocused = false; }); this.scrollToIndex(this.props.selectedOptions.length ? 0 : this.state.focusedIndex, false); From 0e2833010b3be51885059123e6275d6341b3d4d8 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 28 Nov 2023 13:07:10 +0500 Subject: [PATCH 007/120] fix: skeleton being shown when typing --- src/pages/SearchPage.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index a6323729a86d..6759b08060d7 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -52,6 +52,18 @@ const defaultProps = { isSearchingForReports: false, }; +function isSectionsEmpty(sections) { + if (!sections.length) { + return true; + } + + if (!sections[0].data.length) { + return true; + } + + return _.isEmpty(sections[0].data[0]); +} + class SearchPage extends Component { constructor(props) { super(props); @@ -184,7 +196,7 @@ class SearchPage extends Component { render() { const sections = this.getSections(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.state.personalDetails); + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(PersonalDetailsUtils.getPersonalDetails()); const headerMessage = OptionsListUtils.getHeaderMessage( this.state.recentReports.length + this.state.personalDetails.length !== 0, Boolean(this.state.userToInvite), @@ -209,7 +221,7 @@ class SearchPage extends Component { headerMessage={headerMessage} hideSectionHeaders showTitleTooltip - shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady} + shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady && !isSectionsEmpty(sections)} textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={ this.props.network.isOffline ? `${this.props.translate('common.youAppearToBeOffline')} ${this.props.translate('search.resultsAreLimited')}` : '' From e951bb7747210447028f350e5935f0bedb6e83e6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 28 Nov 2023 17:24:39 +0100 Subject: [PATCH 008/120] [TS migration] Migrate 'TransactionEdit.js' lib --- src/ONYXKEYS.ts | 1 + .../{TransactionEdit.js => TransactionEdit.ts} | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) rename src/libs/actions/{TransactionEdit.js => TransactionEdit.ts} (76%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5576eb64736d..0cb3f67bd990 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -446,6 +446,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean; [ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup; [ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction; + [ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; diff --git a/src/libs/actions/TransactionEdit.js b/src/libs/actions/TransactionEdit.ts similarity index 76% rename from src/libs/actions/TransactionEdit.js rename to src/libs/actions/TransactionEdit.ts index 2cb79ac387bd..387dacddbcdc 100644 --- a/src/libs/actions/TransactionEdit.js +++ b/src/libs/actions/TransactionEdit.ts @@ -1,28 +1,31 @@ -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxEntry} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import {Transaction} from '@src/types/onyx'; /** * Makes a backup copy of a transaction object that can be restored when the user cancels editing a transaction. - * - * @param {Object} transaction */ -function createBackupTransaction(transaction) { +function createBackupTransaction(transaction: OnyxEntry) { + if (!transaction) { + return; + } + const newTransaction = { ...transaction, }; + // Use set so that it will always fully overwrite any backup transaction that could have existed before Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction); } /** * Removes a transaction from Onyx that was only used temporary in the edit flow - * @param {String} transactionID */ -function removeBackupTransaction(transactionID) { +function removeBackupTransaction(transactionID: string) { Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, null); } -function restoreOriginalTransactionFromBackup(transactionID) { +function restoreOriginalTransactionFromBackup(transactionID: string) { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, callback: (backupTransaction) => { From 76c1781273e5dd7829d8aa92e87de8e7a0503d4b Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 28 Nov 2023 17:58:49 +0100 Subject: [PATCH 009/120] [TS migration] Migrate 'MemoryOnlyKeys' lib --- .../{MemoryOnlyKeys.js => MemoryOnlyKeys.ts} | 3 ++- .../exposeGlobalMemoryOnlyKeysMethods/index.js | 12 ------------ .../index.native.js | 6 ------ .../index.native.ts | 8 ++++++++ .../exposeGlobalMemoryOnlyKeysMethods/index.ts | 18 ++++++++++++++++++ .../exposeGlobalMemoryOnlyKeysMethods/types.ts | 3 +++ 6 files changed, 31 insertions(+), 19 deletions(-) rename src/libs/actions/MemoryOnlyKeys/{MemoryOnlyKeys.js => MemoryOnlyKeys.ts} (72%) delete mode 100644 src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.js delete mode 100644 src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.js create mode 100644 src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.ts create mode 100644 src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts create mode 100644 src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/types.ts diff --git a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.js b/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts similarity index 72% rename from src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.js rename to src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts index 028bce225909..79d1ec0f82d9 100644 --- a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.js +++ b/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts @@ -1,8 +1,9 @@ import Onyx from 'react-native-onyx'; +import {OnyxKey} from 'react-native-onyx/lib/types'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; -const memoryOnlyKeys = [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.POLICY, ONYXKEYS.PERSONAL_DETAILS_LIST]; +const memoryOnlyKeys: OnyxKey[] = [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.POLICY, ONYXKEYS.PERSONAL_DETAILS_LIST]; const enable = () => { Log.info('[MemoryOnlyKeys] enabled'); diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.js b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.js deleted file mode 100644 index 1d039c8980a9..000000000000 --- a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; - -const exposeGlobalMemoryOnlyKeysMethods = () => { - window.enableMemoryOnlyKeys = () => { - MemoryOnlyKeys.enable(); - }; - window.disableMemoryOnlyKeys = () => { - MemoryOnlyKeys.disable(); - }; -}; - -export default exposeGlobalMemoryOnlyKeysMethods; diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.js b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.js deleted file mode 100644 index 9d08b9db6aa4..000000000000 --- a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * This is a no-op because the global methods will only work for web and desktop - */ -const exposeGlobalMemoryOnlyKeysMethods = () => {}; - -export default exposeGlobalMemoryOnlyKeysMethods; diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.ts b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.ts new file mode 100644 index 000000000000..b89e03bdefdc --- /dev/null +++ b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.ts @@ -0,0 +1,8 @@ +import type ExposeGlobalMemoryOnlyKeysMethods from './types'; + +/** + * This is a no-op because the global methods will only work for web and desktop + */ +const exposeGlobalMemoryOnlyKeysMethods: ExposeGlobalMemoryOnlyKeysMethods = () => {}; + +export default exposeGlobalMemoryOnlyKeysMethods; diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts new file mode 100644 index 000000000000..6d72188803d7 --- /dev/null +++ b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts @@ -0,0 +1,18 @@ +import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; +import type ExposeGlobalMemoryOnlyKeysMethods from './types'; + +type WindowWithMemoryOnlyKeys = Window & { + enableMemoryOnlyKeys?: () => void; + disableMemoryOnlyKeys?: () => void; +}; + +const exposeGlobalMemoryOnlyKeysMethods: ExposeGlobalMemoryOnlyKeysMethods = () => { + (window as WindowWithMemoryOnlyKeys).enableMemoryOnlyKeys = () => { + MemoryOnlyKeys.enable(); + }; + (window as WindowWithMemoryOnlyKeys).disableMemoryOnlyKeys = () => { + MemoryOnlyKeys.disable(); + }; +}; + +export default exposeGlobalMemoryOnlyKeysMethods; diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/types.ts b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/types.ts new file mode 100644 index 000000000000..4cb50041b627 --- /dev/null +++ b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/types.ts @@ -0,0 +1,3 @@ +type ExposeGlobalMemoryOnlyKeysMethods = () => void; + +export default ExposeGlobalMemoryOnlyKeysMethods; From cf2d8e60ac26d35726312388187a4b9f495c3015 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 28 Nov 2023 18:10:36 +0100 Subject: [PATCH 010/120] [TS migration] Migrate 'CanvasSize.js' lib --- src/libs/actions/{CanvasSize.js => CanvasSize.ts} | 6 +++--- src/types/modules/canvas-size.d.ts | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) rename src/libs/actions/{CanvasSize.js => CanvasSize.ts} (89%) create mode 100644 src/types/modules/canvas-size.d.ts diff --git a/src/libs/actions/CanvasSize.js b/src/libs/actions/CanvasSize.ts similarity index 89% rename from src/libs/actions/CanvasSize.js rename to src/libs/actions/CanvasSize.ts index b313763131b9..9de851aacae3 100644 --- a/src/libs/actions/CanvasSize.js +++ b/src/libs/actions/CanvasSize.ts @@ -16,7 +16,7 @@ function retrieveMaxCanvasArea() { useWorker: false, }) .then(() => ({ - onSuccess: (width, height) => { + onSuccess: (width: number, height: number) => { Onyx.merge(ONYXKEYS.MAX_CANVAS_AREA, width * height); }, })); @@ -27,7 +27,7 @@ function retrieveMaxCanvasArea() { */ function retrieveMaxCanvasHeight() { canvasSize.maxHeight({ - onSuccess: (width, height) => { + onSuccess: (width: number, height: number) => { Onyx.merge(ONYXKEYS.MAX_CANVAS_HEIGHT, height); }, }); @@ -38,7 +38,7 @@ function retrieveMaxCanvasHeight() { */ function retrieveMaxCanvasWidth() { canvasSize.maxWidth({ - onSuccess: (width) => { + onSuccess: (width: number) => { Onyx.merge(ONYXKEYS.MAX_CANVAS_WIDTH, width); }, }); diff --git a/src/types/modules/canvas-size.d.ts b/src/types/modules/canvas-size.d.ts new file mode 100644 index 000000000000..6e1243aa657a --- /dev/null +++ b/src/types/modules/canvas-size.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +declare module 'canvas-size' { + import canvasSize from 'canvas-size'; + + export default canvasSize; +} From 819b77ab8f52e06c259d63ffa2877128237638c2 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 28 Nov 2023 18:19:21 +0100 Subject: [PATCH 011/120] Add window.d.ts file --- .../exposeGlobalMemoryOnlyKeysMethods/index.ts | 9 ++------- src/types/modules/window.d.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 src/types/modules/window.d.ts diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts index 6d72188803d7..4514edacb288 100644 --- a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts +++ b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts @@ -1,16 +1,11 @@ import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; import type ExposeGlobalMemoryOnlyKeysMethods from './types'; -type WindowWithMemoryOnlyKeys = Window & { - enableMemoryOnlyKeys?: () => void; - disableMemoryOnlyKeys?: () => void; -}; - const exposeGlobalMemoryOnlyKeysMethods: ExposeGlobalMemoryOnlyKeysMethods = () => { - (window as WindowWithMemoryOnlyKeys).enableMemoryOnlyKeys = () => { + window.enableMemoryOnlyKeys = () => { MemoryOnlyKeys.enable(); }; - (window as WindowWithMemoryOnlyKeys).disableMemoryOnlyKeys = () => { + window.disableMemoryOnlyKeys = () => { MemoryOnlyKeys.disable(); }; }; diff --git a/src/types/modules/window.d.ts b/src/types/modules/window.d.ts new file mode 100644 index 000000000000..1910c26768f5 --- /dev/null +++ b/src/types/modules/window.d.ts @@ -0,0 +1,10 @@ +declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Window { + enableMemoryOnlyKeys: () => void; + disableMemoryOnlyKeys: () => void; + } +} + +// We used the export {} line to mark this file as an external module +export {}; From 2b59ded20f16607387c43b37bfa85230c6f04e31 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 09:25:51 +0100 Subject: [PATCH 012/120] [TS migration] Migrate 'Card.js' lib --- src/libs/actions/Card.js | 176 ------------------------------------- src/libs/actions/Card.ts | 184 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 176 deletions(-) delete mode 100644 src/libs/actions/Card.js create mode 100644 src/libs/actions/Card.ts diff --git a/src/libs/actions/Card.js b/src/libs/actions/Card.js deleted file mode 100644 index 9adcd3803766..000000000000 --- a/src/libs/actions/Card.js +++ /dev/null @@ -1,176 +0,0 @@ -import Onyx from 'react-native-onyx'; -import * as API from '@libs/API'; -import * as Localize from '@libs/Localize'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -/** - * @param {Number} cardID - */ -function reportVirtualExpensifyCardFraud(cardID) { - API.write( - 'ReportVirtualExpensifyCardFraud', - { - cardID, - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, - value: { - isLoading: true, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, - value: { - isLoading: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, - value: { - isLoading: false, - }, - }, - ], - }, - ); -} - -/** - * Call the API to deactivate the card and request a new one - * @param {String} cardId - id of the card that is going to be replaced - * @param {String} reason - reason for replacement ('damaged' | 'stolen') - */ -function requestReplacementExpensifyCard(cardId, reason) { - API.write( - 'RequestReplacementExpensifyCard', - { - cardId, - reason, - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { - isLoading: true, - errors: null, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { - isLoading: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { - isLoading: false, - }, - }, - ], - }, - ); -} - -/** - * Activates the physical Expensify card based on the last four digits of the card number - * - * @param {Number} cardLastFourDigits - * @param {Number} cardID - */ -function activatePhysicalExpensifyCard(cardLastFourDigits, cardID) { - API.write( - 'ActivatePhysicalExpensifyCard', - {cardLastFourDigits, cardID}, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CARD_LIST, - value: { - [cardID]: { - errors: null, - isLoading: true, - }, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CARD_LIST, - value: { - [cardID]: { - isLoading: false, - }, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CARD_LIST, - value: { - [cardID]: { - isLoading: false, - }, - }, - }, - ], - }, - ); -} - -/** - * Clears errors for a specific cardID - * - * @param {Number} cardID - */ -function clearCardListErrors(cardID) { - Onyx.merge(ONYXKEYS.CARD_LIST, {[cardID]: {errors: null, isLoading: false}}); -} - -/** - * Makes an API call to get virtual card details (pan, cvv, expiration date, address) - * This function purposefully uses `makeRequestWithSideEffects` method. For security reason - * card details cannot be persisted in Onyx and have to be asked for each time a user want's to - * reveal them. - * - * @param {String} cardID - virtual card ID - * - * @returns {Promise} - promise with card details object - */ -function revealVirtualCardDetails(cardID) { - return new Promise((resolve, reject) => { - // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('RevealExpensifyCardDetails', {cardID}) - .then((response) => { - if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { - reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); - return; - } - resolve(response); - }) - .catch(() => reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure'))); - }); -} - -export {requestReplacementExpensifyCard, activatePhysicalExpensifyCard, clearCardListErrors, reportVirtualExpensifyCardFraud, revealVirtualCardDetails}; diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts new file mode 100644 index 000000000000..8dd049db1f30 --- /dev/null +++ b/src/libs/actions/Card.ts @@ -0,0 +1,184 @@ +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import * as Localize from '@libs/Localize'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {Response} from '@src/types/onyx'; + +function reportVirtualExpensifyCardFraud(cardID: number) { + type ReportVirtualExpensifyCardFraudParams = { + cardID: number; + }; + + const reportVirtualExpensifyCardFraudParams: ReportVirtualExpensifyCardFraudParams = { + cardID, + }; + + API.write('ReportVirtualExpensifyCardFraud', reportVirtualExpensifyCardFraudParams, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: true, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: false, + }, + }, + ], + }); +} + +/** + * Call the API to deactivate the card and request a new one + * @param cardId - id of the card that is going to be replaced + * @param reason - reason for replacement ('damaged' | 'stolen') + */ +function requestReplacementExpensifyCard(cardId: number, reason: string) { + type RequestReplacementExpensifyCardParams = { + cardId: number; + reason: string; + }; + + const requestReplacementExpensifyCardParams: RequestReplacementExpensifyCardParams = { + cardId, + reason, + }; + + API.write('RequestReplacementExpensifyCard', requestReplacementExpensifyCardParams, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: true, + errors: null, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + }, + }, + ], + }); +} + +/** + * Activates the physical Expensify card based on the last four digits of the card number + */ +function activatePhysicalExpensifyCard(cardLastFourDigits: number, cardID: number) { + type ActivatePhysicalExpensifyCardParams = { + cardLastFourDigits: number; + cardID: number; + }; + + const activatePhysicalExpensifyCardParams: ActivatePhysicalExpensifyCardParams = { + cardLastFourDigits, + cardID, + }; + + API.write('ActivatePhysicalExpensifyCard', activatePhysicalExpensifyCardParams, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + errors: null, + isLoading: true, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + isLoading: false, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + isLoading: false, + }, + }, + }, + ], + }); +} + +/** + * Clears errors for a specific cardID + */ +function clearCardListErrors(cardID: number) { + Onyx.merge(ONYXKEYS.CARD_LIST, {[cardID]: {errors: null, isLoading: false}}); +} + +/** + * Makes an API call to get virtual card details (pan, cvv, expiration date, address) + * This function purposefully uses `makeRequestWithSideEffects` method. For security reason + * card details cannot be persisted in Onyx and have to be asked for each time a user want's to + * reveal them. + * + * @param cardID - virtual card ID + * + * @returns promise with card details object + */ +function revealVirtualCardDetails(cardID: number): Promise { + return new Promise((resolve, reject) => { + type RevealExpensifyCardDetailsParams = {cardID: number}; + + const revealExpensifyCardDetailsParams: RevealExpensifyCardDetailsParams = {cardID}; + + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects('RevealExpensifyCardDetails', revealExpensifyCardDetailsParams) + .then((response) => { + if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { + reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); + return; + } + resolve(response); + }) + .catch(() => reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure'))); + }); +} + +export {requestReplacementExpensifyCard, activatePhysicalExpensifyCard, clearCardListErrors, reportVirtualExpensifyCardFraud, revealVirtualCardDetails}; From 3e15c67d4feb074f8bd6f949c75cda9bae8dc6e9 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 09:31:46 +0100 Subject: [PATCH 013/120] TS update after main merging --- src/libs/actions/Card.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 8dd049db1f30..82137cc7c4cc 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -96,9 +96,9 @@ function requestReplacementExpensifyCard(cardId: number, reason: string) { /** * Activates the physical Expensify card based on the last four digits of the card number */ -function activatePhysicalExpensifyCard(cardLastFourDigits: number, cardID: number) { +function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: number) { type ActivatePhysicalExpensifyCardParams = { - cardLastFourDigits: number; + cardLastFourDigits: string; cardID: number; }; From ad02a8c6f431f5a75fcebe3e1b27a13e97000f13 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 09:55:10 +0100 Subject: [PATCH 014/120] [TS migration] Migrate 'OnyxUpdateManager.ts' lib --- src/libs/actions/App.ts | 4 ++-- ...xUpdateManager.js => OnyxUpdateManager.ts} | 24 +++++++++---------- src/libs/actions/OnyxUpdates.ts | 1 + 3 files changed, 15 insertions(+), 14 deletions(-) rename src/libs/actions/{OnyxUpdateManager.js => OnyxUpdateManager.ts} (85%) diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 4de8f1c1f171..ff4e798ba92a 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -293,12 +293,12 @@ function finalReconnectAppAfterActivatingReliableUpdates(): Promise { +function getMissingOnyxUpdates(updateIDFrom = 0, updateIDTo: number | string = 0): Promise { console.debug(`[OnyxUpdates] Fetching missing updates updateIDFrom: ${updateIDFrom} and updateIDTo: ${updateIDTo}`); type GetMissingOnyxMessagesParams = { updateIDFrom: number; - updateIDTo: number; + updateIDTo: number | string; }; const parameters: GetMissingOnyxMessagesParams = { diff --git a/src/libs/actions/OnyxUpdateManager.js b/src/libs/actions/OnyxUpdateManager.ts similarity index 85% rename from src/libs/actions/OnyxUpdateManager.js rename to src/libs/actions/OnyxUpdateManager.ts index 21cea452295b..b61c8eeae268 100644 --- a/src/libs/actions/OnyxUpdateManager.js +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -1,5 +1,4 @@ import Onyx from 'react-native-onyx'; -import _ from 'underscore'; import Log from '@libs/Log'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; import CONST from '@src/CONST'; @@ -22,27 +21,28 @@ import * as OnyxUpdates from './OnyxUpdates'; // The circular dependency happens because this file calls API.GetMissingOnyxUpdates() which uses the SaveResponseInOnyx.js file // (as a middleware). Therefore, SaveResponseInOnyx.js can't import and use this file directly. -let lastUpdateIDAppliedToClient = 0; +let lastUpdateIDAppliedToClient: number | null = 0; Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, - callback: (val) => (lastUpdateIDAppliedToClient = val), + callback: (value) => (lastUpdateIDAppliedToClient = value), }); export default () => { console.debug('[OnyxUpdateManager] Listening for updates from the server'); Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_FROM_SERVER, - callback: (val) => { - if (!val) { + callback: (value) => { + if (!value) { return; } // Since we used the same key that used to store another object, let's confirm that the current object is // following the new format before we proceed. If it isn't, then let's clear the object in Onyx. if ( - !_.isObject(val) || - !_.has(val, 'type') || - (!(val.type === CONST.ONYX_UPDATE_TYPES.HTTPS && _.has(val, 'request') && _.has(val, 'response')) && !(val.type === CONST.ONYX_UPDATE_TYPES.PUSHER && _.has(val, 'updates'))) + value === null || + !Object.hasOwn(value, 'type') || + (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && Object.hasOwn(value, 'request') && Object.hasOwn(value, 'response')) && + !(value.type === CONST.ONYX_UPDATE_TYPES.PUSHER && Object.hasOwn(value, 'updates'))) ) { console.debug('[OnyxUpdateManager] Invalid format found for updates, cleaning and unpausing the queue'); Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); @@ -50,9 +50,9 @@ export default () => { return; } - const updateParams = val; - const lastUpdateIDFromServer = val.lastUpdateID; - const previousUpdateIDFromServer = val.previousUpdateID; + const updateParams = value; + const lastUpdateIDFromServer = value.lastUpdateID; + const previousUpdateIDFromServer = value.previousUpdateID; // In cases where we received a previousUpdateID and it doesn't match our lastUpdateIDAppliedToClient // we need to perform one of the 2 possible cases: @@ -76,7 +76,7 @@ export default () => { canUnpauseQueuePromise = App.finalReconnectAppAfterActivatingReliableUpdates(); } else { // The flow below is setting the promise to a getMissingOnyxUpdates to address flow (2) explained above. - console.debug(`[OnyxUpdateManager] Client is behind the server by ${previousUpdateIDFromServer - lastUpdateIDAppliedToClient} so fetching incremental updates`); + console.debug(`[OnyxUpdateManager] Client is behind the server by ${Number(previousUpdateIDFromServer) - lastUpdateIDAppliedToClient} so fetching incremental updates`); Log.info('Gap detected in update IDs from server so fetching incremental updates', true, { lastUpdateIDFromServer, previousUpdateIDFromServer, diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index ce673fa6aaaf..af3a16cd3b54 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -68,6 +68,7 @@ function applyPusherOnyxUpdates(updates: OnyxUpdateEvent[]) { */ function apply({lastUpdateID, type, request, response, updates}: Merge): Promise; function apply({lastUpdateID, type, request, response, updates}: Merge): Promise; +function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFromServer): Promise; function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFromServer): Promise | undefined { console.debug(`[OnyxUpdateManager] Applying update type: ${type} with lastUpdateID: ${lastUpdateID}`, {request, response, updates}); From 21eac298f59a709433b1110633f796d4dd848528 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 10:06:43 +0100 Subject: [PATCH 015/120] [TS migration] Migrate 'DemoActions.js' lib --- src/libs/Navigation/Navigation.ts | 4 ++-- .../{DemoActions.js => DemoActions.ts} | 24 ++++++++++++------- src/types/onyx/Response.ts | 1 + 3 files changed, 18 insertions(+), 11 deletions(-) rename src/libs/actions/{DemoActions.js => DemoActions.ts} (80%) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index c2dd3e76e7ad..e90c092327fd 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -69,7 +69,7 @@ function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number * @param path - Path that you are looking for. * @return - Returns distance to path or -1 if the path is not found in root navigator. */ -function getDistanceFromPathInRootNavigator(path: string): number { +function getDistanceFromPathInRootNavigator(path?: string): number { let currentState = navigationRef.getRootState(); for (let index = 0; index < 5; index++) { @@ -138,7 +138,7 @@ function navigate(route: Route = ROUTES.HOME, type?: string) { * @param shouldEnforceFallback - Enforces navigation to fallback route * @param shouldPopToTop - Should we navigate to LHN on back press */ -function goBack(fallbackRoute: Route, shouldEnforceFallback = false, shouldPopToTop = false) { +function goBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopToTop = false) { if (!canNavigate('goBack')) { return; } diff --git a/src/libs/actions/DemoActions.js b/src/libs/actions/DemoActions.ts similarity index 80% rename from src/libs/actions/DemoActions.js rename to src/libs/actions/DemoActions.ts index 245e475e7ca9..41f5a54977cb 100644 --- a/src/libs/actions/DemoActions.js +++ b/src/libs/actions/DemoActions.ts @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import Config from 'react-native-config'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; @@ -7,17 +6,17 @@ import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -let currentUserEmail; +let currentUserEmail: string; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { - currentUserEmail = lodashGet(val, 'email', ''); + currentUserEmail = val?.email ?? ''; }, }); function runMoney2020Demo() { // Try to navigate to existing demo chat if it exists in Onyx - const money2020AccountID = Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_MONEY2020', 15864555)); + const money2020AccountID = Number(Config.EXPENSIFY_ACCOUNT_ID_MONEY2020 ?? 15864555); const existingChatReport = ReportUtils.getChatByParticipants([money2020AccountID]); if (existingChatReport) { // We must call goBack() to remove the demo route from nav history @@ -26,12 +25,19 @@ function runMoney2020Demo() { return; } - // We use makeRequestWithSideEffects here because we need to get the chat report ID to navigate to it after it's created - // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('CreateChatReport', { + type CreateChatReportParams = { + emailList: string; + activationConference: string; + }; + + const createChatReportParams: CreateChatReportParams = { emailList: `${currentUserEmail},money2020@expensify.com`, activationConference: 'money2020', - }).then((response) => { + }; + + // We use makeRequestWithSideEffects here because we need to get the chat report ID to navigate to it after it's created + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects('CreateChatReport', createChatReportParams).then((response) => { // If there's no response or no reportID in the response, navigate the user home so user doesn't get stuck. if (!response || !response.reportID) { Navigation.goBack(); @@ -50,7 +56,7 @@ function runMoney2020Demo() { /** * Runs code for specific demos, based on the provided URL * - * @param {String} url - URL user is navigating to via deep link (or regular link in web) + * @param url - URL user is navigating to via deep link (or regular link in web) */ function runDemoByURL(url = '') { const cleanUrl = (url || '').toLowerCase(); diff --git a/src/types/onyx/Response.ts b/src/types/onyx/Response.ts index 66d5dcbdfd5b..c002c75ec075 100644 --- a/src/types/onyx/Response.ts +++ b/src/types/onyx/Response.ts @@ -11,6 +11,7 @@ type Response = { jsonCode?: number | string; onyxData?: OnyxUpdate[]; requestID?: string; + reportID?: string; shouldPauseQueue?: boolean; authToken?: string; encryptedAuthToken?: string; From 02d646d1c4d503639077a60d4eb97813446d6d82 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 12:12:23 +0100 Subject: [PATCH 016/120] [TS migration] Migrate 'TeacherUnite.js' lib --- src/ONYXKEYS.ts | 2 +- src/libs/PolicyUtils.ts | 2 +- src/libs/ReportUtils.ts | 12 +- src/libs/actions/TeachersUnite.js | 180 ---------------------------- src/libs/actions/TeachersUnite.ts | 189 ++++++++++++++++++++++++++++++ src/types/onyx/OriginalMessage.ts | 4 +- src/types/onyx/PersonalDetails.ts | 4 +- src/types/onyx/Policy.ts | 10 +- src/types/onyx/Report.ts | 3 + src/types/onyx/ReportAction.ts | 2 +- src/types/onyx/index.ts | 3 +- 11 files changed, 213 insertions(+), 198 deletions(-) delete mode 100644 src/libs/actions/TeachersUnite.js create mode 100644 src/libs/actions/TeachersUnite.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0cb3f67bd990..d9d6fb502e5e 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -366,7 +366,7 @@ type OnyxValues = { [ONYXKEYS.NETWORK]: OnyxTypes.Network; [ONYXKEYS.CUSTOM_STATUS_DRAFT]: OnyxTypes.CustomStatusDraft; [ONYXKEYS.INPUT_FOCUSED]: boolean; - [ONYXKEYS.PERSONAL_DETAILS_LIST]: Record; + [ONYXKEYS.PERSONAL_DETAILS_LIST]: OnyxTypes.PersonalDetailsList; [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: OnyxTypes.PrivatePersonalDetails; [ONYXKEYS.TASK]: OnyxTypes.Task; [ONYXKEYS.CURRENCY_LIST]: Record; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 19129959d016..04bf08889870 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -16,7 +16,7 @@ type UnitRate = {rate: number}; function getActivePolicies(policies: OnyxCollection): Policy[] | undefined { return Object.values(policies ?? {}).filter( (policy): policy is Policy => - policy !== null && policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + policy !== null && policy && (policy.isPolicyExpenseChatEnabled || !!policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d93661778b83..a97a24608d66 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -16,8 +16,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Transaction} from '@src/types/onyx'; import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; -import {Message, ReportActions} from '@src/types/onyx/ReportAction'; +import {ChangeLog, IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage'; +import {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; import DeepValueOf from '@src/types/utils/DeepValueOf'; import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; @@ -184,9 +184,10 @@ type OptimisticClosedReportAction = Pick< >; type OptimisticCreatedReportAction = Pick< - ReportAction, - 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' ->; + ReportActionBase, + 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' +> & + OriginalMessageCreated; type OptimisticChatReport = Pick< Report, @@ -311,7 +312,6 @@ type DisplayNameWithTooltips = Array { - sessionEmail = lodashGet(val, 'email', ''); - sessionAccountID = lodashGet(val, 'accountID', 0); - }, -}); - -let allPersonalDetails; -Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => (allPersonalDetails = val), -}); - -/** - * @param {String} partnerUserID - * @param {String} firstName - * @param {String} lastName - */ -function referTeachersUniteVolunteer(partnerUserID, firstName, lastName) { - const optimisticPublicRoom = ReportUtils.buildOptimisticChatReport([], CONST.TEACHERS_UNITE.PUBLIC_ROOM_NAME, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, CONST.TEACHERS_UNITE.POLICY_ID); - const optimisticData = [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticPublicRoom.reportID}`, - value: { - ...optimisticPublicRoom, - reportID: optimisticPublicRoom.reportID, - policyName: CONST.TEACHERS_UNITE.POLICY_NAME, - }, - }, - ]; - API.write( - 'ReferTeachersUniteVolunteer', - { - publicRoomReportID: optimisticPublicRoom.reportID, - firstName, - lastName, - partnerUserID, - }, - {optimisticData}, - ); - Navigation.dismissModal(CONST.TEACHERS_UNITE.PUBLIC_ROOM_ID); -} - -/** - * Optimistically creates a policyExpenseChat for the school principal and passes data to AddSchoolPrincipal - * @param {String} firstName - * @param {String} partnerUserID - * @param {String} lastName - */ -function addSchoolPrincipal(firstName, partnerUserID, lastName) { - const policyName = CONST.TEACHERS_UNITE.POLICY_NAME; - const policyID = CONST.TEACHERS_UNITE.POLICY_ID; - const loggedInEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(sessionEmail); - const reportCreationData = {}; - - const expenseChatData = ReportUtils.buildOptimisticChatReport([sessionAccountID], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, sessionAccountID, true, policyName); - const expenseChatReportID = expenseChatData.reportID; - const expenseReportCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(sessionEmail); - const expenseReportActionData = { - [expenseReportCreatedAction.reportActionID]: expenseReportCreatedAction, - }; - - reportCreationData[loggedInEmail] = { - reportID: expenseChatReportID, - reportActionID: expenseReportCreatedAction.reportActionID, - }; - - API.write( - 'AddSchoolPrincipal', - { - firstName, - lastName, - partnerUserID, - reportCreationData: JSON.stringify(reportCreationData), - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - id: policyID, - isPolicyExpenseChatEnabled: true, - type: CONST.POLICY.TYPE.CORPORATE, - name: policyName, - role: CONST.POLICY.ROLE.USER, - owner: sessionEmail, - outputCurrency: lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD), - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: { - [sessionAccountID]: { - role: CONST.POLICY.ROLE.USER, - errors: {}, - }, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, - value: { - pendingFields: { - addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - ...expenseChatData, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, - value: expenseReportActionData, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: {pendingAction: null}, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, - value: { - pendingFields: { - addWorkspaceRoom: null, - }, - pendingAction: null, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, - value: { - [_.keys(expenseChatData)[0]]: { - pendingAction: null, - }, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: null, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, - value: null, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, - value: null, - }, - ], - }, - ); - Navigation.dismissModal(expenseChatReportID); -} - -export default {referTeachersUniteVolunteer, addSchoolPrincipal}; diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts new file mode 100644 index 000000000000..4b1438090312 --- /dev/null +++ b/src/libs/actions/TeachersUnite.ts @@ -0,0 +1,189 @@ +import Onyx, {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import * as API from '@libs/API'; +import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {PersonalDetailsList} from '@src/types/onyx'; + +type CreationData = { + reportID: string; + reportActionID: string; +}; + +type ReportCreationData = Record; + +let sessionEmail = ''; +let sessionAccountID = 0; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (value) => { + sessionEmail = value?.email ?? ''; + sessionAccountID = value?.accountID ?? 0; + }, +}); + +let allPersonalDetails: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (val) => (allPersonalDetails = val), +}); + +function referTeachersUniteVolunteer(partnerUserID: string, firstName: string, lastName: string) { + const optimisticPublicRoom = ReportUtils.buildOptimisticChatReport([], CONST.TEACHERS_UNITE.PUBLIC_ROOM_NAME, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, CONST.TEACHERS_UNITE.POLICY_ID); + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticPublicRoom.reportID}`, + value: { + ...optimisticPublicRoom, + reportID: optimisticPublicRoom.reportID, + policyName: CONST.TEACHERS_UNITE.POLICY_NAME, + }, + }, + ]; + + type ReferTeachersUniteVolunteerParams = { + publicRoomReportID: string; + firstName: string; + lastName: string; + partnerUserID: string; + }; + + const parameters: ReferTeachersUniteVolunteerParams = { + publicRoomReportID: optimisticPublicRoom.reportID, + firstName, + lastName, + partnerUserID, + }; + + API.write('ReferTeachersUniteVolunteer', parameters, {optimisticData}); + Navigation.dismissModal(CONST.TEACHERS_UNITE.PUBLIC_ROOM_ID); +} + +/** + * Optimistically creates a policyExpenseChat for the school principal and passes data to AddSchoolPrincipal + */ +function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: string) { + const policyName = CONST.TEACHERS_UNITE.POLICY_NAME; + const policyID = CONST.TEACHERS_UNITE.POLICY_ID; + const loggedInEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(sessionEmail); + const reportCreationData: ReportCreationData = {}; + + const expenseChatData = ReportUtils.buildOptimisticChatReport([sessionAccountID], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, sessionAccountID, true, policyName); + const expenseChatReportID = expenseChatData.reportID; + const expenseReportCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(sessionEmail); + const expenseReportActionData = { + [expenseReportCreatedAction.reportActionID]: expenseReportCreatedAction, + }; + + reportCreationData[loggedInEmail] = { + reportID: expenseChatReportID, + reportActionID: expenseReportCreatedAction.reportActionID, + }; + + type AddSchoolPrincipalParams = { + firstName: string; + lastName: string; + partnerUserID: string; + reportCreationData: string; + }; + + const parameters: AddSchoolPrincipalParams = { + firstName, + lastName, + partnerUserID, + reportCreationData: JSON.stringify(reportCreationData), + }; + + API.write('AddSchoolPrincipal', parameters, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + id: policyID, + isPolicyExpenseChatEnabled: true, + type: CONST.POLICY.TYPE.CORPORATE, + name: policyName, + role: CONST.POLICY.ROLE.USER, + owner: sessionEmail, + outputCurrency: allPersonalDetails?.[sessionAccountID]?.localCurrencyCode ?? CONST.CURRENCY.USD, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, + value: { + [sessionAccountID]: { + role: CONST.POLICY.ROLE.USER, + errors: {}, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: { + pendingFields: { + addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + ...expenseChatData, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: expenseReportActionData, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: {pendingAction: null}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: { + pendingFields: { + addWorkspaceRoom: null, + }, + pendingAction: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: { + [Object.keys(expenseChatData)[0]]: { + pendingAction: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: null, + }, + ], + }); + Navigation.dismissModal(expenseChatReportID); +} + +export default {referTeachersUniteVolunteer, addSchoolPrincipal}; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 0dc532ebeded..5e0b70831626 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -117,7 +117,7 @@ type OriginalMessageClosed = { type OriginalMessageCreated = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.CREATED; - originalMessage: unknown; + originalMessage?: unknown; }; type OriginalMessageRenamed = { @@ -225,4 +225,4 @@ type OriginalMessage = | OriginalMessageMoved; export default OriginalMessage; -export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed, OriginalMessageActionName, ChangeLog}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed, OriginalMessageActionName, OriginalMessageCreated, ChangeLog}; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index af559eafd0a1..8f824272230e 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -76,6 +76,8 @@ type PersonalDetails = { payPalMeAddress?: string; }; +type PersonalDetailsList = Record; + export default PersonalDetails; -export type {Timezone, SelectedTimezone}; +export type {Timezone, SelectedTimezone, PersonalDetailsList}; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index e6e3240d1b23..5bef0cf932b1 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -19,7 +19,7 @@ type Policy = { owner: string; /** The accountID of the policy owner */ - ownerAccountID: number; + ownerAccountID?: number; /** The output currency for the policy */ outputCurrency: string; @@ -34,7 +34,7 @@ type Policy = { pendingAction?: OnyxCommon.PendingAction; /** A list of errors keyed by microtime */ - errors: OnyxCommon.Errors; + errors?: OnyxCommon.Errors; /** Whether this policy was loaded from a policy summary, or loaded completely with all of its values */ isFromFullPolicy?: boolean; @@ -46,16 +46,16 @@ type Policy = { customUnits?: Record; /** Whether chat rooms can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ - areChatRoomsEnabled: boolean; + areChatRoomsEnabled?: boolean; /** Whether policy expense chats can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ isPolicyExpenseChatEnabled: boolean; /** Whether the scheduled submit is enabled */ - autoReporting: boolean; + autoReporting?: boolean; /** The scheduled submit frequency set up on the this policy */ - autoReportingFrequency: ValueOf; + autoReportingFrequency?: ValueOf; /** The employee list of the policy */ employeeList?: []; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 81a92c4bf603..0f0ccdd0826e 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -130,6 +130,9 @@ type Report = { /** Pending fields for the report */ pendingFields?: Record; + /** Pending action for the report */ + pendingAction?: OnyxCommon.PendingAction | null; + /** The ID of the preexisting report (it is possible that we optimistically created a Report for which a report already exists) */ preexistingReportID?: string; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 891a0ffcb7b8..895ce793ad53 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -145,4 +145,4 @@ type ReportAction = ReportActionBase & OriginalMessage; type ReportActions = Record; export default ReportAction; -export type {ReportActions, Message}; +export type {ReportActions, Message, ReportActionBase, OriginalMessage}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e7b9c7661c79..f4acef24cd18 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -18,7 +18,7 @@ import Modal from './Modal'; import Network from './Network'; import {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer'; import PersonalBankAccount from './PersonalBankAccount'; -import PersonalDetails from './PersonalDetails'; +import PersonalDetails, {PersonalDetailsList} from './PersonalDetails'; import PlaidData from './PlaidData'; import Policy from './Policy'; import PolicyCategory from './PolicyCategory'; @@ -77,6 +77,7 @@ export type { OnyxUpdatesFromServer, PersonalBankAccount, PersonalDetails, + PersonalDetailsList, PlaidData, Policy, PolicyCategory, From 2857187b7c5831e2ddd436c4a4d4ab19a832fd2a Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 12:42:17 +0100 Subject: [PATCH 017/120] Code improvements --- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Card.ts | 18 +++++++++--------- src/libs/actions/DemoActions.ts | 4 ++-- src/libs/actions/TeachersUnite.ts | 9 ++++++--- src/libs/actions/TransactionEdit.ts | 2 +- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a97a24608d66..ae4c4217e6aa 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4383,4 +4383,4 @@ export { canEditWriteCapability, }; -export type {OptionData}; +export type {OptionData, OptimisticCreatedReportAction}; diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 82137cc7c4cc..8a2923d9c6fd 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -3,18 +3,18 @@ import * as API from '@libs/API'; import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Response} from '@src/types/onyx'; +import type {Response} from '@src/types/onyx'; function reportVirtualExpensifyCardFraud(cardID: number) { type ReportVirtualExpensifyCardFraudParams = { cardID: number; }; - const reportVirtualExpensifyCardFraudParams: ReportVirtualExpensifyCardFraudParams = { + const parameters: ReportVirtualExpensifyCardFraudParams = { cardID, }; - API.write('ReportVirtualExpensifyCardFraud', reportVirtualExpensifyCardFraudParams, { + API.write('ReportVirtualExpensifyCardFraud', parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -56,12 +56,12 @@ function requestReplacementExpensifyCard(cardId: number, reason: string) { reason: string; }; - const requestReplacementExpensifyCardParams: RequestReplacementExpensifyCardParams = { + const parameters: RequestReplacementExpensifyCardParams = { cardId, reason, }; - API.write('RequestReplacementExpensifyCard', requestReplacementExpensifyCardParams, { + API.write('RequestReplacementExpensifyCard', parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -102,12 +102,12 @@ function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: numbe cardID: number; }; - const activatePhysicalExpensifyCardParams: ActivatePhysicalExpensifyCardParams = { + const parameters: ActivatePhysicalExpensifyCardParams = { cardLastFourDigits, cardID, }; - API.write('ActivatePhysicalExpensifyCard', activatePhysicalExpensifyCardParams, { + API.write('ActivatePhysicalExpensifyCard', parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -166,10 +166,10 @@ function revealVirtualCardDetails(cardID: number): Promise { return new Promise((resolve, reject) => { type RevealExpensifyCardDetailsParams = {cardID: number}; - const revealExpensifyCardDetailsParams: RevealExpensifyCardDetailsParams = {cardID}; + const parameters: RevealExpensifyCardDetailsParams = {cardID}; // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('RevealExpensifyCardDetails', revealExpensifyCardDetailsParams) + API.makeRequestWithSideEffects('RevealExpensifyCardDetails', parameters) .then((response) => { if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); diff --git a/src/libs/actions/DemoActions.ts b/src/libs/actions/DemoActions.ts index 41f5a54977cb..79c7c1652b1c 100644 --- a/src/libs/actions/DemoActions.ts +++ b/src/libs/actions/DemoActions.ts @@ -30,14 +30,14 @@ function runMoney2020Demo() { activationConference: string; }; - const createChatReportParams: CreateChatReportParams = { + const parameters: CreateChatReportParams = { emailList: `${currentUserEmail},money2020@expensify.com`, activationConference: 'money2020', }; // We use makeRequestWithSideEffects here because we need to get the chat report ID to navigate to it after it's created // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('CreateChatReport', createChatReportParams).then((response) => { + API.makeRequestWithSideEffects('CreateChatReport', parameters).then((response) => { // If there's no response or no reportID in the response, navigate the user home so user doesn't get stuck. if (!response || !response.reportID) { Navigation.goBack(); diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index 4b1438090312..f264d81f33d4 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -3,9 +3,10 @@ import * as API from '@libs/API'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import type {OptimisticCreatedReportAction} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetailsList} from '@src/types/onyx'; +import type {PersonalDetailsList} from '@src/types/onyx'; type CreationData = { reportID: string; @@ -14,6 +15,8 @@ type CreationData = { type ReportCreationData = Record; +type ExpenseReportActionData = Record; + let sessionEmail = ''; let sessionAccountID = 0; Onyx.connect({ @@ -27,7 +30,7 @@ Onyx.connect({ let allPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => (allPersonalDetails = val), + callback: (value) => (allPersonalDetails = value), }); function referTeachersUniteVolunteer(partnerUserID: string, firstName: string, lastName: string) { @@ -74,7 +77,7 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: const expenseChatData = ReportUtils.buildOptimisticChatReport([sessionAccountID], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, sessionAccountID, true, policyName); const expenseChatReportID = expenseChatData.reportID; const expenseReportCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(sessionEmail); - const expenseReportActionData = { + const expenseReportActionData: ExpenseReportActionData = { [expenseReportCreatedAction.reportActionID]: expenseReportCreatedAction, }; diff --git a/src/libs/actions/TransactionEdit.ts b/src/libs/actions/TransactionEdit.ts index 387dacddbcdc..3831ba8e437d 100644 --- a/src/libs/actions/TransactionEdit.ts +++ b/src/libs/actions/TransactionEdit.ts @@ -1,6 +1,6 @@ import Onyx, {OnyxEntry} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Transaction} from '@src/types/onyx'; +import type {Transaction} from '@src/types/onyx'; /** * Makes a backup copy of a transaction object that can be restored when the user cancels editing a transaction. From 00cad5e62b9e96fb894c5b40a8c1da27e023aee6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 15:09:22 +0100 Subject: [PATCH 018/120] Fix crash --- src/libs/actions/DemoActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/DemoActions.ts b/src/libs/actions/DemoActions.ts index 79c7c1652b1c..b764d8268482 100644 --- a/src/libs/actions/DemoActions.ts +++ b/src/libs/actions/DemoActions.ts @@ -16,7 +16,7 @@ Onyx.connect({ function runMoney2020Demo() { // Try to navigate to existing demo chat if it exists in Onyx - const money2020AccountID = Number(Config.EXPENSIFY_ACCOUNT_ID_MONEY2020 ?? 15864555); + const money2020AccountID = Number(Config?.EXPENSIFY_ACCOUNT_ID_MONEY2020 ?? 15864555); const existingChatReport = ReportUtils.getChatByParticipants([money2020AccountID]); if (existingChatReport) { // We must call goBack() to remove the demo route from nav history From f205afe637eab9117f07b3c493fe831eba0d32b1 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 30 Nov 2023 09:49:29 +0100 Subject: [PATCH 019/120] Add @types/canvas-size lib --- package-lock.json | 13 +++++++++++++ package.json | 1 + src/libs/actions/CanvasSize.ts | 6 +++--- src/libs/actions/Card.ts | 6 ++++-- src/types/modules/canvas-size.d.ts | 6 ------ 5 files changed, 21 insertions(+), 11 deletions(-) delete mode 100644 src/types/modules/canvas-size.d.ts diff --git a/package-lock.json b/package-lock.json index 32271f8dc743..4a333726f64f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -159,6 +159,7 @@ "@testing-library/jest-native": "5.4.1", "@testing-library/react-native": "11.5.1", "@trivago/prettier-plugin-sort-imports": "^4.2.0", + "@types/canvas-size": "^1.2.2", "@types/concurrently": "^7.0.0", "@types/jest": "^29.5.2", "@types/jest-when": "^3.5.2", @@ -19082,6 +19083,12 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/canvas-size": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/canvas-size/-/canvas-size-1.2.2.tgz", + "integrity": "sha512-yuTXFWC4tHV3lt5ZtbIP9VeeMNbDYm5mPyqaQnaMuSSx2mjsfZGXMNmHTnfdsR5qZdB6dtbaV5IP2PKv79vmKg==", + "dev": true + }, "node_modules/@types/concurrently": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/concurrently/-/concurrently-7.0.0.tgz", @@ -66435,6 +66442,12 @@ "@types/responselike": "^1.0.0" } }, + "@types/canvas-size": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/canvas-size/-/canvas-size-1.2.2.tgz", + "integrity": "sha512-yuTXFWC4tHV3lt5ZtbIP9VeeMNbDYm5mPyqaQnaMuSSx2mjsfZGXMNmHTnfdsR5qZdB6dtbaV5IP2PKv79vmKg==", + "dev": true + }, "@types/concurrently": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/concurrently/-/concurrently-7.0.0.tgz", diff --git a/package.json b/package.json index 7da3658e67b6..15d0f876d45d 100644 --- a/package.json +++ b/package.json @@ -206,6 +206,7 @@ "@testing-library/jest-native": "5.4.1", "@testing-library/react-native": "11.5.1", "@trivago/prettier-plugin-sort-imports": "^4.2.0", + "@types/canvas-size": "^1.2.2", "@types/concurrently": "^7.0.0", "@types/jest": "^29.5.2", "@types/jest-when": "^3.5.2", diff --git a/src/libs/actions/CanvasSize.ts b/src/libs/actions/CanvasSize.ts index 9de851aacae3..8e0a155f25eb 100644 --- a/src/libs/actions/CanvasSize.ts +++ b/src/libs/actions/CanvasSize.ts @@ -11,7 +11,7 @@ function retrieveMaxCanvasArea() { // More information at: https://github.com/jhildenbiddle/canvas-size/issues/13 canvasSize .maxArea({ - max: Browser.isMobile() ? 8192 : null, + max: Browser.isMobile() ? 8192 : undefined, usePromise: true, useWorker: false, }) @@ -27,7 +27,7 @@ function retrieveMaxCanvasArea() { */ function retrieveMaxCanvasHeight() { canvasSize.maxHeight({ - onSuccess: (width: number, height: number) => { + onSuccess: (width, height) => { Onyx.merge(ONYXKEYS.MAX_CANVAS_HEIGHT, height); }, }); @@ -38,7 +38,7 @@ function retrieveMaxCanvasHeight() { */ function retrieveMaxCanvasWidth() { canvasSize.maxWidth({ - onSuccess: (width: number) => { + onSuccess: (width) => { Onyx.merge(ONYXKEYS.MAX_CANVAS_WIDTH, width); }, }); diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 8a2923d9c6fd..6e1753fbd591 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -5,6 +5,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Response} from '@src/types/onyx'; +type Reason = 'damaged' | 'stolen'; + function reportVirtualExpensifyCardFraud(cardID: number) { type ReportVirtualExpensifyCardFraudParams = { cardID: number; @@ -48,9 +50,9 @@ function reportVirtualExpensifyCardFraud(cardID: number) { /** * Call the API to deactivate the card and request a new one * @param cardId - id of the card that is going to be replaced - * @param reason - reason for replacement ('damaged' | 'stolen') + * @param reason - reason for replacement */ -function requestReplacementExpensifyCard(cardId: number, reason: string) { +function requestReplacementExpensifyCard(cardId: number, reason: Reason) { type RequestReplacementExpensifyCardParams = { cardId: number; reason: string; diff --git a/src/types/modules/canvas-size.d.ts b/src/types/modules/canvas-size.d.ts deleted file mode 100644 index 6e1243aa657a..000000000000 --- a/src/types/modules/canvas-size.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ -declare module 'canvas-size' { - import canvasSize from 'canvas-size'; - - export default canvasSize; -} From 27c9dde4f846bcbe58187e53ea500618a748c77b Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 30 Nov 2023 10:04:28 +0100 Subject: [PATCH 020/120] Update code to use PersonalDetailsList type --- src/components/ArchivedReportFooter.tsx | 4 ++-- src/libs/GroupChatUtils.ts | 4 ++-- src/libs/PolicyUtils.ts | 3 +-- src/libs/ReportUtils.ts | 4 ++-- src/libs/actions/PersonalDetails.ts | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 3187bf3604e8..712ef6be769e 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -8,7 +8,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Report, ReportAction} from '@src/types/onyx'; +import type {PersonalDetailsList, Report, ReportAction} from '@src/types/onyx'; import Banner from './Banner'; type ArchivedReportFooterOnyxProps = { @@ -16,7 +16,7 @@ type ArchivedReportFooterOnyxProps = { reportClosedAction: OnyxEntry; /** Personal details of all users */ - personalDetails: OnyxEntry>; + personalDetails: OnyxEntry; }; type ArchivedReportFooterProps = ArchivedReportFooterOnyxProps & { diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts index db64f6574824..862c50700c0c 100644 --- a/src/libs/GroupChatUtils.ts +++ b/src/libs/GroupChatUtils.ts @@ -1,10 +1,10 @@ import Onyx, {OnyxEntry} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetails, Report} from '@src/types/onyx'; +import {PersonalDetailsList, Report} from '@src/types/onyx'; import * as OptionsListUtils from './OptionsListUtils'; import * as ReportUtils from './ReportUtils'; -let allPersonalDetails: OnyxEntry> = {}; +let allPersonalDetails: OnyxEntry = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => (allPersonalDetails = val), diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 04bf08889870..d09fdbc892da 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -2,11 +2,10 @@ import Str from 'expensify-common/lib/str'; import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetails, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; +import {PersonalDetailsList, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; type MemberEmailsToAccountIDs = Record; -type PersonalDetailsList = Record; type UnitRate = {rate: number}; /** diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ae4c4217e6aa..fb452ce6f26a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -14,7 +14,7 @@ import CONST from '@src/CONST'; import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Transaction} from '@src/types/onyx'; +import {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, PolicyTags, Report, ReportAction, Transaction} from '@src/types/onyx'; import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import {ChangeLog, IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage'; import {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; @@ -1396,7 +1396,7 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f } function getDisplayNamesWithTooltips( - personalDetailsList: PersonalDetails[] | Record, + personalDetailsList: PersonalDetails[] | PersonalDetailsList, isMultipleParticipantReport: boolean, shouldFallbackToHidden = true, ): DisplayNameWithTooltips { diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 29d18d543a11..02b5f70db285 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -9,7 +9,7 @@ import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {DateOfBirthForm, PersonalDetails, PrivatePersonalDetails} from '@src/types/onyx'; +import {DateOfBirthForm, PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; import {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; type FirstAndLastName = { @@ -27,7 +27,7 @@ Onyx.connect({ }, }); -let allPersonalDetails: OnyxEntry> = null; +let allPersonalDetails: OnyxEntry = null; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => (allPersonalDetails = val), From 2dfbe5da9b46001579cdfca33f8fb4f16274f368 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 30 Nov 2023 10:28:35 +0100 Subject: [PATCH 021/120] Update invalid format check --- src/libs/actions/DemoActions.ts | 4 ++-- src/libs/actions/OnyxUpdateManager.ts | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/DemoActions.ts b/src/libs/actions/DemoActions.ts index b764d8268482..363b8434a2ce 100644 --- a/src/libs/actions/DemoActions.ts +++ b/src/libs/actions/DemoActions.ts @@ -9,8 +9,8 @@ import ROUTES from '@src/ROUTES'; let currentUserEmail: string; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { - currentUserEmail = val?.email ?? ''; + callback: (value) => { + currentUserEmail = value?.email ?? ''; }, }); diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index b61c8eeae268..ab0dea960b27 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -39,10 +39,9 @@ export default () => { // Since we used the same key that used to store another object, let's confirm that the current object is // following the new format before we proceed. If it isn't, then let's clear the object in Onyx. if ( - value === null || - !Object.hasOwn(value, 'type') || - (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && Object.hasOwn(value, 'request') && Object.hasOwn(value, 'response')) && - !(value.type === CONST.ONYX_UPDATE_TYPES.PUSHER && Object.hasOwn(value, 'updates'))) + !(typeof value === 'object' && !!value) || + !('type' in value) || + (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && value.request && value.response) && !(value.type === CONST.ONYX_UPDATE_TYPES.PUSHER && value.updates)) ) { console.debug('[OnyxUpdateManager] Invalid format found for updates, cleaning and unpausing the queue'); Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); From 44cc8fe2bdbe7bca7920d4ca032a891bdae9d8b7 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Dec 2023 16:48:38 +0500 Subject: [PATCH 022/120] refactor: remove unnecessary useMemo --- src/pages/home/sidebar/SidebarLinks.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 52dbe879d218..09d6c1f2de62 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -148,8 +148,6 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT; // eslint-disable-next-line react-hooks/exhaustive-deps - const listStyle = useMemo(() => [isLoading ? styles.flexShrink1 : styles.flex1], [isLoading]); - // eslint-disable-next-line react-hooks/exhaustive-deps const contentContainerStyles = useMemo(() => [styles.sidebarListContainer, {paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}], [insets]); return ( @@ -183,7 +181,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority Date: Mon, 4 Dec 2023 16:15:40 +0100 Subject: [PATCH 023/120] Put onyx update data into separate variables --- src/libs/actions/Card.ts | 190 +++++++++++++++--------------- src/libs/actions/TeachersUnite.ts | 174 +++++++++++++-------------- 2 files changed, 186 insertions(+), 178 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 6e1753fbd591..82b9cbc47e7c 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -1,4 +1,4 @@ -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; @@ -8,6 +8,36 @@ import type {Response} from '@src/types/onyx'; type Reason = 'damaged' | 'stolen'; function reportVirtualExpensifyCardFraud(cardID: number) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: true, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: false, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: false, + }, + }, + ]; + type ReportVirtualExpensifyCardFraudParams = { cardID: number; }; @@ -16,35 +46,7 @@ function reportVirtualExpensifyCardFraud(cardID: number) { cardID, }; - API.write('ReportVirtualExpensifyCardFraud', parameters, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, - value: { - isLoading: true, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, - value: { - isLoading: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, - value: { - isLoading: false, - }, - }, - ], - }); + API.write('ReportVirtualExpensifyCardFraud', parameters, {optimisticData, successData, failureData}); } /** @@ -53,6 +55,37 @@ function reportVirtualExpensifyCardFraud(cardID: number) { * @param reason - reason for replacement */ function requestReplacementExpensifyCard(cardId: number, reason: Reason) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: true, + errors: null, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + }, + }, + ]; + type RequestReplacementExpensifyCardParams = { cardId: number; reason: string; @@ -63,42 +96,50 @@ function requestReplacementExpensifyCard(cardId: number, reason: Reason) { reason, }; - API.write('RequestReplacementExpensifyCard', parameters, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { - isLoading: true, + API.write('RequestReplacementExpensifyCard', parameters, {optimisticData, successData, failureData}); +} + +/** + * Activates the physical Expensify card based on the last four digits of the card number + */ +function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: number) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { errors: null, + isLoading: true, }, }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { isLoading: false, }, }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { isLoading: false, }, }, - ], - }); -} + }, + ]; -/** - * Activates the physical Expensify card based on the last four digits of the card number - */ -function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: number) { type ActivatePhysicalExpensifyCardParams = { cardLastFourDigits: string; cardID: number; @@ -109,42 +150,7 @@ function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: numbe cardID, }; - API.write('ActivatePhysicalExpensifyCard', parameters, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CARD_LIST, - value: { - [cardID]: { - errors: null, - isLoading: true, - }, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CARD_LIST, - value: { - [cardID]: { - isLoading: false, - }, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CARD_LIST, - value: { - [cardID]: { - isLoading: false, - }, - }, - }, - ], - }); + API.write('ActivatePhysicalExpensifyCard', parameters, {optimisticData, successData, failureData}); } /** diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index f264d81f33d4..4768794c39f2 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -86,6 +86,93 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: reportActionID: expenseReportCreatedAction.reportActionID, }; + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + id: policyID, + isPolicyExpenseChatEnabled: true, + type: CONST.POLICY.TYPE.CORPORATE, + name: policyName, + role: CONST.POLICY.ROLE.USER, + owner: sessionEmail, + outputCurrency: allPersonalDetails?.[sessionAccountID]?.localCurrencyCode ?? CONST.CURRENCY.USD, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, + value: { + [sessionAccountID]: { + role: CONST.POLICY.ROLE.USER, + errors: {}, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: { + pendingFields: { + addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + ...expenseChatData, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: expenseReportActionData, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: {pendingAction: null}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: { + pendingFields: { + addWorkspaceRoom: null, + }, + pendingAction: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: { + [Object.keys(expenseChatData)[0]]: { + pendingAction: null, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: null, + }, + ]; + type AddSchoolPrincipalParams = { firstName: string; lastName: string; @@ -100,92 +187,7 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: reportCreationData: JSON.stringify(reportCreationData), }; - API.write('AddSchoolPrincipal', parameters, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - id: policyID, - isPolicyExpenseChatEnabled: true, - type: CONST.POLICY.TYPE.CORPORATE, - name: policyName, - role: CONST.POLICY.ROLE.USER, - owner: sessionEmail, - outputCurrency: allPersonalDetails?.[sessionAccountID]?.localCurrencyCode ?? CONST.CURRENCY.USD, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: { - [sessionAccountID]: { - role: CONST.POLICY.ROLE.USER, - errors: {}, - }, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, - value: { - pendingFields: { - addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - ...expenseChatData, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, - value: expenseReportActionData, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: {pendingAction: null}, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, - value: { - pendingFields: { - addWorkspaceRoom: null, - }, - pendingAction: null, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, - value: { - [Object.keys(expenseChatData)[0]]: { - pendingAction: null, - }, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: null, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, - value: null, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, - value: null, - }, - ], - }); + API.write('AddSchoolPrincipal', parameters, {optimisticData, successData, failureData}); Navigation.dismissModal(expenseChatReportID); } From 97fbe8453a7e6f7ac7b749fc8bf6dfbba7f8e5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Tue, 5 Dec 2023 14:59:43 +0100 Subject: [PATCH 024/120] refactor to functional component --- src/pages/SearchPage.js | 313 +++++++++++++++++------------------- src/pages/SearchPage.old.js | 243 ++++++++++++++++++++++++++++ 2 files changed, 389 insertions(+), 167 deletions(-) mode change 100755 => 100644 src/pages/SearchPage.js create mode 100755 src/pages/SearchPage.old.js diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js old mode 100755 new mode 100644 index 5d111e7c181f..77a9ddf930ca --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -1,21 +1,18 @@ +import _ from 'lodash'; import PropTypes from 'prop-types'; -import React, {Component} from 'react'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import compose from '@libs/compose'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import * as ReportUtils from '@libs/ReportUtils'; +import useThemeStyles from '@styles/useThemeStyles'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; @@ -35,209 +32,191 @@ const propTypes = { /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), - /** Window Dimensions Props */ - ...windowDimensionsPropTypes, - - ...withLocalizePropTypes, - - /** Network info */ - network: networkPropTypes, - /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, - ...withThemeStylesPropTypes, }; const defaultProps = { betas: [], personalDetails: {}, reports: {}, - network: {}, isSearchingForReports: false, }; -class SearchPage extends Component { - constructor(props) { - super(props); +// custom hook that handles debouncing the search value using lodash debounce +function useDebouncedState(initialValue, delay) { + const [value, setValue] = useState(initialValue); + const [debouncedValue, setDebouncedValue] = useState(initialValue); + const debouncedSetDebouncedValue = useRef(_.debounce(setDebouncedValue, delay)).current; + + useEffect(() => debouncedSetDebouncedValue.cancel, [debouncedSetDebouncedValue]); + + const handleSetValue = (newValue) => { + setValue(newValue); + debouncedSetDebouncedValue(newValue); + }; + + return [value, debouncedValue, handleSetValue]; +} + +function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { + const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); + const {translate} = useLocalize(); + const network = useNetwork(); + const themeStyles = useThemeStyles(); + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('', 75); + + useEffect(() => { Timing.start(CONST.TIMING.SEARCH_RENDER); Performance.markStart(CONST.TIMING.SEARCH_RENDER); - - this.searchRendered = this.searchRendered.bind(this); - this.selectReport = this.selectReport.bind(this); - this.onChangeText = this.onChangeText.bind(this); - this.updateOptions = this.updateOptions.bind(this); - this.debouncedUpdateOptions = _.debounce(this.updateOptions.bind(this), 75); - this.state = { - searchValue: '', - recentReports: {}, - personalDetails: {}, - userToInvite: {}, - }; - } - - componentDidUpdate(prevProps) { - if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { - return; + }, []); + + const onChangeText = (text = '') => { + Report.searchInServer(text); + setSearchValue(text); + }; + + const { + recentReports, + personalDetails: localPersonalDetails, + userToInvite, + headerMessage, + } = useMemo(() => { + console.log('updateOptions', isScreenTransitionEnd); + if (!isScreenTransitionEnd) { + return { + recentReports: {}, + personalDetails: {}, + userToInvite: {}, + headerMessage: '', + }; } - this.updateOptions(); - } - - onChangeText(searchValue = '') { - Report.searchInServer(searchValue); - this.setState({searchValue}, this.debouncedUpdateOptions); - } - - /** - * Returns the sections needed for the OptionsSelector - * - * @returns {Array} - */ - getSections() { - const sections = []; + const options = OptionsListUtils.getSearchOptions(reports, personalDetails, debouncedSearchValue.trim(), betas); + const header = OptionsListUtils.getHeaderMessage(options.recentReports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); + return {...options, headerMessage: header}; + }, [debouncedSearchValue, reports, personalDetails, betas, isScreenTransitionEnd]); + + const sections = useMemo(() => { + console.log('updateSections'); + const newSections = []; let indexOffset = 0; - if (this.state.recentReports.length > 0) { - sections.push({ - data: this.state.recentReports, + if (recentReports.length > 0) { + newSections.push({ + data: recentReports, shouldShow: true, indexOffset, }); - indexOffset += this.state.recentReports.length; + indexOffset += recentReports.length; } - if (this.state.personalDetails.length > 0) { - sections.push({ - data: this.state.personalDetails, + if (localPersonalDetails.length > 0) { + newSections.push({ + data: localPersonalDetails, shouldShow: true, indexOffset, }); - indexOffset += this.state.recentReports.length; + indexOffset += recentReports.length; } - if (this.state.userToInvite) { - sections.push({ - data: [this.state.userToInvite], + if (userToInvite) { + newSections.push({ + data: [userToInvite], shouldShow: true, indexOffset, }); } - return sections; - } + return newSections; + }, [localPersonalDetails, recentReports, userToInvite]); - searchRendered() { - Timing.end(CONST.TIMING.SEARCH_RENDER); - Performance.markEnd(CONST.TIMING.SEARCH_RENDER); - } - - updateOptions() { - const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions( - this.props.reports, - this.props.personalDetails, - this.state.searchValue.trim(), - this.props.betas, - ); - this.setState({ - userToInvite, - recentReports, - personalDetails, - }); - } - - /** - * Reset the search value and redirect to the selected report - * - * @param {Object} option - */ - selectReport(option) { + const selectReport = (option) => { if (!option) { return; } if (option.reportID) { - this.setState( - { - searchValue: '', - }, - () => { - Navigation.dismissModal(option.reportID); - }, - ); + setSearchValue(''); + Navigation.dismissModal(option.reportID); } else { Report.navigateToAndOpenReport([option.login]); } - } - - render() { - const sections = this.getSections(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); - const headerMessage = OptionsListUtils.getHeaderMessage( - this.state.recentReports.length + this.state.personalDetails.length !== 0, - Boolean(this.state.userToInvite), - this.state.searchValue, - ); - - return ( - - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( - <> - - - - - - )} - - ); - } + }; + + const searchRendered = () => { + Timing.end(CONST.TIMING.SEARCH_RENDER); + Performance.markEnd(CONST.TIMING.SEARCH_RENDER); + }; + + const handleScreenTransitionEnd = () => { + console.log('handleScreenTransitionEnd'); + setIsScreenTransitionEnd(true); + }; + + const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); + + console.log('render', { + isScreenTransitionEnd, + isOptionsDataReady, + sections: sections.length, + recentReports, + localPersonalDetails, + userToInvite, + headerMessage, + }); + + return ( + + {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + <> + + + + + + )} + + ); } SearchPage.propTypes = propTypes; SearchPage.defaultProps = defaultProps; SearchPage.displayName = 'SearchPage'; -export default compose( - withLocalize, - withWindowDimensions, - withNetwork(), - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, - }), - withThemeStyles, -)(SearchPage); +export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + isSearchingForReports: { + key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, + initWithStoredValues: false, + }, +})(SearchPage); diff --git a/src/pages/SearchPage.old.js b/src/pages/SearchPage.old.js new file mode 100755 index 000000000000..5d111e7c181f --- /dev/null +++ b/src/pages/SearchPage.old.js @@ -0,0 +1,243 @@ +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import networkPropTypes from '@components/networkPropTypes'; +import {withNetwork} from '@components/OnyxProvider'; +import OptionsSelector from '@components/OptionsSelector'; +import ScreenWrapper from '@components/ScreenWrapper'; +import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; +import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import compose from '@libs/compose'; +import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import Performance from '@libs/Performance'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as Report from '@userActions/Report'; +import Timing from '@userActions/Timing'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import personalDetailsPropType from './personalDetailsPropType'; +import reportPropTypes from './reportPropTypes'; + +const propTypes = { + /* Onyx Props */ + + /** Beta features list */ + betas: PropTypes.arrayOf(PropTypes.string), + + /** All of the personal details for everyone */ + personalDetails: PropTypes.objectOf(personalDetailsPropType), + + /** All reports shared with the user */ + reports: PropTypes.objectOf(reportPropTypes), + + /** Window Dimensions Props */ + ...windowDimensionsPropTypes, + + ...withLocalizePropTypes, + + /** Network info */ + network: networkPropTypes, + + /** Whether we are searching for reports in the server */ + isSearchingForReports: PropTypes.bool, + ...withThemeStylesPropTypes, +}; + +const defaultProps = { + betas: [], + personalDetails: {}, + reports: {}, + network: {}, + isSearchingForReports: false, +}; + +class SearchPage extends Component { + constructor(props) { + super(props); + + Timing.start(CONST.TIMING.SEARCH_RENDER); + Performance.markStart(CONST.TIMING.SEARCH_RENDER); + + this.searchRendered = this.searchRendered.bind(this); + this.selectReport = this.selectReport.bind(this); + this.onChangeText = this.onChangeText.bind(this); + this.updateOptions = this.updateOptions.bind(this); + this.debouncedUpdateOptions = _.debounce(this.updateOptions.bind(this), 75); + this.state = { + searchValue: '', + recentReports: {}, + personalDetails: {}, + userToInvite: {}, + }; + } + + componentDidUpdate(prevProps) { + if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { + return; + } + this.updateOptions(); + } + + onChangeText(searchValue = '') { + Report.searchInServer(searchValue); + this.setState({searchValue}, this.debouncedUpdateOptions); + } + + /** + * Returns the sections needed for the OptionsSelector + * + * @returns {Array} + */ + getSections() { + const sections = []; + let indexOffset = 0; + + if (this.state.recentReports.length > 0) { + sections.push({ + data: this.state.recentReports, + shouldShow: true, + indexOffset, + }); + indexOffset += this.state.recentReports.length; + } + + if (this.state.personalDetails.length > 0) { + sections.push({ + data: this.state.personalDetails, + shouldShow: true, + indexOffset, + }); + indexOffset += this.state.recentReports.length; + } + + if (this.state.userToInvite) { + sections.push({ + data: [this.state.userToInvite], + shouldShow: true, + indexOffset, + }); + } + + return sections; + } + + searchRendered() { + Timing.end(CONST.TIMING.SEARCH_RENDER); + Performance.markEnd(CONST.TIMING.SEARCH_RENDER); + } + + updateOptions() { + const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions( + this.props.reports, + this.props.personalDetails, + this.state.searchValue.trim(), + this.props.betas, + ); + this.setState({ + userToInvite, + recentReports, + personalDetails, + }); + } + + /** + * Reset the search value and redirect to the selected report + * + * @param {Object} option + */ + selectReport(option) { + if (!option) { + return; + } + + if (option.reportID) { + this.setState( + { + searchValue: '', + }, + () => { + Navigation.dismissModal(option.reportID); + }, + ); + } else { + Report.navigateToAndOpenReport([option.login]); + } + } + + render() { + const sections = this.getSections(); + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); + const headerMessage = OptionsListUtils.getHeaderMessage( + this.state.recentReports.length + this.state.personalDetails.length !== 0, + Boolean(this.state.userToInvite), + this.state.searchValue, + ); + + return ( + + {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + <> + + + + + + )} + + ); + } +} + +SearchPage.propTypes = propTypes; +SearchPage.defaultProps = defaultProps; +SearchPage.displayName = 'SearchPage'; + +export default compose( + withLocalize, + withWindowDimensions, + withNetwork(), + withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + isSearchingForReports: { + key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, + initWithStoredValues: false, + }, + }), + withThemeStyles, +)(SearchPage); From abb5fc6ec20cc047e0c31618b478e4174e7f8901 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 6 Dec 2023 17:06:50 +0500 Subject: [PATCH 025/120] fix: personalDetails not updating --- src/libs/PersonalDetailsUtils.js | 10 ---------- src/pages/SearchPage.js | 15 +++++++++++---- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index bbe4df529ade..560480dcec9d 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -197,17 +197,7 @@ function getFormattedAddress(privatePersonalDetails) { return formattedAddress.trim().replace(/,$/, ''); } -/** - * get personal details - * - * @returns {Object} - */ -function getPersonalDetails() { - return allPersonalDetails || {}; -} - export { - getPersonalDetails, getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 34afe5e3c3b5..5dfa3a0cacc4 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -15,12 +15,12 @@ import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import personalDetailsPropType from './personalDetailsPropType'; import reportPropTypes from './reportPropTypes'; const propTypes = { @@ -29,6 +29,9 @@ const propTypes = { /** Beta features list */ betas: PropTypes.arrayOf(PropTypes.string), + /** All of the personal details for everyone */ + personalDetails: PropTypes.objectOf(personalDetailsPropType), + /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), @@ -47,6 +50,7 @@ const propTypes = { const defaultProps = { betas: [], + personalDetails: {}, reports: {}, network: {}, isSearchingForReports: false, @@ -85,7 +89,7 @@ class SearchPage extends Component { } componentDidUpdate(prevProps) { - if (_.isEqual(prevProps.reports, this.props.reports)) { + if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { return; } this.updateOptions(); @@ -155,7 +159,7 @@ class SearchPage extends Component { this.interactionTask = InteractionManager.runAfterInteractions(() => { const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions( this.props.reports, - PersonalDetailsUtils.getPersonalDetails(), + this.props.personalDetails, this.state.searchValue.trim(), this.props.betas, ); @@ -193,7 +197,7 @@ class SearchPage extends Component { render() { const sections = this.getSections(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(PersonalDetailsUtils.getPersonalDetails()); + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); const headerMessage = OptionsListUtils.getHeaderMessage( this.state.recentReports.length + this.state.personalDetails.length !== 0, Boolean(this.state.userToInvite), @@ -257,6 +261,9 @@ export default compose( key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, initWithStoredValues: false, }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, }), withThemeStyles, )(SearchPage); From e51ee0bdbaaa43cecd74ebca8bd051fac4ac514d Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 6 Dec 2023 16:21:58 +0100 Subject: [PATCH 026/120] Fix TS issue --- src/types/onyx/DemoInfo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/DemoInfo.ts b/src/types/onyx/DemoInfo.ts index dcd7efc44d8d..300846fb1a0e 100644 --- a/src/types/onyx/DemoInfo.ts +++ b/src/types/onyx/DemoInfo.ts @@ -1,5 +1,5 @@ type DemoInfo = { - money2020: { + money2020?: { /** If the beginning demo should be shown */ isBeginningDemo?: boolean; }; From 588ac688083ca9db8bb25b7e2e3d8a4763a1c103 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 00:03:25 +0530 Subject: [PATCH 027/120] 1 - show report preview in offline on mr deletion --- src/libs/actions/IOU.js | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ed43569c360a..8b20eb84c69a 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2004,6 +2004,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView const updatedReportAction = { [reportAction.reportActionID]: { pendingAction: shouldShowDeletedRequestMessage ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + childReportID: shouldDeleteTransactionThread ? null : reportAction.childReportID, previousMessage: reportAction.message, message: [ { @@ -2027,9 +2028,9 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView iouReportLastMessageText.length === 0 && !ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && (!transactionThreadID || shouldDeleteTransactionThread); // STEP 4: Update the iouReport and reportPreview with new totals and messages if it wasn't deleted - let updatedIOUReport = null; - let updatedReportPreviewAction = null; - if (!shouldDeleteIOUReport) { + let updatedIOUReport = {...iouReport}; + let updatedReportPreviewAction = {...reportPreviewAction}; + updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; if (ReportUtils.isExpenseReport(iouReport)) { updatedIOUReport = {...iouReport}; @@ -2048,7 +2049,6 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView updatedIOUReport.lastMessageText = iouReportLastMessageText; updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); - updatedReportPreviewAction = {...reportPreviewAction}; const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', @@ -2059,7 +2059,6 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView if (reportPreviewAction.childMoneyRequestCount > 0) { updatedReportPreviewAction.childMoneyRequestCount = reportPreviewAction.childMoneyRequestCount - 1; } - } // STEP 5: Build Onyx data const optimisticData = [ @@ -2083,12 +2082,12 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView ] : []), { - onyxMethod: shouldDeleteIOUReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, - value: shouldDeleteIOUReport ? null : updatedReportAction, + value: updatedReportAction, }, { - onyxMethod: shouldDeleteIOUReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, value: updatedIOUReport, }, @@ -2107,7 +2106,6 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView value: { hasOutstandingIOU: false, hasOutstandingChildRequest: false, - iouReportID: null, lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}).lastMessageText, lastVisibleActionCreated: lodashGet(ReportActionsUtils.getLastVisibleAction(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}), 'created'), }, @@ -2124,6 +2122,25 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView [reportAction.reportActionID]: {pendingAction: null}, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, + value: { + [reportPreviewAction.reportActionID]: shouldDeleteIOUReport ? null : { + pendingAction: null, + errors: null, + }, + }, + }, + ...(shouldDeleteIOUReport + ? [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: null, + }, + ] + : []), ]; const failureData = [ From 84b623310a0e4ba5beab632f471911315e42f562 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 00:53:02 +0530 Subject: [PATCH 028/120] 2 - fix for TBD on last MR deletion with comments --- src/libs/ReportUtils.ts | 3 +-- src/libs/TransactionUtils.ts | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1266f145de30..f50ad7780ec4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -898,8 +898,7 @@ function hasSingleParticipant(report: OnyxEntry): boolean { * */ function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): boolean { - const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); - return allTransactions.every((transaction) => TransactionUtils.isDistanceRequest(transaction)); + return TransactionUtils.areAllDistanceRequestTransactions(iouReportID); } /** diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index d03235a637c7..74c79430e3d1 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -404,6 +404,12 @@ function getAllReportTransactions(reportID?: string): Transaction[] { return transactions.filter((transaction) => `${transaction.reportID}` === `${reportID}`); } +function areAllDistanceRequestTransactions(reportID?: string): boolean { + const reportTransactions: Transaction[] = getAllReportTransactions(reportID); + const areAllDistanceRequestTransactions = reportTransactions.every((transaction) => isDistanceRequest(transaction)); + return reportTransactions.length > 0 && areAllDistanceRequestTransactions; +} + function waypointHasValidAddress(waypoint: RecentWaypoint | Waypoint): boolean { return !!waypoint?.address?.trim(); } @@ -480,6 +486,7 @@ export { getTag, getLinkedTransaction, getAllReportTransactions, + areAllDistanceRequestTransactions, hasReceipt, hasEReceipt, hasRoute, From 337813bbe70bcf6bd4f84aaa04f65b747f6164db Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 01:23:04 +0530 Subject: [PATCH 029/120] 3 - display correct total amount when MR exist --- src/libs/IOUUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index afbbcc2684a0..51c131281676 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -44,7 +44,6 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu // Make a copy so we don't mutate the original object const iouReportUpdate: Report = {...iouReport}; - if (iouReportUpdate.total) { if (actorAccountID === iouReport.ownerAccountID) { iouReportUpdate.total += isDeleting ? -amount : amount; } else { @@ -59,7 +58,6 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu } iouReportUpdate.hasOutstandingIOU = iouReportUpdate.total !== 0; - } return iouReportUpdate; } From 0a839489da5d858ace97a77987063133c7f986d4 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 01:43:34 +0530 Subject: [PATCH 030/120] 4 - reply link in report preview opens iou report --- src/libs/ReportUtils.ts | 2 ++ src/pages/home/report/ReportActionItem.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ac712c50e641..5e8a36515230 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -150,6 +150,7 @@ type OptimisticReportPreview = Pick< | 'childLastMoneyRequestComment' | 'childRecentReceiptTransactionIDs' | 'whisperedToAccountIDs' + | 'childReportID' > & {reportID?: string; accountID?: number}; type UpdateReportPreview = Pick< @@ -2919,6 +2920,7 @@ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: accountID: iouReport?.managerID ?? 0, // The preview is initially whispered if created with a receipt, so the actor is the current user as well actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.managerID ?? 0, + childReportID: iouReport?.reportID, childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, childRecentReceiptTransactionIDs: hasReceipt && isNotEmptyObject(transaction) ? {[transaction?.transactionID ?? '']: created} : undefined, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index f850daaa1ffb..e4219220c464 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -524,7 +524,7 @@ function ReportActionItem(props) { {shouldDisplayThreadReplies && ( Date: Fri, 8 Dec 2023 02:06:07 +0530 Subject: [PATCH 031/120] safe logic for issue-3 as per proposal --- src/libs/IOUUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 51c131281676..f8611bdf671e 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -43,7 +43,7 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu // Make a copy so we don't mutate the original object const iouReportUpdate: Report = {...iouReport}; - + if (typeof iouReportUpdate.total !== 'undefined') { if (actorAccountID === iouReport.ownerAccountID) { iouReportUpdate.total += isDeleting ? -amount : amount; } else { @@ -58,6 +58,7 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu } iouReportUpdate.hasOutstandingIOU = iouReportUpdate.total !== 0; + } return iouReportUpdate; } From efbe915914189e9cb337b27d071dff8e14211d5b Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 02:26:44 +0530 Subject: [PATCH 032/120] lint fixes --- src/libs/TransactionUtils.ts | 4 ++-- src/libs/actions/IOU.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 74c79430e3d1..664d9bcd8a23 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -406,8 +406,8 @@ function getAllReportTransactions(reportID?: string): Transaction[] { function areAllDistanceRequestTransactions(reportID?: string): boolean { const reportTransactions: Transaction[] = getAllReportTransactions(reportID); - const areAllDistanceRequestTransactions = reportTransactions.every((transaction) => isDistanceRequest(transaction)); - return reportTransactions.length > 0 && areAllDistanceRequestTransactions; + const areAllDistanceRequests = reportTransactions.every((transaction) => isDistanceRequest(transaction)); + return reportTransactions.length > 0 && areAllDistanceRequests; } function waypointHasValidAddress(waypoint: RecentWaypoint | Waypoint): boolean { diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 8b20eb84c69a..b2637e618fde 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2029,7 +2029,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView // STEP 4: Update the iouReport and reportPreview with new totals and messages if it wasn't deleted let updatedIOUReport = {...iouReport}; - let updatedReportPreviewAction = {...reportPreviewAction}; + const updatedReportPreviewAction = {...reportPreviewAction}; updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; if (ReportUtils.isExpenseReport(iouReport)) { updatedIOUReport = {...iouReport}; From 92bdf81d53509aa454c152c3696634f8bafbf091 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 13:00:43 +0530 Subject: [PATCH 033/120] jest fixes --- src/libs/actions/IOU.js | 6 ++++-- tests/actions/IOUTest.js | 11 +++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index b2637e618fde..0b58b1f78de4 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2116,10 +2116,12 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView const successData = [ { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: shouldDeleteIOUReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, value: { - [reportAction.reportActionID]: {pendingAction: null}, + [reportAction.reportActionID]: shouldDeleteIOUReport ? null : { + pendingAction: null + }, }, }, { diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 18793e88d624..eb373c653148 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -2003,7 +2003,8 @@ describe('actions/IOU', () => { }); createIOUAction = _.find(reportActionsForReport, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); - expect(createIOUAction).toBeFalsy(); + // Then the IOU Action should be truthy for offline support. + expect(createIOUAction).toBeTruthy(); // Then we check if the transaction is removed from the transactions collection const t = await new Promise((resolve) => { @@ -2021,6 +2022,7 @@ describe('actions/IOU', () => { // Given fetch operations are resumed fetch.resume(); + await waitForBatchedUpdates(); // Then we recheck the IOU report action from the report actions collection reportActionsForReport = await new Promise((resolve) => { @@ -2071,11 +2073,12 @@ describe('actions/IOU', () => { }); }); - // Then the report should be falsy (indicating deletion) - expect(report).toBeFalsy(); + // Then the report should be truthy for offline support + expect(report).toBeTruthy(); // Given the resumed fetch state fetch.resume(); + await waitForBatchedUpdates(); report = await new Promise((resolve) => { const connectionID = Onyx.connect({ @@ -2088,7 +2091,7 @@ describe('actions/IOU', () => { }); }); - // Then the report should still be falsy (confirming deletion persisted) + // Then the report should be falsy so that there is no trace of the money request. expect(report).toBeFalsy(); }); From 6b954ec0e152b205e735ebc00bafc2c474003c9e Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 15:22:46 +0530 Subject: [PATCH 034/120] added comments and minor fixes --- src/libs/IOUUtils.ts | 7 +++++-- src/libs/TransactionUtils.ts | 4 ++++ src/libs/actions/IOU.js | 1 - 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index f8611bdf671e..6efe7df2fc2f 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -43,7 +43,11 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu // Make a copy so we don't mutate the original object const iouReportUpdate: Report = {...iouReport}; - if (typeof iouReportUpdate.total !== 'undefined') { + + // Let us ensure a valid value before updating the total amount. + if (!iouReportUpdate.total) { + iouReportUpdate.total = 0; + } if (actorAccountID === iouReport.ownerAccountID) { iouReportUpdate.total += isDeleting ? -amount : amount; } else { @@ -58,7 +62,6 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu } iouReportUpdate.hasOutstandingIOU = iouReportUpdate.total !== 0; - } return iouReportUpdate; } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 664d9bcd8a23..a44e1b6a47ef 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -404,6 +404,10 @@ function getAllReportTransactions(reportID?: string): Transaction[] { return transactions.filter((transaction) => `${transaction.reportID}` === `${reportID}`); } +/** + * Check if all the transactions in the iou report are distance requests. If so, return true. Else, return false. + * + */ function areAllDistanceRequestTransactions(reportID?: string): boolean { const reportTransactions: Transaction[] = getAllReportTransactions(reportID); const areAllDistanceRequests = reportTransactions.every((transaction) => isDistanceRequest(transaction)); diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 0b58b1f78de4..7b86fa1f8cb6 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2004,7 +2004,6 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView const updatedReportAction = { [reportAction.reportActionID]: { pendingAction: shouldShowDeletedRequestMessage ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - childReportID: shouldDeleteTransactionThread ? null : reportAction.childReportID, previousMessage: reportAction.message, message: [ { From 8e55cca513ff56b47031a8332a4cba849e46722f Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 8 Dec 2023 11:25:27 +0100 Subject: [PATCH 035/120] Move updates to global.d.ts file --- src/types/global.d.ts | 6 ++++++ src/types/modules/window.d.ts | 10 ---------- 2 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 src/types/modules/window.d.ts diff --git a/src/types/global.d.ts b/src/types/global.d.ts index a807b4328d50..f131eb5ef849 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -22,3 +22,9 @@ declare module '*.lottie' { } declare module 'react-native-device-info/jest/react-native-device-info-mock'; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +interface Window { + enableMemoryOnlyKeys: () => void; + disableMemoryOnlyKeys: () => void; +} diff --git a/src/types/modules/window.d.ts b/src/types/modules/window.d.ts deleted file mode 100644 index 1910c26768f5..000000000000 --- a/src/types/modules/window.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare global { - // eslint-disable-next-line @typescript-eslint/consistent-type-definitions - interface Window { - enableMemoryOnlyKeys: () => void; - disableMemoryOnlyKeys: () => void; - } -} - -// We used the export {} line to mark this file as an external module -export {}; From 43d403c13041d05d2dc6bc36f31387999784197c Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 16:26:13 +0530 Subject: [PATCH 036/120] prettier fix --- src/libs/IOUUtils.ts | 26 ++++++------ src/libs/TransactionUtils.ts | 2 +- src/libs/actions/IOU.js | 80 +++++++++++++++++++----------------- 3 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 6efe7df2fc2f..9671808e5c7d 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -45,23 +45,23 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu const iouReportUpdate: Report = {...iouReport}; // Let us ensure a valid value before updating the total amount. - if (!iouReportUpdate.total) { + if (!iouReportUpdate.total) { iouReportUpdate.total = 0; } - if (actorAccountID === iouReport.ownerAccountID) { - iouReportUpdate.total += isDeleting ? -amount : amount; - } else { - iouReportUpdate.total += isDeleting ? amount : -amount; - } + if (actorAccountID === iouReport.ownerAccountID) { + iouReportUpdate.total += isDeleting ? -amount : amount; + } else { + iouReportUpdate.total += isDeleting ? amount : -amount; + } - if (iouReportUpdate.total < 0) { - // The total sign has changed and hence we need to flip the manager and owner of the report. - iouReportUpdate.ownerAccountID = iouReport.managerID; - iouReportUpdate.managerID = iouReport.ownerAccountID; - iouReportUpdate.total = -iouReportUpdate.total; - } + if (iouReportUpdate.total < 0) { + // The total sign has changed and hence we need to flip the manager and owner of the report. + iouReportUpdate.ownerAccountID = iouReport.managerID; + iouReportUpdate.managerID = iouReport.ownerAccountID; + iouReportUpdate.total = -iouReportUpdate.total; + } - iouReportUpdate.hasOutstandingIOU = iouReportUpdate.total !== 0; + iouReportUpdate.hasOutstandingIOU = iouReportUpdate.total !== 0; return iouReportUpdate; } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index a44e1b6a47ef..60104a030370 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -408,7 +408,7 @@ function getAllReportTransactions(reportID?: string): Transaction[] { * Check if all the transactions in the iou report are distance requests. If so, return true. Else, return false. * */ -function areAllDistanceRequestTransactions(reportID?: string): boolean { +function areAllDistanceRequestTransactions(reportID?: string): boolean { const reportTransactions: Transaction[] = getAllReportTransactions(reportID); const areAllDistanceRequests = reportTransactions.every((transaction) => isDistanceRequest(transaction)); return reportTransactions.length > 0 && areAllDistanceRequests; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7b86fa1f8cb6..a55933ac9318 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2030,34 +2030,34 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView let updatedIOUReport = {...iouReport}; const updatedReportPreviewAction = {...reportPreviewAction}; updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; - if (ReportUtils.isExpenseReport(iouReport)) { - updatedIOUReport = {...iouReport}; + if (ReportUtils.isExpenseReport(iouReport)) { + updatedIOUReport = {...iouReport}; - // Because of the Expense reports are stored as negative values, we add the total from the amount - updatedIOUReport.total += TransactionUtils.getAmount(transaction, true); - } else { - updatedIOUReport = IOUUtils.updateIOUOwnerAndTotal( - iouReport, - reportAction.actorAccountID, - TransactionUtils.getAmount(transaction, false), - TransactionUtils.getCurrency(transaction), - true, - ); - } + // Because of the Expense reports are stored as negative values, we add the total from the amount + updatedIOUReport.total += TransactionUtils.getAmount(transaction, true); + } else { + updatedIOUReport = IOUUtils.updateIOUOwnerAndTotal( + iouReport, + reportAction.actorAccountID, + TransactionUtils.getAmount(transaction, false), + TransactionUtils.getCurrency(transaction), + true, + ); + } - updatedIOUReport.lastMessageText = iouReportLastMessageText; - updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); + updatedIOUReport.lastMessageText = iouReportLastMessageText; + updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); - const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { - payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', - amount: CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency), - }); - updatedReportPreviewAction.message[0].text = messageText; - updatedReportPreviewAction.message[0].html = messageText; - if (reportPreviewAction.childMoneyRequestCount > 0) { - updatedReportPreviewAction.childMoneyRequestCount = reportPreviewAction.childMoneyRequestCount - 1; - } + const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); + const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { + payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', + amount: CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency), + }); + updatedReportPreviewAction.message[0].text = messageText; + updatedReportPreviewAction.message[0].html = messageText; + if (reportPreviewAction.childMoneyRequestCount > 0) { + updatedReportPreviewAction.childMoneyRequestCount = reportPreviewAction.childMoneyRequestCount - 1; + } // STEP 5: Build Onyx data const optimisticData = [ @@ -2118,29 +2118,33 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView onyxMethod: shouldDeleteIOUReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, value: { - [reportAction.reportActionID]: shouldDeleteIOUReport ? null : { - pendingAction: null - }, + [reportAction.reportActionID]: shouldDeleteIOUReport + ? null + : { + pendingAction: null, + }, }, }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, value: { - [reportPreviewAction.reportActionID]: shouldDeleteIOUReport ? null : { - pendingAction: null, - errors: null, - }, + [reportPreviewAction.reportActionID]: shouldDeleteIOUReport + ? null + : { + pendingAction: null, + errors: null, + }, }, }, ...(shouldDeleteIOUReport ? [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: null, - }, - ] + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: null, + }, + ] : []), ]; From 18e0e5e6ace9088a2fae1350144b304a510dbb77 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Sat, 9 Dec 2023 10:21:58 +0530 Subject: [PATCH 037/120] merge fix and usage of nullish coalescence --- src/libs/IOUUtils.ts | 5 ++--- src/libs/ReportUtils.ts | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index e7285d25fba0..b608c4da5268 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -83,9 +83,8 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu const iouReportUpdate: Report = {...iouReport}; // Let us ensure a valid value before updating the total amount. - if (!iouReportUpdate.total) { - iouReportUpdate.total = 0; - } + iouReportUpdate.total = iouReportUpdate.total ?? 0; + if (actorAccountID === iouReport.ownerAccountID) { iouReportUpdate.total += isDeleting ? -amount : amount; } else { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ed8616ea86c4..2431e85a881b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2927,11 +2927,10 @@ function buildOptimisticReportPreview( accountID: iouReport?.managerID ?? 0, // The preview is initially whispered if created with a receipt, so the actor is the current user as well actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.managerID ?? 0, - childReportID: iouReport?.reportID, + childReportID: childReportID ?? iouReport?.reportID, childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, childRecentReceiptTransactionIDs: hasReceipt && isNotEmptyObject(transaction) ? {[transaction?.transactionID ?? '']: created} : undefined, - childReportID, whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID ?? -1] : [], }; } From 59816df7f3f8b760b554a06bd464bafce5d1fb4c Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Sat, 9 Dec 2023 10:30:36 +0530 Subject: [PATCH 038/120] lint fix --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2431e85a881b..d9049b4939f3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -151,7 +151,6 @@ type OptimisticReportPreview = Pick< | 'childRecentReceiptTransactionIDs' | 'childReportID' | 'whisperedToAccountIDs' - | 'childReportID' > & {reportID?: string; accountID?: number}; type UpdateReportPreview = Pick< From 2786593e43064db6be6bead4097fbc3e6abcf261 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Sat, 9 Dec 2023 10:43:24 +0530 Subject: [PATCH 039/120] prettier fix --- src/libs/IOUUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index b608c4da5268..6e038da66ad0 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -84,7 +84,7 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu // Let us ensure a valid value before updating the total amount. iouReportUpdate.total = iouReportUpdate.total ?? 0; - + if (actorAccountID === iouReport.ownerAccountID) { iouReportUpdate.total += isDeleting ? -amount : amount; } else { From 4bcd51db51218e0e1665c9f64a50c161238d1719 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Sat, 9 Dec 2023 15:19:26 +0530 Subject: [PATCH 040/120] lhn message fix for report preview --- src/libs/ReportActionsUtils.ts | 19 ++++++++++++------- src/libs/actions/IOU.js | 5 +++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 93d747c7018e..2816db9a485e 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -355,6 +355,13 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: return !isDeleted || isPending || isDeletedParentAction(reportAction) || isReversedTransaction(reportAction); } +/** + * A helper method to identify if the message is deleted or not. + */ +function isMessageDeleted(reportAction: OnyxEntry): boolean { + return reportAction?.message?.[0]?.isDeletedParentAction ?? false; +} + /** * Checks if a reportAction is fit for display as report last action, meaning that * it satisfies shouldReportActionBeVisible, it's not whisper action and not deleted. @@ -368,6 +375,11 @@ function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxEntry): boole return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !!reportAction.originalMessage?.taskReportID; } -/** - * A helper method to identify if the message is deleted or not. - */ -function isMessageDeleted(reportAction: OnyxEntry): boolean { - return reportAction?.message?.[0]?.isDeletedParentAction ?? false; -} - /** * Returns the number of money requests associated with a report preview */ diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 90c7566c693b..a886ad4be53e 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2202,6 +2202,10 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView updatedIOUReport.lastMessageText = iouReportLastMessageText; updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); + if (shouldDeleteIOUReport) { + // In offline scenario, let us ensure that LHN does not consider this action for display text. + updatedReportPreviewAction.message[0].isDeletedParentAction = true; + } const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', @@ -2209,6 +2213,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView }); updatedReportPreviewAction.message[0].text = messageText; updatedReportPreviewAction.message[0].html = messageText; + if (reportPreviewAction.childMoneyRequestCount > 0) { updatedReportPreviewAction.childMoneyRequestCount = reportPreviewAction.childMoneyRequestCount - 1; } From 127b84b9267b2235fe15073394a1c278975e013a Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Sat, 9 Dec 2023 17:30:09 +0530 Subject: [PATCH 041/120] console error fix on ios native device --- src/libs/actions/IOU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index a886ad4be53e..64c1802e2e54 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2274,7 +2274,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView const successData = [ { - onyxMethod: shouldDeleteIOUReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, value: { [reportAction.reportActionID]: shouldDeleteIOUReport From a96cdbbec47dbaa72ba62dfa374f93b3699ebd5f Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 12 Dec 2023 09:49:53 +0100 Subject: [PATCH 042/120] Fix ts issues --- src/types/onyx/OriginalMessage.ts | 2 +- src/types/onyx/Report.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index f30dcb750c14..f68cf06310c5 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -234,4 +234,4 @@ type OriginalMessage = | OriginalMessageMoved; export default OriginalMessage; -export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed, OriginalMessageActionName, OriginalMessageCreated, ChangeLog, OriginalMessageIOU, OriginalMessageCreated}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed, OriginalMessageActionName, OriginalMessageCreated, ChangeLog, OriginalMessageIOU}; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 70665210547f..25c544f6f5c3 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -146,9 +146,6 @@ type Report = { /** Pending fields for the report */ pendingFields?: Record; - /** Pending action for the report */ - pendingAction?: OnyxCommon.PendingAction | null; - /** The ID of the preexisting report (it is possible that we optimistically created a Report for which a report already exists) */ preexistingReportID?: string; From 5641af0b4752f2a0e010ecad60d3409de25433f4 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 12 Dec 2023 09:54:56 +0100 Subject: [PATCH 043/120] Rename type --- src/libs/actions/Card.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 82b9cbc47e7c..e5ae09d5b28a 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -5,7 +5,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Response} from '@src/types/onyx'; -type Reason = 'damaged' | 'stolen'; +type ReplacementReason = 'damaged' | 'stolen'; function reportVirtualExpensifyCardFraud(cardID: number) { const optimisticData: OnyxUpdate[] = [ @@ -54,7 +54,7 @@ function reportVirtualExpensifyCardFraud(cardID: number) { * @param cardId - id of the card that is going to be replaced * @param reason - reason for replacement */ -function requestReplacementExpensifyCard(cardId: number, reason: Reason) { +function requestReplacementExpensifyCard(cardId: number, reason: ReplacementReason) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, From 84e2fd5212b2b717f7a124768e2417f9537d26c5 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 12 Dec 2023 17:29:16 +0100 Subject: [PATCH 044/120] Add comments to window methods --- src/types/global.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/global.d.ts b/src/types/global.d.ts index f131eb5ef849..1df100f67fe3 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -25,6 +25,9 @@ declare module 'react-native-device-info/jest/react-native-device-info-mock'; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface Window { + // Method to enable memory only keys feature enableMemoryOnlyKeys: () => void; + + // Method to disable memory only keys feature disableMemoryOnlyKeys: () => void; } From acfb595794d57e0e5b06c810204ad8d9a3b26111 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Thu, 14 Dec 2023 16:41:22 +0530 Subject: [PATCH 045/120] redundant code removal and merge fix --- src/libs/IOUUtils.ts | 2 -- src/pages/home/report/ReportActionItem.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 6e038da66ad0..03a5c41f4fa4 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -98,8 +98,6 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu iouReportUpdate.total = -iouReportUpdate.total; } - iouReportUpdate.hasOutstandingIOU = iouReportUpdate.total !== 0; - return iouReportUpdate; } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index ffb8e96edb2f..2e888a5471b8 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -529,7 +529,7 @@ function ReportActionItem(props) { {shouldDisplayThreadReplies && ( Date: Thu, 14 Dec 2023 14:51:32 +0100 Subject: [PATCH 046/120] list refactor --- .../SelectionList/BaseSelectionList.js | 2 + src/pages/SearchPage.js | 102 +++++++++++++----- 2 files changed, 76 insertions(+), 28 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 6f53679f28d3..03ca6f6646e9 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -39,6 +39,7 @@ function BaseSelectionList({ textInputLabel = '', textInputPlaceholder = '', textInputValue = '', + textInputHint = '', textInputMaxLength, inputMode = CONST.INPUT_MODE.TEXT, onChangeText, @@ -405,6 +406,7 @@ function BaseSelectionList({ }} label={textInputLabel} accessibilityLabel={textInputLabel} + hint={textInputHint} role={CONST.ACCESSIBILITY_ROLE.TEXT} value={textInputValue} placeholder={textInputPlaceholder} diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 77a9ddf930ca..56ca5a8724b1 100644 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -4,19 +4,25 @@ import React, {useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import OptionsSelector from '@components/OptionsSelector'; +import Icon from '@components/Icon'; +import {Info} from '@components/Icon/Expensicons'; +import {PressableWithoutFeedback} from '@components/Pressable'; import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import * as ReportUtils from '@libs/ReportUtils'; +import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import personalDetailsPropType from './personalDetailsPropType'; import reportPropTypes from './reportPropTypes'; @@ -62,8 +68,11 @@ function useDebouncedState(initialValue, delay) { function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); - const network = useNetwork(); + const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); + const theme = useTheme(); + + const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('', 75); @@ -83,7 +92,6 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { userToInvite, headerMessage, } = useMemo(() => { - console.log('updateOptions', isScreenTransitionEnd); if (!isScreenTransitionEnd) { return { recentReports: {}, @@ -98,7 +106,6 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { }, [debouncedSearchValue, reports, personalDetails, betas, isScreenTransitionEnd]); const sections = useMemo(() => { - console.log('updateSections'); const newSections = []; let indexOffset = 0; @@ -150,21 +157,47 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { }; const handleScreenTransitionEnd = () => { - console.log('handleScreenTransitionEnd'); setIsScreenTransitionEnd(true); }; const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); - console.log('render', { - isScreenTransitionEnd, - isOptionsDataReady, - sections: sections.length, - recentReports, - localPersonalDetails, - userToInvite, - headerMessage, - }); + const footerRender = ( + + { + Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND)); + }} + style={[ + themeStyles.p5, + themeStyles.w100, + themeStyles.br2, + themeStyles.highlightBG, + themeStyles.flexRow, + themeStyles.justifyContentBetween, + themeStyles.alignItemsCenter, + {gap: 10}, + ]} + accessibilityLabel="referral" + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + > + + {translate(`referralProgram.${CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}.buttonText1`)} + + {translate(`referralProgram.${CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}.buttonText2`)} + + + + + + ); return ( ( <> - - + + {/* */} )} From 71b50568ce53b5d556b34f3edab60c88b227f273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Thu, 14 Dec 2023 15:48:43 +0100 Subject: [PATCH 047/120] moved useDebouncedState hook to an independent file --- src/hooks/useDebouncedState.ts | 34 ++++++++++++++++++++++++++++++++++ src/pages/SearchPage.js | 20 ++------------------ 2 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 src/hooks/useDebouncedState.ts diff --git a/src/hooks/useDebouncedState.ts b/src/hooks/useDebouncedState.ts new file mode 100644 index 000000000000..601cd05de3cd --- /dev/null +++ b/src/hooks/useDebouncedState.ts @@ -0,0 +1,34 @@ +import {debounce} from 'lodash'; +import {useEffect, useRef, useState} from 'react'; + +/** + * A React hook that provides a state and its debounced version. + * + * @param initialValue - The initial value of the state. + * @param delay - The debounce delay in milliseconds. Defaults to 100ms. + * @returns A tuple containing: + * - The current state value. + * - The debounced state value. + * - A function to set both the current and debounced state values. + * + * @template T The type of the state value. + * + * @example + * const [value, debouncedValue, setValue] = useDebouncedState("", 300); + */ +function useDebouncedState(initialValue: T, delay = 100): [T, T, (value: T) => void] { + const [value, setValue] = useState(initialValue); + const [debouncedValue, setDebouncedValue] = useState(initialValue); + const debouncedSetDebouncedValue = useRef(debounce(setDebouncedValue, delay)).current; + + useEffect(() => () => debouncedSetDebouncedValue.cancel(), [debouncedSetDebouncedValue]); + + const handleSetValue = (newValue: T) => { + setValue(newValue); + debouncedSetDebouncedValue(newValue); + }; + + return [value, debouncedValue, handleSetValue]; +} + +export default useDebouncedState; diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 56ca5a8724b1..896c26519b5c 100644 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -1,6 +1,5 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; -import React, {useEffect, useMemo, useRef, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -10,6 +9,7 @@ import {PressableWithoutFeedback} from '@components/Pressable'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import Text from '@components/Text'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import Navigation from '@libs/Navigation/Navigation'; @@ -49,22 +49,6 @@ const defaultProps = { isSearchingForReports: false, }; -// custom hook that handles debouncing the search value using lodash debounce -function useDebouncedState(initialValue, delay) { - const [value, setValue] = useState(initialValue); - const [debouncedValue, setDebouncedValue] = useState(initialValue); - const debouncedSetDebouncedValue = useRef(_.debounce(setDebouncedValue, delay)).current; - - useEffect(() => debouncedSetDebouncedValue.cancel, [debouncedSetDebouncedValue]); - - const handleSetValue = (newValue) => { - setValue(newValue); - debouncedSetDebouncedValue(newValue); - }; - - return [value, debouncedValue, handleSetValue]; -} - function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); From 186bfb26312cf9e5a01738da8bc8922ae3d6afe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Thu, 14 Dec 2023 15:50:44 +0100 Subject: [PATCH 048/120] removed comment and unused param --- src/pages/SearchPage.js | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 896c26519b5c..65e03ae7577a 100644 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -37,19 +37,15 @@ const propTypes = { /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), - - /** Whether we are searching for reports in the server */ - isSearchingForReports: PropTypes.bool, }; const defaultProps = { betas: [], personalDetails: {}, reports: {}, - isSearchingForReports: false, }; -function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { +function SearchPage({betas, personalDetails, reports}) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -206,24 +202,6 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { showLoadingPlaceholder={!didScreenTransitionEnd || !isOptionsDataReady} footerContent={footerRender} /> - {/* */} )} From e859ff125f55a9f0ee9f3e45873302787794a6a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Thu, 14 Dec 2023 16:11:26 +0100 Subject: [PATCH 049/120] removed old SearchPage component --- src/pages/SearchPage.old.js | 243 ------------------------------------ 1 file changed, 243 deletions(-) delete mode 100755 src/pages/SearchPage.old.js diff --git a/src/pages/SearchPage.old.js b/src/pages/SearchPage.old.js deleted file mode 100755 index 5d111e7c181f..000000000000 --- a/src/pages/SearchPage.old.js +++ /dev/null @@ -1,243 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {Component} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import OptionsSelector from '@components/OptionsSelector'; -import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import compose from '@libs/compose'; -import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import Performance from '@libs/Performance'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as Report from '@userActions/Report'; -import Timing from '@userActions/Timing'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import personalDetailsPropType from './personalDetailsPropType'; -import reportPropTypes from './reportPropTypes'; - -const propTypes = { - /* Onyx Props */ - - /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), - - /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - - /** Window Dimensions Props */ - ...windowDimensionsPropTypes, - - ...withLocalizePropTypes, - - /** Network info */ - network: networkPropTypes, - - /** Whether we are searching for reports in the server */ - isSearchingForReports: PropTypes.bool, - ...withThemeStylesPropTypes, -}; - -const defaultProps = { - betas: [], - personalDetails: {}, - reports: {}, - network: {}, - isSearchingForReports: false, -}; - -class SearchPage extends Component { - constructor(props) { - super(props); - - Timing.start(CONST.TIMING.SEARCH_RENDER); - Performance.markStart(CONST.TIMING.SEARCH_RENDER); - - this.searchRendered = this.searchRendered.bind(this); - this.selectReport = this.selectReport.bind(this); - this.onChangeText = this.onChangeText.bind(this); - this.updateOptions = this.updateOptions.bind(this); - this.debouncedUpdateOptions = _.debounce(this.updateOptions.bind(this), 75); - this.state = { - searchValue: '', - recentReports: {}, - personalDetails: {}, - userToInvite: {}, - }; - } - - componentDidUpdate(prevProps) { - if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { - return; - } - this.updateOptions(); - } - - onChangeText(searchValue = '') { - Report.searchInServer(searchValue); - this.setState({searchValue}, this.debouncedUpdateOptions); - } - - /** - * Returns the sections needed for the OptionsSelector - * - * @returns {Array} - */ - getSections() { - const sections = []; - let indexOffset = 0; - - if (this.state.recentReports.length > 0) { - sections.push({ - data: this.state.recentReports, - shouldShow: true, - indexOffset, - }); - indexOffset += this.state.recentReports.length; - } - - if (this.state.personalDetails.length > 0) { - sections.push({ - data: this.state.personalDetails, - shouldShow: true, - indexOffset, - }); - indexOffset += this.state.recentReports.length; - } - - if (this.state.userToInvite) { - sections.push({ - data: [this.state.userToInvite], - shouldShow: true, - indexOffset, - }); - } - - return sections; - } - - searchRendered() { - Timing.end(CONST.TIMING.SEARCH_RENDER); - Performance.markEnd(CONST.TIMING.SEARCH_RENDER); - } - - updateOptions() { - const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions( - this.props.reports, - this.props.personalDetails, - this.state.searchValue.trim(), - this.props.betas, - ); - this.setState({ - userToInvite, - recentReports, - personalDetails, - }); - } - - /** - * Reset the search value and redirect to the selected report - * - * @param {Object} option - */ - selectReport(option) { - if (!option) { - return; - } - - if (option.reportID) { - this.setState( - { - searchValue: '', - }, - () => { - Navigation.dismissModal(option.reportID); - }, - ); - } else { - Report.navigateToAndOpenReport([option.login]); - } - } - - render() { - const sections = this.getSections(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); - const headerMessage = OptionsListUtils.getHeaderMessage( - this.state.recentReports.length + this.state.personalDetails.length !== 0, - Boolean(this.state.userToInvite), - this.state.searchValue, - ); - - return ( - - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( - <> - - - - - - )} - - ); - } -} - -SearchPage.propTypes = propTypes; -SearchPage.defaultProps = defaultProps; -SearchPage.displayName = 'SearchPage'; - -export default compose( - withLocalize, - withWindowDimensions, - withNetwork(), - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, - }), - withThemeStyles, -)(SearchPage); From ad06affa9e14f0e4b76066360a4685931ec9e7c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 18 Dec 2023 11:02:50 +0100 Subject: [PATCH 050/120] fixed theme hooks import --- src/pages/SearchPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 65e03ae7577a..b693294bad64 100644 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -12,12 +12,12 @@ import Text from '@components/Text'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import * as ReportUtils from '@libs/ReportUtils'; -import useTheme from '@styles/themes/useTheme'; -import useThemeStyles from '@styles/useThemeStyles'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; From 486ae09632799a90800731e056f709575db64890 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Mon, 18 Dec 2023 13:27:00 +0100 Subject: [PATCH 051/120] Update comment --- src/types/global.d.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 1df100f67fe3..935c48942a68 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -23,11 +23,9 @@ declare module '*.lottie' { declare module 'react-native-device-info/jest/react-native-device-info-mock'; +// Global methods for Onyx key management for debugging purposes // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface Window { - // Method to enable memory only keys feature enableMemoryOnlyKeys: () => void; - - // Method to disable memory only keys feature disableMemoryOnlyKeys: () => void; } From ef63c515880e5851c0bcc0f87bc0a9067257d431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 18 Dec 2023 15:39:25 +0100 Subject: [PATCH 052/120] move to usePersonalDetails hook --- src/pages/SearchPage.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index b693294bad64..5e1a384fc4fe 100644 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; import {Info} from '@components/Icon/Expensicons'; +import {usePersonalDetails} from '@components/OnyxProvider'; import {PressableWithoutFeedback} from '@components/Pressable'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -23,7 +24,6 @@ import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import personalDetailsPropType from './personalDetailsPropType'; import reportPropTypes from './reportPropTypes'; const propTypes = { @@ -32,25 +32,22 @@ const propTypes = { /** Beta features list */ betas: PropTypes.arrayOf(PropTypes.string), - /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), }; const defaultProps = { betas: [], - personalDetails: {}, reports: {}, }; -function SearchPage({betas, personalDetails, reports}) { +function SearchPage({betas, reports}) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); const theme = useTheme(); + const personalDetails = usePersonalDetails(); const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; @@ -217,9 +214,6 @@ export default withOnyx({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, betas: { key: ONYXKEYS.BETAS, }, From af685f86f33f661a512171a3f476ea40d0936c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 18 Dec 2023 15:48:23 +0100 Subject: [PATCH 053/120] moved footer to seperate component --- src/pages/SearchPage/SearchPageFooter.tsx | 59 +++++++++++++++++++ .../{SearchPage.js => SearchPage/index.js} | 49 +-------------- 2 files changed, 62 insertions(+), 46 deletions(-) create mode 100644 src/pages/SearchPage/SearchPageFooter.tsx rename src/pages/{SearchPage.js => SearchPage/index.js} (76%) diff --git a/src/pages/SearchPage/SearchPageFooter.tsx b/src/pages/SearchPage/SearchPageFooter.tsx new file mode 100644 index 000000000000..69429962869b --- /dev/null +++ b/src/pages/SearchPage/SearchPageFooter.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import {Info} from '@components/Icon/Expensicons'; +import {PressableWithoutFeedback} from '@components/Pressable'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function SearchPageFooter() { + const themeStyles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + + return ( + + { + Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND)); + }} + style={[ + themeStyles.p5, + themeStyles.w100, + themeStyles.br2, + themeStyles.highlightBG, + themeStyles.flexRow, + themeStyles.justifyContentBetween, + themeStyles.alignItemsCenter, + {gap: 10}, + ]} + accessibilityLabel="referral" + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + > + + {translate(`referralProgram.${CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}.buttonText1`)} + + {translate(`referralProgram.${CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}.buttonText2`)} + + + + + + ); +} + +SearchPageFooter.displayName = 'SearchPageFooter'; + +export default SearchPageFooter; diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage/index.js similarity index 76% rename from src/pages/SearchPage.js rename to src/pages/SearchPage/index.js index 5e1a384fc4fe..becc30ada6a4 100644 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage/index.js @@ -3,28 +3,23 @@ import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import Icon from '@components/Icon'; -import {Info} from '@components/Icon/Expensicons'; import {usePersonalDetails} from '@components/OnyxProvider'; -import {PressableWithoutFeedback} from '@components/Pressable'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; -import Text from '@components/Text'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import * as ReportUtils from '@libs/ReportUtils'; +import reportPropTypes from '@pages/reportPropTypes'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import reportPropTypes from './reportPropTypes'; +import SearchPageFooter from './SearchPageFooter'; const propTypes = { /* Onyx Props */ @@ -46,7 +41,6 @@ function SearchPage({betas, reports}) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); - const theme = useTheme(); const personalDetails = usePersonalDetails(); const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; @@ -139,43 +133,6 @@ function SearchPage({betas, reports}) { const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); - const footerRender = ( - - { - Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND)); - }} - style={[ - themeStyles.p5, - themeStyles.w100, - themeStyles.br2, - themeStyles.highlightBG, - themeStyles.flexRow, - themeStyles.justifyContentBetween, - themeStyles.alignItemsCenter, - {gap: 10}, - ]} - accessibilityLabel="referral" - role={CONST.ACCESSIBILITY_ROLE.BUTTON} - > - - {translate(`referralProgram.${CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}.buttonText1`)} - - {translate(`referralProgram.${CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}.buttonText2`)} - - - - - - ); - return ( } /> From aba386f60b7380f96ef2923e96dfed4c63719c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 18 Dec 2023 15:49:56 +0100 Subject: [PATCH 054/120] moved searchRendered function outside of component scope --- src/pages/SearchPage/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index becc30ada6a4..b8ecd67dae96 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -36,6 +36,11 @@ const defaultProps = { reports: {}, }; +const searchRendered = () => { + Timing.end(CONST.TIMING.SEARCH_RENDER); + Performance.markEnd(CONST.TIMING.SEARCH_RENDER); +}; + function SearchPage({betas, reports}) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); @@ -122,11 +127,6 @@ function SearchPage({betas, reports}) { } }; - const searchRendered = () => { - Timing.end(CONST.TIMING.SEARCH_RENDER); - Performance.markEnd(CONST.TIMING.SEARCH_RENDER); - }; - const handleScreenTransitionEnd = () => { setIsScreenTransitionEnd(true); }; From f013c6b84ce7a6e99f9fce0f8c92db9f864292e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Tue, 19 Dec 2023 10:47:35 +0100 Subject: [PATCH 055/120] changed debouncedstate default delay value to val from const --- src/hooks/useDebouncedState.ts | 7 ++++--- src/pages/SearchPage/index.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/hooks/useDebouncedState.ts b/src/hooks/useDebouncedState.ts index 601cd05de3cd..66850676e00b 100644 --- a/src/hooks/useDebouncedState.ts +++ b/src/hooks/useDebouncedState.ts @@ -1,11 +1,12 @@ -import {debounce} from 'lodash'; +import debounce from 'lodash/debounce'; import {useEffect, useRef, useState} from 'react'; +import CONST from '@src/CONST'; /** * A React hook that provides a state and its debounced version. * * @param initialValue - The initial value of the state. - * @param delay - The debounce delay in milliseconds. Defaults to 100ms. + * @param delay - The debounce delay in milliseconds. Defaults to SEARCH_FOR_REPORTS_DEBOUNCE_TIME = 300ms. * @returns A tuple containing: * - The current state value. * - The debounced state value. @@ -16,7 +17,7 @@ import {useEffect, useRef, useState} from 'react'; * @example * const [value, debouncedValue, setValue] = useDebouncedState("", 300); */ -function useDebouncedState(initialValue: T, delay = 100): [T, T, (value: T) => void] { +function useDebouncedState(initialValue: T, delay = CONST.TIMING.SEARCH_FOR_REPORTS_DEBOUNCE_TIME): [T, T, (value: T) => void] { const [value, setValue] = useState(initialValue); const [debouncedValue, setDebouncedValue] = useState(initialValue); const debouncedSetDebouncedValue = useRef(debounce(setDebouncedValue, delay)).current; diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index b8ecd67dae96..8136299e22ed 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -50,7 +50,7 @@ function SearchPage({betas, reports}) { const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; - const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('', 75); + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); useEffect(() => { Timing.start(CONST.TIMING.SEARCH_RENDER); From 3407d737c24581a0e14df2ecbfda50f4e8c89f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Tue, 19 Dec 2023 10:56:18 +0100 Subject: [PATCH 056/120] rename perf function --- src/pages/SearchPage/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index 8136299e22ed..d7c95570c1e1 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -36,7 +36,7 @@ const defaultProps = { reports: {}, }; -const searchRendered = () => { +const setPerformanceTimersEnd = () => { Timing.end(CONST.TIMING.SEARCH_RENDER); Performance.markEnd(CONST.TIMING.SEARCH_RENDER); }; @@ -150,7 +150,7 @@ function SearchPage({betas, reports}) { textInputHint={offlineMessage} onChangeText={onChangeText} headerMessage={headerMessage} - onLayout={searchRendered} + onLayout={setPerformanceTimersEnd} autoFocus onSelectRow={selectReport} showLoadingPlaceholder={!didScreenTransitionEnd || !isOptionsDataReady} From c0c09a33e1c9003f92b4a9bf90a1bb68e23ff41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Tue, 19 Dec 2023 11:06:11 +0100 Subject: [PATCH 057/120] fallback to global empty array --- src/pages/SearchPage/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index d7c95570c1e1..a3bdab295280 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -144,7 +144,7 @@ function SearchPage({betas, reports}) { Date: Tue, 19 Dec 2023 11:37:46 +0100 Subject: [PATCH 058/120] search page footer instance --- src/pages/SearchPage/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index a3bdab295280..211f3622e06c 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -41,6 +41,8 @@ const setPerformanceTimersEnd = () => { Performance.markEnd(CONST.TIMING.SEARCH_RENDER); }; +const SearchPageFooterInstance = ; + function SearchPage({betas, reports}) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); @@ -154,7 +156,7 @@ function SearchPage({betas, reports}) { autoFocus onSelectRow={selectReport} showLoadingPlaceholder={!didScreenTransitionEnd || !isOptionsDataReady} - footerContent={} + footerContent={SearchPageFooterInstance} /> From cd60062f9074aa3ad8093504eedef647088edaad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Tue, 19 Dec 2023 15:13:51 +0100 Subject: [PATCH 059/120] removed old component --- src/pages/SearchPage.js | 232 ---------------------------------------- 1 file changed, 232 deletions(-) delete mode 100755 src/pages/SearchPage.js diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js deleted file mode 100755 index 28718d9aac1c..000000000000 --- a/src/pages/SearchPage.js +++ /dev/null @@ -1,232 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {Component} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import OptionsSelector from '@components/OptionsSelector'; -import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import compose from '@libs/compose'; -import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import Performance from '@libs/Performance'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as Report from '@userActions/Report'; -import Timing from '@userActions/Timing'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import personalDetailsPropType from './personalDetailsPropType'; -import reportPropTypes from './reportPropTypes'; - -const propTypes = { - /* Onyx Props */ - - /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), - - /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - - /** Window Dimensions Props */ - ...windowDimensionsPropTypes, - - ...withLocalizePropTypes, - - /** Network info */ - network: networkPropTypes, - - /** Whether we are searching for reports in the server */ - isSearchingForReports: PropTypes.bool, - ...withThemeStylesPropTypes, -}; - -const defaultProps = { - betas: [], - personalDetails: {}, - reports: {}, - network: {}, - isSearchingForReports: false, -}; - -class SearchPage extends Component { - constructor(props) { - super(props); - - Timing.start(CONST.TIMING.SEARCH_RENDER); - Performance.markStart(CONST.TIMING.SEARCH_RENDER); - - this.searchRendered = this.searchRendered.bind(this); - this.selectReport = this.selectReport.bind(this); - this.onChangeText = this.onChangeText.bind(this); - this.updateOptions = this.updateOptions.bind(this); - this.state = { - searchValue: '', - recentReports: {}, - personalDetails: {}, - userToInvite: {}, - }; - } - - componentDidUpdate(prevProps) { - if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { - return; - } - this.updateOptions(); - } - - onChangeText(searchValue = '') { - Report.searchInServer(searchValue); - this.setState({searchValue}, this.updateOptions); - } - - /** - * Returns the sections needed for the OptionsSelector - * - * @returns {Array} - */ - getSections() { - const sections = []; - let indexOffset = 0; - - if (this.state.recentReports.length > 0) { - sections.push({ - data: this.state.recentReports, - shouldShow: true, - indexOffset, - }); - indexOffset += this.state.recentReports.length; - } - - if (this.state.personalDetails.length > 0) { - sections.push({ - data: this.state.personalDetails, - shouldShow: true, - indexOffset, - }); - indexOffset += this.state.recentReports.length; - } - - if (this.state.userToInvite) { - sections.push({ - data: [this.state.userToInvite], - shouldShow: true, - indexOffset, - }); - } - - return sections; - } - - searchRendered() { - Timing.end(CONST.TIMING.SEARCH_RENDER); - Performance.markEnd(CONST.TIMING.SEARCH_RENDER); - } - - updateOptions() { - const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions( - this.props.reports, - this.props.personalDetails, - this.state.searchValue.trim(), - this.props.betas, - ); - this.setState({ - userToInvite, - recentReports, - personalDetails, - }); - } - - /** - * Reset the search value and redirect to the selected report - * - * @param {Object} option - */ - selectReport(option) { - if (!option) { - return; - } - - if (option.reportID) { - Navigation.dismissModal(option.reportID); - } else { - Report.navigateToAndOpenReport([option.login]); - } - } - - render() { - const sections = this.getSections(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); - const headerMessage = OptionsListUtils.getHeaderMessage( - this.state.recentReports.length + this.state.personalDetails.length !== 0, - Boolean(this.state.userToInvite), - this.state.searchValue, - ); - - return ( - - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( - <> - - - - - - )} - - ); - } -} - -SearchPage.propTypes = propTypes; -SearchPage.defaultProps = defaultProps; -SearchPage.displayName = 'SearchPage'; - -export default compose( - withLocalize, - withWindowDimensions, - withNetwork(), - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, - }), - withThemeStyles, -)(SearchPage); From 4d09449a39f8dfb5da667ea4603983970504b1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Tue, 19 Dec 2023 15:20:28 +0100 Subject: [PATCH 060/120] const migration --- src/hooks/useDebouncedState.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useDebouncedState.ts b/src/hooks/useDebouncedState.ts index 66850676e00b..3677c85f3081 100644 --- a/src/hooks/useDebouncedState.ts +++ b/src/hooks/useDebouncedState.ts @@ -6,7 +6,7 @@ import CONST from '@src/CONST'; * A React hook that provides a state and its debounced version. * * @param initialValue - The initial value of the state. - * @param delay - The debounce delay in milliseconds. Defaults to SEARCH_FOR_REPORTS_DEBOUNCE_TIME = 300ms. + * @param delay - The debounce delay in milliseconds. Defaults to SEARCH_OPTION_LIST_DEBOUNCE_TIME = 300ms. * @returns A tuple containing: * - The current state value. * - The debounced state value. @@ -17,7 +17,7 @@ import CONST from '@src/CONST'; * @example * const [value, debouncedValue, setValue] = useDebouncedState("", 300); */ -function useDebouncedState(initialValue: T, delay = CONST.TIMING.SEARCH_FOR_REPORTS_DEBOUNCE_TIME): [T, T, (value: T) => void] { +function useDebouncedState(initialValue: T, delay = CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME): [T, T, (value: T) => void] { const [value, setValue] = useState(initialValue); const [debouncedValue, setDebouncedValue] = useState(initialValue); const debouncedSetDebouncedValue = useRef(debounce(setDebouncedValue, delay)).current; From 2819e569a1b65a34f25e7d64f5f4f3da59e462e5 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Thu, 21 Dec 2023 11:21:02 +0530 Subject: [PATCH 061/120] review feedback changes --- src/libs/ReportActionsUtils.ts | 19 +++++++------------ src/libs/ReportUtils.ts | 3 ++- src/libs/TransactionUtils.ts | 11 ----------- src/libs/actions/IOU.js | 4 ---- 4 files changed, 9 insertions(+), 28 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index fdf313078598..9a3099ba6c02 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -379,13 +379,6 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: return !isDeleted || isPending || isDeletedParentAction(reportAction) || isReversedTransaction(reportAction); } -/** - * A helper method to identify if the message is deleted or not. - */ -function isMessageDeleted(reportAction: OnyxEntry): boolean { - return reportAction?.message?.[0]?.isDeletedParentAction ?? false; -} - /** * Checks if a reportAction is fit for display as report last action, meaning that * it satisfies shouldReportActionBeVisible, it's not whisper action and not deleted. @@ -399,11 +392,6 @@ function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxEntry): boole return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !!reportAction.originalMessage?.taskReportID; } +/** + * A helper method to identify if the message is deleted or not. + */ +function isMessageDeleted(reportAction: OnyxEntry): boolean { + return reportAction?.message?.[0]?.isDeletedParentAction ?? false; +} + /** * Returns the number of money requests associated with a report preview */ diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8fdc670c7b4d..6ff614c24c75 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -938,7 +938,8 @@ function hasSingleParticipant(report: OnyxEntry): boolean { * */ function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): boolean { - return TransactionUtils.areAllDistanceRequestTransactions(iouReportID); + const transactions = TransactionUtils.getAllReportTransactions(iouReportID); + return transactions.length > 0 && transactions.every((transaction) => TransactionUtils.isDistanceRequest(transaction)); } /** diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index d08beec0d14c..232c30658838 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -455,16 +455,6 @@ function getAllReportTransactions(reportID?: string): Transaction[] { return transactions.filter((transaction) => `${transaction.reportID}` === `${reportID}`); } -/** - * Check if all the transactions in the iou report are distance requests. If so, return true. Else, return false. - * - */ -function areAllDistanceRequestTransactions(reportID?: string): boolean { - const reportTransactions: Transaction[] = getAllReportTransactions(reportID); - const areAllDistanceRequests = reportTransactions.every((transaction) => isDistanceRequest(transaction)); - return reportTransactions.length > 0 && areAllDistanceRequests; -} - function waypointHasValidAddress(waypoint: RecentWaypoint | Waypoint): boolean { return !!waypoint?.address?.trim(); } @@ -546,7 +536,6 @@ export { getTag, getLinkedTransaction, getAllReportTransactions, - areAllDistanceRequestTransactions, hasReceipt, hasEReceipt, hasRoute, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 5d9232b56ee5..b8b06e46a0af 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2318,10 +2318,6 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView updatedIOUReport.lastMessageText = iouReportLastMessageText; updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); - if (shouldDeleteIOUReport) { - // In offline scenario, let us ensure that LHN does not consider this action for display text. - updatedReportPreviewAction.message[0].isDeletedParentAction = true; - } const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', From 118dd2d6c7b2663d4a1ba7fb24447fd58cccb1b3 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Wed, 13 Dec 2023 01:16:49 +0700 Subject: [PATCH 062/120] #32864 md syntax create task --- src/pages/home/ReportScreen.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 6afe7f92075b..7896b835b064 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -10,6 +10,7 @@ import DragAndDropProvider from '@components/DragAndDrop/Provider'; import MoneyReportHeader from '@components/MoneyReportHeader'; import MoneyRequestHeader from '@components/MoneyRequestHeader'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import {usePersonalDetails} from '@components/OnyxProvider'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ScreenWrapper from '@components/ScreenWrapper'; import TaskHeaderActionButton from '@components/TaskHeaderActionButton'; @@ -34,6 +35,7 @@ import reportMetadataPropTypes from '@pages/reportMetadataPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; import * as ComposerActions from '@userActions/Composer'; import * as Report from '@userActions/Report'; +import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -282,12 +284,26 @@ function ReportScreen({ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(accountManagerReportID)); }, [accountManagerReportID]); + const allPersonalDetails = usePersonalDetails(); + /** * @param {String} text */ const onSubmitComment = useCallback( (text) => { - Report.addComment(getReportID(route), text); + const taskRegex = /^\[\](?:\s+@([^\s@]+@[\w.-]+))?\s+(.+)/; + const match = text.match(taskRegex); + if (match) { + const email = match[1] ? match[1].trim() : undefined; // Email might be undefined if not captured + const title = match[2]; + let assignee = {}; + if (email) { + assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {}; + } + Task.createTaskAndNavigate(getReportID(route), title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID); + } else { + Report.addComment(getReportID(route), text); + } // We need to scroll to the bottom of the list after the comment is added const refID = setTimeout(() => { From 42c79a8d1e070f08e1456eef72f5b4ca9a629437 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Thu, 21 Dec 2023 12:58:24 +0530 Subject: [PATCH 063/120] Update src/libs/ReportUtils.ts Co-authored-by: Fedi Rajhi --- src/libs/ReportUtils.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6ff614c24c75..25b1f3276852 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -939,7 +939,13 @@ function hasSingleParticipant(report: OnyxEntry): boolean { */ function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): boolean { const transactions = TransactionUtils.getAllReportTransactions(iouReportID); - return transactions.length > 0 && transactions.every((transaction) => TransactionUtils.isDistanceRequest(transaction)); + + // Early return false in case not having any transaction + if (!transactions || transactions.length === 0) { + return false; + } + + return transactions.every((transaction) => TransactionUtils.isDistanceRequest(transaction)); } /** From 82b6560151e03abf3a12c5f2049b79570ea51886 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Thu, 21 Dec 2023 18:07:42 +0700 Subject: [PATCH 064/120] #32864 validate email is valid --- src/pages/home/ReportScreen.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 7896b835b064..0adf246bf7e3 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -291,16 +291,27 @@ function ReportScreen({ */ const onSubmitComment = useCallback( (text) => { - const taskRegex = /^\[\](?:\s+@([^\s@]+@[\w.-]+))?\s+(.+)/; + /** + * Matching task rule by group + * Group 1: Start task rule with [] + * Group 2: Optional email group between \s+....\s* start rule with @+valid email + * Group 3: Title is remaining characters + */ + const taskRegex = /^\[\]\s+(?:@([^\s@]+@[\w.-]+\.[a-zA-Z]{2,}))?\s*(.*)/; + const match = text.match(taskRegex); if (match) { - const email = match[1] ? match[1].trim() : undefined; // Email might be undefined if not captured - const title = match[2]; - let assignee = {}; - if (email) { - assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {}; + const email = match[1] ? match[1].trim() : undefined; + const title = match[2] ? match[2].trim() : undefined; + if (title) { + let assignee = {}; + if (email) { + assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {}; + } + Task.createTaskAndNavigate(getReportID(route), title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID); + } else { + Report.addComment(getReportID(route), text); } - Task.createTaskAndNavigate(getReportID(route), title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID); } else { Report.addComment(getReportID(route), text); } From a4b21b690f2afe9bdaa76faf749b8228e5935b68 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Thu, 21 Dec 2023 23:23:15 +0700 Subject: [PATCH 065/120] markdown task replace multiple line --- src/pages/home/ReportScreen.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 0adf246bf7e3..e740b2244674 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -297,12 +297,12 @@ function ReportScreen({ * Group 2: Optional email group between \s+....\s* start rule with @+valid email * Group 3: Title is remaining characters */ - const taskRegex = /^\[\]\s+(?:@([^\s@]+@[\w.-]+\.[a-zA-Z]{2,}))?\s*(.*)/; + const taskRegex = /^\[\]\s+(?:@([^\s@]+@[\w.-]+\.[a-zA-Z]{2,}))?\s*([\s\S]*)/; const match = text.match(taskRegex); if (match) { const email = match[1] ? match[1].trim() : undefined; - const title = match[2] ? match[2].trim() : undefined; + const title = match[2] ? match[2].trim().replace(/\n/g, ' ') : undefined; if (title) { let assignee = {}; if (email) { From a47ba92753bb3b24c9c7de11d3f18d2315cbfb4d Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Thu, 21 Dec 2023 23:50:26 +0700 Subject: [PATCH 066/120] update lint code, prettier --- src/pages/home/ReportScreen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index e740b2244674..47b75eba9bda 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -323,7 +323,7 @@ function ReportScreen({ return () => clearTimeout(refID); }, - [route], + [route, allPersonalDetails], ); // Clear notifications for the current report when it's opened and re-focused From 06829c82852ac46c86a621e0fa19ae31f5b5e4fb Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Fri, 22 Dec 2023 00:49:05 +0700 Subject: [PATCH 067/120] update code lint --- src/pages/home/ReportScreen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 47b75eba9bda..e681aa1f1023 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -323,7 +323,7 @@ function ReportScreen({ return () => clearTimeout(refID); }, - [route, allPersonalDetails], + [route, allPersonalDetails, report.policyID], ); // Clear notifications for the current report when it's opened and re-focused From 3f057546a3a808ecc77355ac452132d9b7899889 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 22 Dec 2023 12:11:11 +0500 Subject: [PATCH 068/120] fix: skeleton not hiding when no result found for input --- src/pages/SearchPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 69448f3a52e1..20d00fab662d 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -222,7 +222,7 @@ class SearchPage extends Component { headerMessage={headerMessage} hideSectionHeaders showTitleTooltip - shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady && !isSectionsEmpty(sections)} + shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady && (!isSectionsEmpty(sections) || this.state.searchValue.length)} textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={ this.props.network.isOffline ? `${this.props.translate('common.youAppearToBeOffline')} ${this.props.translate('search.resultsAreLimited')}` : '' From 935d275220095df55fb7b64912102432d82283a8 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 22 Dec 2023 10:01:17 +0100 Subject: [PATCH 069/120] migrate ContextMenuItem to TypeScript --- ...ContextMenuItem.js => ContextMenuItem.tsx} | 64 +++++++------------ src/components/MenuItem.tsx | 10 +-- 2 files changed, 27 insertions(+), 47 deletions(-) rename src/components/{ContextMenuItem.js => ContextMenuItem.tsx} (65%) diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.tsx similarity index 65% rename from src/components/ContextMenuItem.js rename to src/components/ContextMenuItem.tsx index 307cfcde9b10..fd883624d179 100644 --- a/src/components/ContextMenuItem.js +++ b/src/components/ContextMenuItem.tsx @@ -1,57 +1,47 @@ -import PropTypes from 'prop-types'; -import React, {forwardRef, useImperativeHandle} from 'react'; +import React, {ForwardedRef, forwardRef, ReactNode, useImperativeHandle} from 'react'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; import useWindowDimensions from '@hooks/useWindowDimensions'; import getButtonState from '@libs/getButtonState'; +import CONST from '@src/CONST'; import BaseMiniContextMenuItem from './BaseMiniContextMenuItem'; -import Icon from './Icon'; +import Icon, {SrcProps} from './Icon'; import MenuItem from './MenuItem'; -const propTypes = { +type ContextMenuItemProps = { /** Icon Component */ - icon: PropTypes.elementType.isRequired, + icon: (props: SrcProps) => ReactNode; /** Text to display */ - text: PropTypes.string.isRequired, + text: string; /** Icon to show when interaction was successful */ - successIcon: PropTypes.elementType, + successIcon?: (props: SrcProps) => ReactNode; /** Text to show when interaction was successful */ - successText: PropTypes.string, + successText?: string; /** Whether to show the mini menu */ - isMini: PropTypes.bool, + isMini?: boolean; /** Callback to fire when the item is pressed */ - onPress: PropTypes.func.isRequired, + onPress: () => void; /** A description text to show under the title */ - description: PropTypes.string, + description?: string; /** The action accept for anonymous user or not */ - isAnonymousAction: PropTypes.bool, + isAnonymousAction?: boolean; /** Whether the menu item is focused or not */ - isFocused: PropTypes.bool, - - /** Forwarded ref to ContextMenuItem */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), -}; - -const defaultProps = { - isMini: false, - successIcon: null, - successText: '', - description: '', - isAnonymousAction: false, - isFocused: false, - innerRef: null, + isFocused?: boolean; }; -function ContextMenuItem({onPress, successIcon, successText, icon, text, isMini, description, isAnonymousAction, isFocused, innerRef}) { +function ContextMenuItem( + {onPress, successIcon, successText = '', icon, text, isMini = false, description = '', isAnonymousAction = false, isFocused = false}: ContextMenuItemProps, + ref: ForwardedRef, +) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {windowWidth} = useWindowDimensions(); @@ -65,12 +55,13 @@ function ContextMenuItem({onPress, successIcon, successText, icon, text, isMini, // We only set the success state when we have icon or text to represent the success state // We may want to replace this check by checking the Result from OnPress Callback in future. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (successIcon || successText) { setThrottledButtonInactive(); } }; - useImperativeHandle(innerRef, () => ({triggerPressAndUpdateSuccess})); + useImperativeHandle(ref, () => ({triggerPressAndUpdateSuccess})); const itemIcon = !isThrottledButtonActive && successIcon ? successIcon : icon; const itemText = !isThrottledButtonActive && successText ? successText : text; @@ -101,23 +92,12 @@ function ContextMenuItem({onPress, successIcon, successText, icon, text, isMini, style={StyleUtils.getContextMenuItemStyles(windowWidth)} isAnonymousAction={isAnonymousAction} focused={isFocused} - interactive={isThrottledButtonActive} + interactive + iconType={CONST.ICON_TYPE_ICON} /> ); } -ContextMenuItem.propTypes = propTypes; -ContextMenuItem.defaultProps = defaultProps; ContextMenuItem.displayName = 'ContextMenuItem'; -const ContextMenuItemWithRef = forwardRef((props, ref) => ( - -)); - -ContextMenuItemWithRef.displayName = 'ContextMenuItemWithRef'; - -export default ContextMenuItemWithRef; +export default forwardRef(ContextMenuItem); diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 96e6ea0bbc44..36366cc0e07d 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -72,7 +72,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & badgeText?: string; /** Used to apply offline styles to child text components */ - style?: ViewStyle; + style?: ViewStyle | ViewStyle[]; /** Any additional styles to apply */ wrapperStyle?: StyleProp; @@ -84,7 +84,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & titleStyle?: ViewStyle; /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle: StyleProp>; + hoverAndPressStyle?: StyleProp>; /** Additional styles to style the description text below the title */ descriptionTextStyle?: StyleProp; @@ -174,7 +174,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & isSelected?: boolean; /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: boolean; + shouldStackHorizontally?: boolean; /** Prop to represent the size of the avatar images to be shown */ avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; @@ -219,10 +219,10 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & furtherDetails?: string; /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction: () => void; + onSecondaryInteraction?: () => void; /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: DisplayNameWithTooltip[]; + titleWithTooltips?: DisplayNameWithTooltip[]; }; function MenuItem( From 0f6211e24949936b6e366986d7081f8520870e1f Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 22 Dec 2023 13:38:24 +0100 Subject: [PATCH 070/120] apply suggested changes --- src/components/ContextMenuItem.tsx | 19 +++++++++++-------- src/components/MenuItem.tsx | 7 ++++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index fd883624d179..e08bba10fafe 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -1,23 +1,23 @@ -import React, {ForwardedRef, forwardRef, ReactNode, useImperativeHandle} from 'react'; +import React, {ForwardedRef, forwardRef, useImperativeHandle} from 'react'; +import {SvgProps} from 'react-native-svg'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; import useWindowDimensions from '@hooks/useWindowDimensions'; import getButtonState from '@libs/getButtonState'; -import CONST from '@src/CONST'; import BaseMiniContextMenuItem from './BaseMiniContextMenuItem'; -import Icon, {SrcProps} from './Icon'; +import Icon from './Icon'; import MenuItem from './MenuItem'; type ContextMenuItemProps = { /** Icon Component */ - icon: (props: SrcProps) => ReactNode; + icon: React.FC; /** Text to display */ text: string; /** Icon to show when interaction was successful */ - successIcon?: (props: SrcProps) => ReactNode; + successIcon?: React.FC; /** Text to show when interaction was successful */ successText?: string; @@ -38,9 +38,13 @@ type ContextMenuItemProps = { isFocused?: boolean; }; +type ContextMenuItemRef = { + triggerPressAndUpdateSuccess?: () => void; +}; + function ContextMenuItem( {onPress, successIcon, successText = '', icon, text, isMini = false, description = '', isAnonymousAction = false, isFocused = false}: ContextMenuItemProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -56,7 +60,7 @@ function ContextMenuItem( // We only set the success state when we have icon or text to represent the success state // We may want to replace this check by checking the Result from OnPress Callback in future. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (successIcon || successText) { + if (!!successIcon || successText) { setThrottledButtonInactive(); } }; @@ -93,7 +97,6 @@ function ContextMenuItem( isAnonymousAction={isAnonymousAction} focused={isFocused} interactive - iconType={CONST.ICON_TYPE_ICON} /> ); } diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 36366cc0e07d..24efb83b7507 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -55,7 +55,7 @@ type IconProps = { }; type AvatarProps = { - iconType: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; + iconType?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; icon: AvatarSource; }; @@ -72,7 +72,8 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & badgeText?: string; /** Used to apply offline styles to child text components */ - style?: ViewStyle | ViewStyle[]; + // style?: ViewStyle | ViewStyle[]; + style?: StyleProp; /** Any additional styles to apply */ wrapperStyle?: StyleProp; @@ -289,7 +290,7 @@ function MenuItem( const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const combinedStyle = StyleUtils.combineStyles(style ?? {}, styles.popoverMenuItem); + const combinedStyle = [style, styles.popoverMenuItem]; const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = useState(''); const titleRef = useRef(''); From 2e251aad0649df9d090fd834ba1e7b5209223c0a Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 22 Dec 2023 13:49:19 +0100 Subject: [PATCH 071/120] remove comment, change type name --- src/components/ContextMenuItem.tsx | 4 ++-- src/components/MenuItem.tsx | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index e08bba10fafe..f8166e609407 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -38,13 +38,13 @@ type ContextMenuItemProps = { isFocused?: boolean; }; -type ContextMenuItemRef = { +type ContextMenuItemHandle = { triggerPressAndUpdateSuccess?: () => void; }; function ContextMenuItem( {onPress, successIcon, successText = '', icon, text, isMini = false, description = '', isAnonymousAction = false, isFocused = false}: ContextMenuItemProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 24efb83b7507..6dca8778bb26 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -72,7 +72,6 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & badgeText?: string; /** Used to apply offline styles to child text components */ - // style?: ViewStyle | ViewStyle[]; style?: StyleProp; /** Any additional styles to apply */ From 82e9c7ce918bfc20e96ab2dd7bbbfe3948398eaf Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 28 Dec 2023 16:47:24 +0500 Subject: [PATCH 072/120] fix: PR feedbacks --- src/components/OptionsList/BaseOptionsList.js | 2 +- src/components/OptionsSelector/BaseOptionsSelector.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index 1d7faf9ae0f9..1699b68b2348 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -85,7 +85,7 @@ const BaseOptionsList = forwardRef( const didLayout = useRef(false); const listContainerStyles = useMemo(() => listContainerStylesProp || [styles.flex1], [listContainerStylesProp, styles.flex1]); - const contentContainerStyles = useMemo(() => [contentContainerStylesProp, safeAreaPaddingBottomStyle], [contentContainerStylesProp, safeAreaPaddingBottomStyle]); + const contentContainerStyles = useMemo(() => [safeAreaPaddingBottomStyle, ...contentContainerStylesProp], [contentContainerStylesProp, safeAreaPaddingBottomStyle]); /** * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 22e302448f29..b44c5375d720 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -100,6 +100,9 @@ class BaseOptionsSelector extends Component { componentDidMount() { this.focusListener = this.props.navigation.addListener('focus', () => { + // Screen coming back into focus, for example + // when doing Cmd+Shift+K, then Cmd+K, then Cmd+Shift+K. + // Only applies to platforms that support keyboard shortcuts if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform())) { this.subscribeToKeyboardShortcut(); } @@ -188,7 +191,9 @@ class BaseOptionsSelector extends Component { } componentWillUnmount() { - this.interactionTask.cancel(); + if (this.interactionTask) { + this.interactionTask.cancel(); + } this.focusListener(); this.blurListener(); if (this.focusTimeout) { From 94d9a50b8a00b43a28ed9eb90f33cf38d423b6dc Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 28 Dec 2023 16:47:50 +0500 Subject: [PATCH 073/120] test: fix reassure test --- tests/perf-test/OptionsSelector.perf-test.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/perf-test/OptionsSelector.perf-test.js b/tests/perf-test/OptionsSelector.perf-test.js index da706d7bb629..2152767b1df0 100644 --- a/tests/perf-test/OptionsSelector.perf-test.js +++ b/tests/perf-test/OptionsSelector.perf-test.js @@ -36,6 +36,17 @@ jest.mock('../../src/components/withNavigationFocus', () => (Component) => { return WithNavigationFocus; }); +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useNavigation: () => ({ + navigate: jest.fn(), + addListener: () => jest.fn(), + }), + }; +}); + const generateSections = (sectionConfigs) => _.map(sectionConfigs, ({numItems, indexOffset, shouldShow = true}) => ({ data: Array.from({length: numItems}, (_v, i) => ({ @@ -118,10 +129,10 @@ test('[OptionsSelector] should scroll and press few items', () => { const eventData = generateEventData(100, variables.optionRowHeight); const eventData2 = generateEventData(200, variables.optionRowHeight); - const scenario = (screen) => { + const scenario = async (screen) => { fireEvent.press(screen.getByText('Item 10')); fireEvent.scroll(screen.getByTestId('options-list'), eventData); - fireEvent.press(screen.getByText('Item 100')); + fireEvent.press(await screen.findByText('Item 100')); fireEvent.scroll(screen.getByTestId('options-list'), eventData2); fireEvent.press(screen.getByText('Item 200')); }; From 56f5f4948fed0e6d57a6aa72a224de550a3414ea Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 28 Dec 2023 19:58:54 +0500 Subject: [PATCH 074/120] test: mock withNavigation for options selector reassure test --- tests/perf-test/OptionsSelector.perf-test.js | 25 +++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/tests/perf-test/OptionsSelector.perf-test.js b/tests/perf-test/OptionsSelector.perf-test.js index 2152767b1df0..557a0baf1ba4 100644 --- a/tests/perf-test/OptionsSelector.perf-test.js +++ b/tests/perf-test/OptionsSelector.perf-test.js @@ -20,31 +20,22 @@ jest.mock('../../src/components/withLocalize', () => (Component) => { return WrappedComponent; }); -jest.mock('../../src/components/withNavigationFocus', () => (Component) => { - function WithNavigationFocus(props) { +jest.mock('../../src/components/withNavigation', () => (Component) => { + function withNavigation(props) { return ( jest.fn(), + }} /> ); } - WithNavigationFocus.displayName = 'WithNavigationFocus'; - - return WithNavigationFocus; -}); - -jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); - return { - ...actualNav, - useNavigation: () => ({ - navigate: jest.fn(), - addListener: () => jest.fn(), - }), - }; + withNavigation.displayName = 'withNavigation'; + return withNavigation; }); const generateSections = (sectionConfigs) => From c4859f4996667471218286632fa32d1ef3a19fa1 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 29 Dec 2023 14:16:58 +0500 Subject: [PATCH 075/120] fix: linting --- src/components/OptionsList/BaseOptionsList.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index 1699b68b2348..8b24066af969 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -101,6 +101,7 @@ const BaseOptionsList = forwardRef( // Build the flat array for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { const section = sections[sectionIndex]; + // Add the section header const sectionHeaderHeight = section.title && !hideSectionHeaders ? variables.optionsListSectionHeaderHeight : 0; flatArray.push({length: sectionHeaderHeight, offset}); @@ -172,6 +173,7 @@ const BaseOptionsList = forwardRef( /** * Returns the key used by the list + * * @param {Object} option * @return {String} */ From 96a368834a57debd201e41275b7c78e2a1cb49fa Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Sat, 30 Dec 2023 18:18:28 +0530 Subject: [PATCH 076/120] fix for unwanted actions in deleted iou report --- src/libs/actions/IOU.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 2f9506c1e870..3985fb7913be 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2437,6 +2437,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, value: { hasOutstandingChildRequest: false, + iouReportID: null, lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}).lastMessageText, lastVisibleActionCreated: lodashGet(ReportActionsUtils.getLastVisibleAction(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}), 'created'), }, From 31e0e0b14f17e8d1681e3e5ce95a941cc83f0842 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sun, 31 Dec 2023 22:49:17 +0530 Subject: [PATCH 077/120] fixed error flow in submit report --- src/libs/actions/IOU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ecae885392b9..4c82ae23696b 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -3075,7 +3075,7 @@ function submitReport(expenseReport) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, value: { - [expenseReport.reportActionID]: { + [optimisticSubmittedReportAction.reportActionID]: { errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), }, }, From 311b4d428a2cecff5e62e826c698f91724346ef0 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 2 Jan 2024 11:20:39 +0100 Subject: [PATCH 078/120] make titleWithTooltips prop optional --- src/components/MenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 324910ebd0c1..a5ac3e9b9b93 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -223,7 +223,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & onSecondaryInteraction?: () => void; /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: DisplayNameWithTooltip[]; + titleWithTooltips?: DisplayNameWithTooltip[]; /** Icon should be displayed in its own color */ displayInDefaultIconColor?: boolean; From b3974b7a14228ec0101113a34093317adf8adf8d Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 2 Jan 2024 11:58:24 +0100 Subject: [PATCH 079/120] get rid of interactive/uninteractive props --- src/components/ContextMenuItem.tsx | 2 +- src/components/MenuItem.tsx | 241 ++++++++++++++--------------- 2 files changed, 117 insertions(+), 126 deletions(-) diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index f8166e609407..0975794f1e45 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -96,7 +96,7 @@ function ContextMenuItem( style={StyleUtils.getContextMenuItemStyles(windowWidth)} isAnonymousAction={isAnonymousAction} focused={isFocused} - interactive + interactive={isThrottledButtonActive} /> ); } diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index a5ac3e9b9b93..2b192aba2f2e 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -33,20 +33,6 @@ import RenderHTML from './RenderHTML'; import SelectCircle from './SelectCircle'; import Text from './Text'; -type ResponsiveProps = { - /** Function to fire when component is pressed */ - onPress: (event: GestureResponderEvent | KeyboardEvent) => void; - - interactive?: true; -}; - -type UnresponsiveProps = { - onPress?: undefined; - - /** Whether the menu item should be interactive at all */ - interactive: false; -}; - type IconProps = { /** Flag to choose between avatar image or an icon */ iconType: typeof CONST.ICON_TYPE_ICON; @@ -67,170 +53,175 @@ type NoIcon = { icon?: undefined; }; -type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & - (IconProps | AvatarProps | NoIcon) & { - /** Text to be shown as badge near the right end. */ - badgeText?: string; +type MenuItemProps = (IconProps | AvatarProps | NoIcon) & { + /** Function to fire when component is pressed */ + onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; - /** Used to apply offline styles to child text components */ - style?: StyleProp; + /** Whether the menu item should be interactive at all */ + interactive?: boolean; - /** Any additional styles to apply */ - wrapperStyle?: StyleProp; + /** Text to be shown as badge near the right end. */ + badgeText?: string; - /** Any additional styles to apply on the outer element */ - containerStyle?: StyleProp; + /** Used to apply offline styles to child text components */ + style?: StyleProp; - /** Used to apply styles specifically to the title */ - titleStyle?: ViewStyle; + /** Any additional styles to apply */ + wrapperStyle?: StyleProp; - /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle?: StyleProp>; + /** Any additional styles to apply on the outer element */ + containerStyle?: StyleProp; - /** Additional styles to style the description text below the title */ - descriptionTextStyle?: StyleProp; + /** Used to apply styles specifically to the title */ + titleStyle?: ViewStyle; - /** The fill color to pass into the icon. */ - iconFill?: string; + /** Any adjustments to style when menu item is hovered or pressed */ + hoverAndPressStyle?: StyleProp>; - /** Secondary icon to display on the left side of component, right of the icon */ - secondaryIcon?: IconAsset; + /** Additional styles to style the description text below the title */ + descriptionTextStyle?: StyleProp; - /** The fill color to pass into the secondary icon. */ - secondaryIconFill?: string; + /** The fill color to pass into the icon. */ + iconFill?: string; - /** Icon Width */ - iconWidth?: number; + /** Secondary icon to display on the left side of component, right of the icon */ + secondaryIcon?: IconAsset; - /** Icon Height */ - iconHeight?: number; + /** The fill color to pass into the secondary icon. */ + secondaryIconFill?: string; - /** Any additional styles to pass to the icon container. */ - iconStyles?: StyleProp; + /** Icon Width */ + iconWidth?: number; - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon?: IconAsset; + /** Icon Height */ + iconHeight?: number; - /** An icon to display under the main item */ - furtherDetailsIcon?: IconAsset; + /** Any additional styles to pass to the icon container. */ + iconStyles?: StyleProp; - /** Boolean whether to display the title right icon */ - shouldShowTitleIcon?: boolean; + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon?: IconAsset; - /** Icon to display at right side of title */ - titleIcon?: IconAsset; + /** An icon to display under the main item */ + furtherDetailsIcon?: IconAsset; - /** Boolean whether to display the right icon */ - shouldShowRightIcon?: boolean; + /** Boolean whether to display the title right icon */ + shouldShowTitleIcon?: boolean; - /** Overrides the icon for shouldShowRightIcon */ - iconRight?: IconAsset; + /** Icon to display at right side of title */ + titleIcon?: IconAsset; - /** Should render component on the right */ - shouldShowRightComponent?: boolean; + /** Boolean whether to display the right icon */ + shouldShowRightIcon?: boolean; - /** Component to be displayed on the right */ - rightComponent?: ReactNode; + /** Overrides the icon for shouldShowRightIcon */ + iconRight?: IconAsset; - /** A description text to show under the title */ - description?: string; + /** Should render component on the right */ + shouldShowRightComponent?: boolean; - /** Should the description be shown above the title (instead of the other way around) */ - shouldShowDescriptionOnTop?: boolean; + /** Component to be displayed on the right */ + rightComponent?: ReactNode; - /** Error to display below the title */ - error?: string; + /** A description text to show under the title */ + description?: string; - /** Error to display at the bottom of the component */ - errorText?: string; + /** Should the description be shown above the title (instead of the other way around) */ + shouldShowDescriptionOnTop?: boolean; - /** A boolean flag that gives the icon a green fill if true */ - success?: boolean; + /** Error to display below the title */ + error?: string; - /** Whether item is focused or active */ - focused?: boolean; + /** Error to display at the bottom of the component */ + errorText?: string; - /** Should we disable this menu item? */ - disabled?: boolean; + /** A boolean flag that gives the icon a green fill if true */ + success?: boolean; - /** Text that appears above the title */ - label?: string; + /** Whether item is focused or active */ + focused?: boolean; - /** Label to be displayed on the right */ - rightLabel?: string; + /** Should we disable this menu item? */ + disabled?: boolean; - /** Text to display for the item */ - title?: string; + /** Text that appears above the title */ + label?: string; - /** A right-aligned subtitle for this menu option */ - subtitle?: string | number; + /** Label to be displayed on the right */ + rightLabel?: string; - /** Should the title show with normal font weight (not bold) */ - shouldShowBasicTitle?: boolean; + /** Text to display for the item */ + title?: string; - /** Should we make this selectable with a checkbox */ - shouldShowSelectedState?: boolean; + /** A right-aligned subtitle for this menu option */ + subtitle?: string | number; - /** Whether this item is selected */ - isSelected?: boolean; + /** Should the title show with normal font weight (not bold) */ + shouldShowBasicTitle?: boolean; - /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally?: boolean; + /** Should we make this selectable with a checkbox */ + shouldShowSelectedState?: boolean; - /** Prop to represent the size of the avatar images to be shown */ - avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; + /** Whether this item is selected */ + isSelected?: boolean; - /** Avatars to show on the right of the menu item */ - floatRightAvatars?: IconType[]; + /** Prop to identify if we should load avatars vertically instead of diagonally */ + shouldStackHorizontally?: boolean; - /** Prop to represent the size of the float right avatar images to be shown */ - floatRightAvatarSize?: ValueOf; + /** Prop to represent the size of the avatar images to be shown */ + avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; - /** Affects avatar size */ - viewMode?: ValueOf; + /** Avatars to show on the right of the menu item */ + floatRightAvatars?: IconType[]; - /** Used to truncate the text with an ellipsis after computing the text layout */ - numberOfLinesTitle?: number; + /** Prop to represent the size of the float right avatar images to be shown */ + floatRightAvatarSize?: ValueOf; - /** Whether we should use small avatar subscript sizing the for menu item */ - isSmallAvatarSubscriptMenu?: boolean; + /** Affects avatar size */ + viewMode?: ValueOf; - /** The type of brick road indicator to show. */ - brickRoadIndicator?: ValueOf; + /** Used to truncate the text with an ellipsis after computing the text layout */ + numberOfLinesTitle?: number; - /** Should render the content in HTML format */ - shouldRenderAsHTML?: boolean; + /** Whether we should use small avatar subscript sizing the for menu item */ + isSmallAvatarSubscriptMenu?: boolean; - /** Should we grey out the menu item when it is disabled? */ - shouldGreyOutWhenDisabled?: boolean; + /** The type of brick road indicator to show. */ + brickRoadIndicator?: ValueOf; - /** The action accept for anonymous user or not */ - isAnonymousAction?: boolean; + /** Should render the content in HTML format */ + shouldRenderAsHTML?: boolean; - /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ - shouldBlockSelection?: boolean; + /** Should we grey out the menu item when it is disabled? */ + shouldGreyOutWhenDisabled?: boolean; - /** Whether should render title as HTML or as Text */ - shouldParseTitle?: false; + /** The action accept for anonymous user or not */ + isAnonymousAction?: boolean; - /** Should check anonymous user in onPress function */ - shouldCheckActionAllowedOnPress?: boolean; + /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ + shouldBlockSelection?: boolean; - /** Text to display under the main item */ - furtherDetails?: string; + /** Whether should render title as HTML or as Text */ + shouldParseTitle?: false; - /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction?: () => void; + /** Should check anonymous user in onPress function */ + shouldCheckActionAllowedOnPress?: boolean; - /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips?: DisplayNameWithTooltip[]; + /** Text to display under the main item */ + furtherDetails?: string; - /** Icon should be displayed in its own color */ - displayInDefaultIconColor?: boolean; + /** The function that should be called when this component is LongPressed or right-clicked. */ + onSecondaryInteraction?: () => void; - /** Determines how the icon should be resized to fit its container */ - contentFit?: ImageContentFit; - }; + /** Array of objects that map display names to their corresponding tooltip */ + titleWithTooltips?: DisplayNameWithTooltip[]; + + /** Icon should be displayed in its own color */ + displayInDefaultIconColor?: boolean; + + /** Determines how the icon should be resized to fit its container */ + contentFit?: ImageContentFit; +}; function MenuItem( { From 66cb0155b8a5f5cec8dd3bfb9431d93e0f6f066e Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 2 Jan 2024 12:36:19 +0100 Subject: [PATCH 080/120] remove unnecessary eslint disable --- src/components/ContextMenuItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index 0975794f1e45..4a47221b3af8 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -59,7 +59,6 @@ function ContextMenuItem( // We only set the success state when we have icon or text to represent the success state // We may want to replace this check by checking the Result from OnPress Callback in future. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (!!successIcon || successText) { setThrottledButtonInactive(); } From 00cf257b770bfd2336f50463d14b86f8d430acbe Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Tue, 2 Jan 2024 17:36:24 +0530 Subject: [PATCH 081/120] remove money request from composer action list on pending deletion --- src/libs/ReportUtils.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a3adfce762dc..d9500d539d64 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3710,6 +3710,13 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o if (isOwnExpenseReport && PolicyUtils.isPaidGroupPolicy(policy)) { return isDraftExpenseReport(report); } + + // If the Money Request report is marked for deletion, let us prevent any further money requests from being generated. + const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); + if (parentReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return false; + } + return (isOwnExpenseReport || isIOUReport(report)) && !isReportApproved(report) && !isSettled(report?.reportID); } From 934daf817eff1c5219f31c42c4aba8a8d2b81f9a Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 3 Jan 2024 08:58:19 +0100 Subject: [PATCH 082/120] change icon type --- src/components/ContextMenuItem.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index 4a47221b3af8..5cbc5b8179c3 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -1,23 +1,23 @@ import React, {ForwardedRef, forwardRef, useImperativeHandle} from 'react'; -import {SvgProps} from 'react-native-svg'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; import useWindowDimensions from '@hooks/useWindowDimensions'; import getButtonState from '@libs/getButtonState'; +import IconAsset from '@src/types/utils/IconAsset'; import BaseMiniContextMenuItem from './BaseMiniContextMenuItem'; import Icon from './Icon'; import MenuItem from './MenuItem'; type ContextMenuItemProps = { /** Icon Component */ - icon: React.FC; + icon: IconAsset; /** Text to display */ text: string; /** Icon to show when interaction was successful */ - successIcon?: React.FC; + successIcon?: IconAsset; /** Text to show when interaction was successful */ successText?: string; From d4ce66fd9f3e23696ebf66065396f7c419981d38 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 3 Jan 2024 09:14:45 +0100 Subject: [PATCH 083/120] TS fix after merging main --- src/types/onyx/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 2a0990580ed0..9cc8464e2b28 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -75,7 +75,7 @@ type Policy = { autoReportingFrequency?: ValueOf; /** Whether the scheduled submit is enabled */ - isHarvestingEnabled: boolean; + isHarvestingEnabled?: boolean; /** The accountID of manager who the employee submits their expenses to on paid policies */ submitsTo?: number; From 16b3280bff67407008db18a5199593c52c5dca09 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 3 Jan 2024 09:41:25 +0100 Subject: [PATCH 084/120] fix: remove doubled name for IOUs previews in SearchPage --- src/libs/OptionsListUtils.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 0d5162399fcb..53ccb4c8ad4c 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -72,6 +72,7 @@ Onyx.connect({ const lastReportActions = {}; const allSortedReportActions = {}; const allReportActions = {}; +const visibleReportActionItems = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, callback: (actions, key) => { @@ -83,6 +84,18 @@ Onyx.connect({ const sortedReportActions = ReportActionUtils.getSortedReportActions(_.toArray(actions), true); allSortedReportActions[reportID] = sortedReportActions; lastReportActions[reportID] = _.first(sortedReportActions); + + // The report is only visible if it is the last action not deleted that + // does not match a closed or created state. + const reportActionsForDisplay = _.filter( + sortedReportActions, + (reportAction, actionKey) => + ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey) && + !ReportActionUtils.isWhisperAction(reportAction) && + reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && + reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + ); + visibleReportActionItems[reportID] = reportActionsForDisplay[reportActionsForDisplay.length - 1]; }, }); @@ -513,7 +526,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { const lastActorDetails = personalDetailMap[report.lastActorAccountID] || null; const lastActorDisplayName = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? lastActorDetails.firstName || lastActorDetails.displayName : ''; - let lastMessageText = lastActorDisplayName ? `${lastActorDisplayName}: ${lastMessageTextFromReport}` : lastMessageTextFromReport; + let lastMessageText = lastMessageTextFromReport; if (result.isArchivedRoom) { const archiveReason = @@ -525,12 +538,16 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { }); } + const lastAction = visibleReportActionItems[report.reportID]; + if (result.isThread || result.isMoneyRequestReport) { result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); } else if (result.isChatRoom || result.isPolicyExpenseChat) { result.alternateText = showChatPreviewLine && !forcePolicyNamePreview && lastMessageText ? lastMessageText : subtitle; } else if (result.isTaskReport) { result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageTextFromReport : Localize.translate(preferredLocale, 'report.noActivityYet'); + } else if (lastAction && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) { + result.alternateText = `${lastActorDisplayName}: ${lastMessageText}`; } else { result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail.login); } From 8a99355d3fa2660f987e363d869d636df09fd5b3 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 3 Jan 2024 11:06:41 +0100 Subject: [PATCH 085/120] Re-run performance test From 4de7a5ed23c231251f1bee824c46bac35892efa1 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 4 Jan 2024 10:08:21 +0100 Subject: [PATCH 086/120] Fix lint errors --- src/libs/actions/Card.ts | 3 ++- src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts | 2 +- src/libs/actions/TeachersUnite.ts | 3 ++- src/libs/actions/TransactionEdit.ts | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index e5ae09d5b28a..172b0ac73ca6 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -1,4 +1,5 @@ -import Onyx, {OnyxUpdate} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import type {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; diff --git a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts b/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts index 79d1ec0f82d9..3e8c613187b4 100644 --- a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts +++ b/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts @@ -1,5 +1,5 @@ import Onyx from 'react-native-onyx'; -import {OnyxKey} from 'react-native-onyx/lib/types'; +import type {OnyxKey} from 'react-native-onyx/lib/types'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index 4768794c39f2..066e55a4d365 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -1,4 +1,5 @@ -import Onyx, {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; diff --git a/src/libs/actions/TransactionEdit.ts b/src/libs/actions/TransactionEdit.ts index 3831ba8e437d..b1710aa72cbb 100644 --- a/src/libs/actions/TransactionEdit.ts +++ b/src/libs/actions/TransactionEdit.ts @@ -1,4 +1,5 @@ -import Onyx, {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction} from '@src/types/onyx'; From 2a73e488f9e0cc172edf5011bf1641430863968c Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 4 Jan 2024 10:17:59 +0100 Subject: [PATCH 087/120] refactor: remove getDisplayNamesStringFromTooltips --- src/libs/GroupChatUtils.ts | 22 +++++++--------------- src/libs/ReportUtils.ts | 12 ------------ 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts index ba14bc9c9c3d..26b3665ca4ce 100644 --- a/src/libs/GroupChatUtils.ts +++ b/src/libs/GroupChatUtils.ts @@ -1,26 +1,18 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList, Report} from '@src/types/onyx'; -import * as OptionsListUtils from './OptionsListUtils'; +import type {Report} from '@src/types/onyx'; import * as ReportUtils from './ReportUtils'; -let allPersonalDetails: OnyxEntry = {}; -Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => (allPersonalDetails = val), -}); - /** * Returns the report name if the report is a group chat */ function getGroupChatName(report: Report): string | undefined { const participants = report.participantAccountIDs ?? []; const isMultipleParticipantReport = participants.length > 1; - const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, allPersonalDetails ?? {}); - // @ts-expect-error Error will gone when OptionsListUtils will be migrated to Typescript - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipantReport); - return ReportUtils.getDisplayNamesStringFromTooltips(displayNamesWithTooltips); + + return participants + .map((participant) => ReportUtils.getDisplayNameForParticipant(participant, isMultipleParticipantReport)) + .sort((first, second) => first?.localeCompare(second ?? '') ?? 0) + .filter(Boolean) + .join(', '); } export { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 33a18b8534df..4a8883ddf60c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1537,17 +1537,6 @@ function getDisplayNamesWithTooltips( }); } -/** - * Gets a joined string of display names from the list of display name with tooltip objects. - * - */ -function getDisplayNamesStringFromTooltips(displayNamesWithTooltips: DisplayNameWithTooltips | undefined) { - return displayNamesWithTooltips - ?.map(({displayName}) => displayName) - .filter(Boolean) - .join(', '); -} - /** * For a deleted parent report action within a chat report, * let us return the appropriate display message @@ -4317,7 +4306,6 @@ export { getIcons, getRoomWelcomeMessage, getDisplayNamesWithTooltips, - getDisplayNamesStringFromTooltips, getReportName, getReport, getReportNotificationPreference, From 1c53bfaa739ca31cd4e6ab3261378ffb6312b30b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 4 Jan 2024 10:27:33 +0100 Subject: [PATCH 088/120] refactor: rename getDisplayName to createDisplayName --- src/libs/ReportUtils.ts | 1 - src/libs/actions/PersonalDetails.ts | 15 +++++++-------- src/libs/actions/User.js | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4a8883ddf60c..bfa384da7af6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1475,7 +1475,6 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f } const personalDetails = getPersonalDetailsForAccountID(accountID); - // console.log(personalDetails); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetails.login || ''); // This is to check if account is an invite/optimistically created one diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 20c6d8fef247..6bcbb57695c9 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -41,20 +41,19 @@ Onyx.connect({ }); /** - * Returns the displayName for a user + * Creates a new displayName for a user based on passed personal details or login. */ -function getDisplayName(login: string, personalDetail: Pick | null): string { +function createDisplayName(login: string, personalDetails: Pick): string { // If we have a number like +15857527441@expensify.sms then let's remove @expensify.sms and format it // so that the option looks cleaner in our UI. const userLogin = LocalePhoneNumber.formatPhoneNumber(login); - const userDetails = personalDetail ?? allPersonalDetails?.[login]; - if (!userDetails) { + if (!personalDetails) { return userLogin; } - const firstName = userDetails.firstName ?? ''; - const lastName = userDetails.lastName ?? ''; + const firstName = personalDetails.firstName ?? ''; + const lastName = personalDetails.lastName ?? ''; const fullName = `${firstName} ${lastName}`.trim(); // It's possible for fullName to be empty string, so we must use "||" to fallback to userLogin. @@ -148,7 +147,7 @@ function updateDisplayName(firstName: string, lastName: string) { [currentUserAccountID]: { firstName, lastName, - displayName: getDisplayName(currentUserEmail ?? '', { + displayName: createDisplayName(currentUserEmail ?? '', { firstName, lastName, }), @@ -560,7 +559,7 @@ export { deleteAvatar, extractFirstAndLastNameFromAvailableDetails, getCountryISO, - getDisplayName, + createDisplayName, getPrivatePersonalDetails, openPersonalDetailsPage, openPublicProfilePage, diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 67828c766147..4984d32bafb5 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -724,7 +724,7 @@ function setContactMethodAsDefault(newDefaultContactMethod) { value: { [currentUserAccountID]: { login: newDefaultContactMethod, - displayName: PersonalDetails.getDisplayName(newDefaultContactMethod, myPersonalDetails), + displayName: PersonalDetails.createDisplayName(newDefaultContactMethod, myPersonalDetails), }, }, }, From 6a29334f3d6d19277bb1c6db8676d6e5c8ef47c5 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 4 Jan 2024 16:28:59 +0700 Subject: [PATCH 089/120] fix: Enable Wallet card flashes briefly when returning from card details page --- src/pages/settings/Wallet/PaymentMethodList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/PaymentMethodList.js b/src/pages/settings/Wallet/PaymentMethodList.js index 06bd8afa6140..0c708442cec6 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.js +++ b/src/pages/settings/Wallet/PaymentMethodList.js @@ -337,7 +337,7 @@ function PaymentMethodList({ return ( <> - + Date: Thu, 4 Jan 2024 19:43:59 +0100 Subject: [PATCH 090/120] fix: restore getDisplayNameOrDefault --- src/libs/OptionsListUtils.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 767b468a8c26..667ae6910ba7 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -529,7 +529,9 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { const lastMessageTextFromReport = getLastMessageTextForReport(report); const lastActorDetails = personalDetailMap[report.lastActorAccountID] || null; const lastActorDisplayName = - hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? lastActorDetails.firstName || lastActorDetails.displayName : ''; + hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID + ? lastActorDetails.firstName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails) + : ''; let lastMessageText = lastMessageTextFromReport; if (result.isArchivedRoom) { From ad41ffb80d0c2b804951c9c47a777f20ac852a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Fri, 5 Jan 2024 16:36:59 +0100 Subject: [PATCH 091/120] SectionList wrapper opacity fix to show skeleton --- src/components/SelectionList/BaseSelectionList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index dd35c44e6827..fb47a27ded74 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -78,7 +78,7 @@ function BaseSelectionList({ const isFocused = useIsFocused(); const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); const [isInitialRender, setIsInitialRender] = useState(true); - const wrapperStyles = useMemo(() => ({opacity: isInitialRender ? 0 : 1}), [isInitialRender]); + const wrapperStyles = useMemo(() => ({opacity: isInitialRender && !showLoadingPlaceholder ? 0 : 1}), [isInitialRender, showLoadingPlaceholder]); /** * Iterates through the sections and items inside each section, and builds 3 arrays along the way: From 6c2aa5113574cdce55289b7e6d1bc9fbd84a208b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Fri, 5 Jan 2024 16:50:40 +0100 Subject: [PATCH 092/120] merge with main fixes --- src/components/SelectionList/BaseSelectionList.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 455e4b1935d3..8161116b7145 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -77,8 +77,6 @@ function BaseSelectionList({ const activeElement = useActiveElement(); const isFocused = useIsFocused(); const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); - const [isInitialRender, setIsInitialRender] = useState(true); - const wrapperStyles = useMemo(() => ({opacity: isInitialRender && !showLoadingPlaceholder ? 0 : 1}), [isInitialRender, showLoadingPlaceholder]); const [isInitialSectionListRender, setIsInitialSectionListRender] = useState(true); /** From 538b1918536961e5b466cc4cda2fc96e7040c82c Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 5 Jan 2024 23:11:25 +0530 Subject: [PATCH 093/120] hide composer on offline deletion of last money request --- src/libs/ReportUtils.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d9500d539d64..6b891a77a21b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3711,12 +3711,6 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o return isDraftExpenseReport(report); } - // If the Money Request report is marked for deletion, let us prevent any further money requests from being generated. - const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); - if (parentReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - return false; - } - return (isOwnExpenseReport || isIOUReport(report)) && !isReportApproved(report) && !isSettled(report?.reportID); } @@ -3885,6 +3879,13 @@ function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Recor function canUserPerformWriteAction(report: OnyxEntry) { const reportErrors = getAddWorkspaceRoomOrChatReportErrors(report); + // If the Money Request report is marked for deletion, let us prevent any further write action. + if (isMoneyRequestReport(report)) { + const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); + if (parentReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return false; + } + } return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser; } From 6cffe7c3ae40b9f6af5634062273f9da436aaa97 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 5 Jan 2024 09:41:48 -0800 Subject: [PATCH 094/120] only show physical card row on Expensify Card Page if card is open --- src/pages/settings/Wallet/ExpensifyCardPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 376c69beb955..3c44f806fdb8 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -265,7 +265,7 @@ function ExpensifyCardPage({ /> )} - {!_.isEmpty(physicalCard) && ( + {physicalCard.state === CONST.EXPENSIFY_CARD.STATE.OPEN && ( <> Date: Sun, 7 Jan 2024 19:29:25 +0100 Subject: [PATCH 095/120] Make sure not to show (none) in the modified expense message --- src/libs/ModifiedExpenseMessage.ts | 4 +++- src/pages/EditRequestMerchantPage.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 600cfb48a1c1..61e7ce04ab71 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -38,8 +38,10 @@ function buildMessageFragmentForValue( const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; + const isOldValuePartialMerchant = valueName === Localize.translateLocal('common.merchant') && oldValue === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - if (!oldValue) { + // In case of a partial merchant value, we want to avoid user seeing the "(none)" value in the message. + if (!oldValue || isOldValuePartialMerchant) { const fragment = Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); setFragments.push(fragment); } else if (!newValue) { diff --git a/src/pages/EditRequestMerchantPage.js b/src/pages/EditRequestMerchantPage.js index c8766d9acc67..e5966bad2d2b 100644 --- a/src/pages/EditRequestMerchantPage.js +++ b/src/pages/EditRequestMerchantPage.js @@ -27,7 +27,7 @@ function EditRequestMerchantPage({defaultMerchant, onSubmit, isPolicyExpenseChat const styles = useThemeStyles(); const {translate} = useLocalize(); const merchantInputRef = useRef(null); - const isEmptyMerchant = defaultMerchant === '' || defaultMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || defaultMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; + const isEmptyMerchant = defaultMerchant === '' || defaultMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const validate = useCallback( (value) => { From 37e83cdcf5e34bdeceec82a4d4bc5673e03d4eff Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 8 Jan 2024 09:49:07 +0100 Subject: [PATCH 096/120] fix: display actor name for threads --- src/libs/OptionsListUtils.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index a8ece15c7623..19c019b5e41c 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -545,6 +545,11 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { } const lastAction = visibleReportActionItems[report.reportID]; + const shouldDisplayLastActorName = lastAction && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU + + if (shouldDisplayLastActorName && lastActorDisplayName && lastMessageTextFromReport) { + lastMessageText = `${lastActorDisplayName}: ${lastMessageTextFromReport}`; + } if (result.isThread || result.isMoneyRequestReport) { result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); @@ -552,8 +557,6 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.alternateText = showChatPreviewLine && !forcePolicyNamePreview && lastMessageText ? lastMessageText : subtitle; } else if (result.isTaskReport) { result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageTextFromReport : Localize.translate(preferredLocale, 'report.noActivityYet'); - } else if (lastAction && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) { - result.alternateText = `${lastActorDisplayName}: ${lastMessageText}`; } else { result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail.login); } From 4b985db0d015d877e793f344e4dfb789ae391b22 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 8 Jan 2024 10:02:29 +0100 Subject: [PATCH 097/120] fix: run prettier --- src/libs/OptionsListUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 19c019b5e41c..9516b34dddaf 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -545,7 +545,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { } const lastAction = visibleReportActionItems[report.reportID]; - const shouldDisplayLastActorName = lastAction && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU + const shouldDisplayLastActorName = lastAction && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU; if (shouldDisplayLastActorName && lastActorDisplayName && lastMessageTextFromReport) { lastMessageText = `${lastActorDisplayName}: ${lastMessageTextFromReport}`; From 190fb91cc312e5648cf814559949877d5706bcd9 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 8 Jan 2024 15:14:03 +0100 Subject: [PATCH 098/120] decrease line height for report preview --- src/components/ReportActionItem/ReportPreview.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 27447a10a32b..abc7e3954200 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -15,7 +15,6 @@ import {showContextMenuForReport} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -30,7 +29,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; -import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -128,7 +126,6 @@ const defaultProps = { function ReportPreview(props) { const theme = useTheme(); const styles = useThemeStyles(); - const {getLineHeightStyle} = useStyleUtils(); const {translate} = useLocalize(); const [hasMissingSmartscanFields, sethasMissingSmartscanFields] = useState(false); @@ -286,7 +283,7 @@ function ReportPreview(props) { - {getPreviewMessage()} + {getPreviewMessage()} {!iouSettled && hasErrors && ( Date: Mon, 8 Jan 2024 23:18:16 +0700 Subject: [PATCH 099/120] 32864 cleanup elsewhere condition --- src/pages/home/ReportScreen.js | 40 ++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index e681aa1f1023..0e9c2eb95c93 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -289,7 +289,7 @@ function ReportScreen({ /** * @param {String} text */ - const onSubmitComment = useCallback( + const handleCreateTask = useCallback( (text) => { /** * Matching task rule by group @@ -300,19 +300,31 @@ function ReportScreen({ const taskRegex = /^\[\]\s+(?:@([^\s@]+@[\w.-]+\.[a-zA-Z]{2,}))?\s*([\s\S]*)/; const match = text.match(taskRegex); - if (match) { - const email = match[1] ? match[1].trim() : undefined; - const title = match[2] ? match[2].trim().replace(/\n/g, ' ') : undefined; - if (title) { - let assignee = {}; - if (email) { - assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {}; - } - Task.createTaskAndNavigate(getReportID(route), title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID); - } else { - Report.addComment(getReportID(route), text); + if (!match) { + return false; + } + const email = match[1] ? match[1].trim() : undefined; + const title = match[2] ? match[2].trim().replace(/\n/g, ' ') : undefined; + if (title) { + let assignee = {}; + if (email) { + assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {}; } - } else { + Task.createTaskAndNavigate(getReportID(route), title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID); + return true; + } + return false; + }, + [allPersonalDetails, report.policyID, route], + ); + + /** + * @param {String} text + */ + const onSubmitComment = useCallback( + (text) => { + const isHandled = handleCreateTask(text); + if (!isHandled) { Report.addComment(getReportID(route), text); } @@ -323,7 +335,7 @@ function ReportScreen({ return () => clearTimeout(refID); }, - [route, allPersonalDetails, report.policyID], + [route, handleCreateTask], ); // Clear notifications for the current report when it's opened and re-focused From 6c788e94994df88f29078279a589a7add3762b0c Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Mon, 8 Jan 2024 23:24:31 +0700 Subject: [PATCH 100/120] 32864 cleanup elsewhere condition --- src/pages/home/ReportScreen.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 0e9c2eb95c93..c972618ff1e8 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -303,17 +303,17 @@ function ReportScreen({ if (!match) { return false; } - const email = match[1] ? match[1].trim() : undefined; const title = match[2] ? match[2].trim().replace(/\n/g, ' ') : undefined; - if (title) { - let assignee = {}; - if (email) { - assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {}; - } - Task.createTaskAndNavigate(getReportID(route), title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID); - return true; + if (!title) { + return false; + } + const email = match[1] ? match[1].trim() : undefined; + let assignee = {}; + if (email) { + assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {}; } - return false; + Task.createTaskAndNavigate(getReportID(route), title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID); + return true; }, [allPersonalDetails, report.policyID, route], ); From b5f09b6614cac97bab159dc0bc0aaa22fe908fde Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Tue, 9 Jan 2024 01:24:19 +0700 Subject: [PATCH 101/120] update code review --- src/pages/home/ReportScreen.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index c972618ff1e8..67bd95ab5ce0 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -323,8 +323,8 @@ function ReportScreen({ */ const onSubmitComment = useCallback( (text) => { - const isHandled = handleCreateTask(text); - if (!isHandled) { + const isTaskCreated = handleCreateTask(text); + if (!isTaskCreated) { Report.addComment(getReportID(route), text); } From 864e4c96e1d9c90d54c22fc694bc27fc8e46eaf7 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 9 Jan 2024 15:00:40 +0700 Subject: [PATCH 102/120] resize the parent navigation subtitle of IOU report header --- src/components/AvatarWithDisplayName.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 4580f3b7e4d4..e9e1054427b9 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -141,6 +141,7 @@ function AvatarWithDisplayName({ )} {!!subtitle && ( From 733b3b127506686d76f13cddb784c26db3135b7e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 9 Jan 2024 09:42:32 +0100 Subject: [PATCH 103/120] Updates from main branch --- src/libs/actions/TeachersUnite.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index 066e55a4d365..6aff31ad3836 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -34,44 +34,46 @@ Onyx.connect({ callback: (value) => (allPersonalDetails = value), }); -function referTeachersUniteVolunteer(partnerUserID: string, firstName: string, lastName: string) { - const optimisticPublicRoom = ReportUtils.buildOptimisticChatReport([], CONST.TEACHERS_UNITE.PUBLIC_ROOM_NAME, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, CONST.TEACHERS_UNITE.POLICY_ID); +/** + * @param publicRoomReportID - This is the global reportID for the public room, we'll ignore the optimistic one + */ +function referTeachersUniteVolunteer(partnerUserID: string, firstName: string, lastName: string, policyID: string, publicRoomReportID: string) { + const optimisticPublicRoom = ReportUtils.buildOptimisticChatReport([], CONST.TEACHERS_UNITE.PUBLIC_ROOM_NAME, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, policyID); const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticPublicRoom.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${publicRoomReportID}`, value: { ...optimisticPublicRoom, - reportID: optimisticPublicRoom.reportID, + reportID: publicRoomReportID, policyName: CONST.TEACHERS_UNITE.POLICY_NAME, }, }, ]; type ReferTeachersUniteVolunteerParams = { - publicRoomReportID: string; + reportID: string; firstName: string; lastName: string; partnerUserID: string; }; const parameters: ReferTeachersUniteVolunteerParams = { - publicRoomReportID: optimisticPublicRoom.reportID, + reportID: publicRoomReportID, firstName, lastName, partnerUserID, }; API.write('ReferTeachersUniteVolunteer', parameters, {optimisticData}); - Navigation.dismissModal(CONST.TEACHERS_UNITE.PUBLIC_ROOM_ID); + Navigation.dismissModal(publicRoomReportID); } /** * Optimistically creates a policyExpenseChat for the school principal and passes data to AddSchoolPrincipal */ -function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: string) { +function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: string, policyID: string) { const policyName = CONST.TEACHERS_UNITE.POLICY_NAME; - const policyID = CONST.TEACHERS_UNITE.POLICY_ID; const loggedInEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(sessionEmail); const reportCreationData: ReportCreationData = {}; @@ -178,6 +180,7 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: firstName: string; lastName: string; partnerUserID: string; + policyID: string; reportCreationData: string; }; @@ -185,6 +188,7 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: firstName, lastName, partnerUserID, + policyID, reportCreationData: JSON.stringify(reportCreationData), }; From cfa48926e8119151a13619b5417a31e0389083f6 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 9 Jan 2024 11:45:38 +0100 Subject: [PATCH 104/120] Delete unused old Form component --- src/components/Form.js | 592 ----------------------------------------- 1 file changed, 592 deletions(-) delete mode 100644 src/components/Form.js diff --git a/src/components/Form.js b/src/components/Form.js deleted file mode 100644 index 7b6f587e7bd1..000000000000 --- a/src/components/Form.js +++ /dev/null @@ -1,592 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {Keyboard, ScrollView, StyleSheet} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import FormUtils from '@libs/FormUtils'; -import * as ValidationUtils from '@libs/ValidationUtils'; -import Visibility from '@libs/Visibility'; -import stylePropTypes from '@styles/stylePropTypes'; -import * as FormActions from '@userActions/FormActions'; -import CONST from '@src/CONST'; -import FormAlertWithSubmitButton from './FormAlertWithSubmitButton'; -import FormSubmit from './FormSubmit'; -import networkPropTypes from './networkPropTypes'; -import {withNetwork} from './OnyxProvider'; -import SafeAreaConsumer from './SafeAreaConsumer'; -import ScrollViewWithContext from './ScrollViewWithContext'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; - -const propTypes = { - /** A unique Onyx key identifying the form */ - formID: PropTypes.string.isRequired, - - /** Text to be displayed in the submit button */ - submitButtonText: PropTypes.string, - - /** Controls the submit button's visibility */ - isSubmitButtonVisible: PropTypes.bool, - - /** Callback to validate the form */ - validate: PropTypes.func, - - /** Callback to submit the form */ - onSubmit: PropTypes.func.isRequired, - - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /* Onyx Props */ - - /** Contains the form state that must be accessed outside of the component */ - formState: PropTypes.shape({ - /** Controls the loading state of the form */ - isLoading: PropTypes.bool, - - /** Server side errors keyed by microtime */ - errors: PropTypes.objectOf(PropTypes.string), - - /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - }), - - /** Contains draft values for each input in the form */ - // eslint-disable-next-line react/forbid-prop-types - draftValues: PropTypes.object, - - /** Should the button be enabled when offline */ - enabledWhenOffline: PropTypes.bool, - - /** Whether the form submit action is dangerous */ - isSubmitActionDangerous: PropTypes.bool, - - /** Whether the validate() method should run on input changes */ - shouldValidateOnChange: PropTypes.bool, - - /** Whether the validate() method should run on blur */ - shouldValidateOnBlur: PropTypes.bool, - - /** Whether ScrollWithContext should be used instead of regular ScrollView. - * Set to true when there's a nested Picker component in Form. - */ - scrollContextEnabled: PropTypes.bool, - - /** Container styles */ - style: stylePropTypes, - - /** Submit button container styles */ - // eslint-disable-next-line react/forbid-prop-types - submitButtonStyles: PropTypes.arrayOf(PropTypes.object), - - /** Custom content to display in the footer after submit button */ - footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), - - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** Style for the error message for submit button */ - errorMessageStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - ...withLocalizePropTypes, -}; - -const defaultProps = { - isSubmitButtonVisible: true, - formState: { - isLoading: false, - }, - draftValues: {}, - enabledWhenOffline: false, - isSubmitActionDangerous: false, - scrollContextEnabled: false, - shouldValidateOnChange: true, - shouldValidateOnBlur: true, - footerContent: null, - style: [], - errorMessageStyle: [], - submitButtonStyles: [], - validate: () => ({}), - submitButtonText: '', -}; - -const Form = forwardRef((props, forwardedRef) => { - const styles = useThemeStyles(); - const [errors, setErrors] = useState({}); - const [inputValues, setInputValues] = useState(() => ({...props.draftValues})); - const formRef = useRef(null); - const formContentRef = useRef(null); - const inputRefs = useRef({}); - const touchedInputs = useRef({}); - const focusedInput = useRef(null); - const isFirstRender = useRef(true); - - const {validate, onSubmit, children} = props; - - const hasServerError = useMemo(() => Boolean(props.formState) && !_.isEmpty(props.formState.errors), [props.formState]); - - /** - * @param {Object} values - An object containing the value of each inputID, e.g. {inputID1: value1, inputID2: value2} - * @returns {Object} - An object containing the errors for each inputID, e.g. {inputID1: error1, inputID2: error2} - */ - const onValidate = useCallback( - (values, shouldClearServerError = true) => { - // Trim all string values - const trimmedStringValues = ValidationUtils.prepareValues(values); - - if (shouldClearServerError) { - FormActions.setErrors(props.formID, null); - } - FormActions.setErrorFields(props.formID, null); - - // Run any validations passed as a prop - const validationErrors = validate(trimmedStringValues); - - // Validate the input for html tags. It should supercede any other error - _.each(trimmedStringValues, (inputValue, inputID) => { - // If the input value is empty OR is non-string, we don't need to validate it for HTML tags - if (!inputValue || !_.isString(inputValue)) { - return; - } - const foundHtmlTagIndex = inputValue.search(CONST.VALIDATE_FOR_HTML_TAG_REGEX); - const leadingSpaceIndex = inputValue.search(CONST.VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX); - - // Return early if there are no HTML characters - if (leadingSpaceIndex === -1 && foundHtmlTagIndex === -1) { - return; - } - - const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX); - let isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(inputValue)); - // Check for any matches that the original regex (foundHtmlTagIndex) matched - if (matchedHtmlTags) { - // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed. - for (let i = 0; i < matchedHtmlTags.length; i++) { - const htmlTag = matchedHtmlTags[i]; - isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(htmlTag)); - if (!isMatch) { - break; - } - } - } - - if (isMatch && leadingSpaceIndex === -1) { - return; - } - - // Add a validation error here because it is a string value that contains HTML characters - validationErrors[inputID] = 'common.error.invalidCharacter'; - }); - - if (!_.isObject(validationErrors)) { - throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); - } - - const touchedInputErrors = _.pick(validationErrors, (inputValue, inputID) => Boolean(touchedInputs.current[inputID])); - - if (!_.isEqual(errors, touchedInputErrors)) { - setErrors(touchedInputErrors); - } - - return touchedInputErrors; - }, - [props.formID, validate, errors], - ); - - useEffect(() => { - // We want to skip Form validation on initial render. - // This also avoids a bug where we immediately clear server errors when the loading indicator unmounts and Form remounts with server errors. - if (isFirstRender.current) { - isFirstRender.current = false; - return; - } - - onValidate(inputValues); - - // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to revalidate the form on update if the preferred locale changed on another device so that errors get translated - }, [props.preferredLocale]); - - const errorMessage = useMemo(() => { - const latestErrorMessage = ErrorUtils.getLatestErrorMessage(props.formState); - return typeof latestErrorMessage === 'string' ? latestErrorMessage : ''; - }, [props.formState]); - - /** - * @param {String} inputID - The inputID of the input being touched - */ - const setTouchedInput = useCallback( - (inputID) => { - touchedInputs.current[inputID] = true; - }, - [touchedInputs], - ); - - const submit = useCallback(() => { - // Return early if the form is already submitting to avoid duplicate submission - if (props.formState.isLoading) { - return; - } - - // Trim all string values - const trimmedStringValues = ValidationUtils.prepareValues(inputValues); - - // Touches all form inputs so we can validate the entire form - _.each(inputRefs.current, (inputRef, inputID) => (touchedInputs.current[inputID] = true)); - - // Validate form and return early if any errors are found - if (!_.isEmpty(onValidate(trimmedStringValues))) { - return; - } - - // Do not submit form if network is offline and the form is not enabled when offline - if (props.network.isOffline && !props.enabledWhenOffline) { - return; - } - - // Call submit handler - onSubmit(trimmedStringValues); - }, [props.formState.isLoading, props.network.isOffline, props.enabledWhenOffline, inputValues, onValidate, onSubmit]); - - /** - * Resets the form - */ - const resetForm = useCallback( - (optionalValue) => { - _.each(inputValues, (inputRef, inputID) => { - setInputValues((prevState) => { - const copyPrevState = _.clone(prevState); - - touchedInputs.current[inputID] = false; - copyPrevState[inputID] = optionalValue[inputID] || ''; - - return copyPrevState; - }); - }); - setErrors({}); - }, - [inputValues], - ); - - useImperativeHandle(forwardedRef, () => ({ - resetForm, - })); - - /** - * Loops over Form's children and automatically supplies Form props to them - * - * @param {Array | Function | Node} children - An array containing all Form children - * @returns {React.Component} - */ - const childrenWrapperWithProps = useCallback( - (childNodes) => { - const childrenElements = React.Children.map(childNodes, (child) => { - // Just render the child if it is not a valid React element, e.g. text within a component - if (!React.isValidElement(child)) { - return child; - } - - // Depth first traversal of the render tree as the input element is likely to be the last node - if (child.props.children) { - return React.cloneElement(child, { - children: childrenWrapperWithProps(child.props.children), - }); - } - - // Look for any inputs nested in a custom component, e.g AddressForm or IdentityForm - if (_.isFunction(child.type)) { - const childNode = new child.type(child.props); - - // If the custom component has a render method, use it to get the nested children - const nestedChildren = _.isFunction(childNode.render) ? childNode.render() : childNode; - - // Render the custom component if it's a valid React element - // If the custom component has nested children, Loop over them and supply From props - if (React.isValidElement(nestedChildren) || lodashGet(nestedChildren, 'props.children')) { - return childrenWrapperWithProps(nestedChildren); - } - - // Just render the child if it's custom component not a valid React element, or if it hasn't children - return child; - } - - // We check if the child has the inputID prop. - // We don't want to pass form props to non form components, e.g. View, Text, etc - if (!child.props.inputID) { - return child; - } - - // We clone the child passing down all form props - const inputID = child.props.inputID; - let defaultValue; - - // We need to make sure that checkboxes have correct - // value assigned from the list of draft values - // https://github.com/Expensify/App/issues/16885#issuecomment-1520846065 - if (_.isBoolean(props.draftValues[inputID])) { - defaultValue = props.draftValues[inputID]; - } else { - defaultValue = props.draftValues[inputID] || child.props.defaultValue; - } - - // We want to initialize the input value if it's undefined - if (_.isUndefined(inputValues[inputID])) { - // eslint-disable-next-line es/no-nullish-coalescing-operators - inputValues[inputID] = defaultValue ?? ''; - } - - // We force the form to set the input value from the defaultValue props if there is a saved valid value - if (child.props.shouldUseDefaultValue) { - inputValues[inputID] = child.props.defaultValue; - } - - if (!_.isUndefined(child.props.value)) { - inputValues[inputID] = child.props.value; - } - - const errorFields = lodashGet(props.formState, 'errorFields', {}); - const fieldErrorMessage = - _.chain(errorFields[inputID]) - .keys() - .sortBy() - .reverse() - .map((key) => errorFields[inputID][key]) - .first() - .value() || ''; - - return React.cloneElement(child, { - ref: (node) => { - inputRefs.current[inputID] = node; - - const {ref} = child; - if (_.isFunction(ref)) { - ref(node); - } - }, - value: inputValues[inputID], - // As the text input is controlled, we never set the defaultValue prop - // as this is already happening by the value prop. - defaultValue: undefined, - errorText: errors[inputID] || fieldErrorMessage, - onFocus: (event) => { - focusedInput.current = inputID; - if (_.isFunction(child.props.onFocus)) { - child.props.onFocus(event); - } - }, - onBlur: (event) => { - // Only run validation when user proactively blurs the input. - if (Visibility.isVisible() && Visibility.hasFocus()) { - const relatedTargetId = lodashGet(event, 'nativeEvent.relatedTarget.id'); - // We delay the validation in order to prevent Checkbox loss of focus when - // the user are focusing a TextInput and proceeds to toggle a CheckBox in - // web and mobile web platforms. - - setTimeout(() => { - if ( - relatedTargetId && - _.includes([CONST.OVERLAY.BOTTOM_BUTTON_NATIVE_ID, CONST.OVERLAY.TOP_BUTTON_NATIVE_ID, CONST.BACK_BUTTON_NATIVE_ID], relatedTargetId) - ) { - return; - } - setTouchedInput(inputID); - if (props.shouldValidateOnBlur) { - onValidate(inputValues, !hasServerError); - } - }, 200); - } - - if (_.isFunction(child.props.onBlur)) { - child.props.onBlur(event); - } - }, - onTouched: () => { - setTouchedInput(inputID); - }, - onInputChange: (value, key) => { - const inputKey = key || inputID; - - if (focusedInput.current && focusedInput.current !== inputKey) { - setTouchedInput(focusedInput.current); - } - - setInputValues((prevState) => { - const newState = { - ...prevState, - [inputKey]: value, - }; - - if (props.shouldValidateOnChange) { - onValidate(newState); - } - return newState; - }); - - if (child.props.shouldSaveDraft) { - FormActions.setDraftValues(props.formID, {[inputKey]: value}); - } - - if (child.props.onValueChange) { - child.props.onValueChange(value, inputKey); - } - }, - }); - }); - - return childrenElements; - }, - [ - errors, - inputRefs, - inputValues, - onValidate, - props.draftValues, - props.formID, - props.formState, - setTouchedInput, - props.shouldValidateOnBlur, - props.shouldValidateOnChange, - hasServerError, - ], - ); - - const scrollViewContent = useCallback( - (safeAreaPaddingBottomStyle) => ( - - {childrenWrapperWithProps(_.isFunction(children) ? children({inputValues}) : children)} - {props.isSubmitButtonVisible && ( - 0 || Boolean(errorMessage) || !_.isEmpty(props.formState.errorFields)} - isLoading={props.formState.isLoading} - message={_.isEmpty(props.formState.errorFields) ? errorMessage : null} - onSubmit={submit} - footerContent={props.footerContent} - onFixTheErrorsLinkPressed={() => { - const errorFields = !_.isEmpty(errors) ? errors : props.formState.errorFields; - const focusKey = _.find(_.keys(inputRefs.current), (key) => _.keys(errorFields).includes(key)); - const focusInput = inputRefs.current[focusKey]; - - // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. - if (typeof focusInput.isFocused !== 'function') { - Keyboard.dismiss(); - } - - // We subtract 10 to scroll slightly above the input - if (focusInput.measureLayout && typeof focusInput.measureLayout === 'function') { - // We measure relative to the content root, not the scroll view, as that gives - // consistent results across mobile and web - focusInput.measureLayout(formContentRef.current, (x, y) => formRef.current.scrollTo({y: y - 10, animated: false})); - } - - // Focus the input after scrolling, as on the Web it gives a slightly better visual result - if (focusInput.focus && typeof focusInput.focus === 'function') { - focusInput.focus(); - } - }} - containerStyles={[styles.mh0, styles.mt5, styles.flex1, ...props.submitButtonStyles]} - enabledWhenOffline={props.enabledWhenOffline} - isSubmitActionDangerous={props.isSubmitActionDangerous} - useSmallerSubmitButtonSize={props.useSmallerSubmitButtonSize} - disablePressOnEnter - errorMessageStyle={props.errorMessageStyle} - /> - )} - - ), - - [ - props.style, - props.isSubmitButtonVisible, - props.submitButtonText, - props.useSmallerSubmitButtonSize, - props.errorMessageStyle, - props.formState.errorFields, - props.formState.isLoading, - props.footerContent, - props.submitButtonStyles, - props.enabledWhenOffline, - props.isSubmitActionDangerous, - submit, - childrenWrapperWithProps, - children, - inputValues, - errors, - errorMessage, - styles.mh0, - styles.mt5, - styles.flex1, - ], - ); - - useEffect(() => { - _.each(inputRefs.current, (inputRef, inputID) => { - if (inputRef) { - return; - } - - delete inputRefs.current[inputID]; - delete touchedInputs.current[inputID]; - - setInputValues((prevState) => { - const copyPrevState = _.clone(prevState); - - delete copyPrevState[inputID]; - - return copyPrevState; - }); - }); - // We need to verify that all references and values are still actual. - // We should not store it when e.g. some input has been unmounted. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [children]); - - return ( - - {({safeAreaPaddingBottomStyle}) => - props.scrollContextEnabled ? ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) : ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) - } - - ); -}); - -Form.displayName = 'Form'; -Form.propTypes = propTypes; -Form.defaultProps = defaultProps; - -export default compose( - withLocalize, - withNetwork(), - withOnyx({ - formState: { - key: (props) => props.formID, - }, - draftValues: { - key: (props) => FormUtils.getDraftKey(props.formID), - }, - }), -)(Form); From d8dc8d4249f254ba9d4c607e77bb7890d6d147b1 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Tue, 9 Jan 2024 22:05:46 +0700 Subject: [PATCH 105/120] update early condition --- src/pages/home/ReportScreen.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index ead72d760c1d..9d8564a835a0 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -329,9 +329,10 @@ function ReportScreen({ const onSubmitComment = useCallback( (text) => { const isTaskCreated = handleCreateTask(text); - if (!isTaskCreated) { - Report.addComment(getReportID(route), text); + if (isTaskCreated) { + return; } + Report.addComment(getReportID(route), text); }, [route, handleCreateTask], ); From d8b9af7b44d618088de2e72a895e8bfe0cd8539c Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 9 Jan 2024 17:03:40 +0100 Subject: [PATCH 106/120] fix: fix typing --- src/libs/actions/PersonalDetails.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 18320f3f2406..508cca34fb88 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -45,7 +45,7 @@ Onyx.connect({ /** * Creates a new displayName for a user based on passed personal details or login. */ -function createDisplayName(login: string, personalDetails: Pick): string { +function createDisplayName(login: string, personalDetails: Pick | OnyxEntry): string { // If we have a number like +15857527441@expensify.sms then let's remove @expensify.sms and format it // so that the option looks cleaner in our UI. const userLogin = LocalePhoneNumber.formatPhoneNumber(login); From 5d39c82e6e8d54df4e786c449760bca225315f15 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 9 Jan 2024 09:04:18 -0700 Subject: [PATCH 107/120] close modal when merchant is empty --- src/pages/EditRequestPage.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index b322f4eb106c..fe43d96001a0 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -156,13 +156,8 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep return; } - // This is possible only in case of IOU requests. - if (newTrimmedMerchant === '') { - IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); - return; - } - - IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, newMerchant); + // An empty newTrimmedMerchant is only possible for the P2P IOU case + IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, newTrimmedMerchant || CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); Navigation.dismissModal(); }, [transactionMerchant, transaction, report], From e751101aaa59048873157abcbc90b692e7aa4c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 9 Jan 2024 17:45:42 +0100 Subject: [PATCH 108/120] fix #33257 issues --- src/components/Tooltip/BaseTooltip/index.tsx | 2 +- src/pages/home/report/ReportActionItem.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Tooltip/BaseTooltip/index.tsx b/src/components/Tooltip/BaseTooltip/index.tsx index 2adde759b847..2487cbbfe092 100644 --- a/src/components/Tooltip/BaseTooltip/index.tsx +++ b/src/components/Tooltip/BaseTooltip/index.tsx @@ -189,7 +189,7 @@ function Tooltip( (e: MouseEvent) => { updateTargetAndMousePosition(e); if (React.isValidElement(children)) { - children.props.onMouseEnter(e); + children.props.onMouseEnter?.(e); } }, [children, updateTargetAndMousePosition], diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index f48fdd1d53dc..5d8fd2da2558 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -671,7 +671,7 @@ function ReportActionItem(props) { > {(hovered) => ( From 1e876b07e7899e1346fec82bb9c5292bbdd5feac Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Tue, 9 Jan 2024 12:52:07 -0500 Subject: [PATCH 109/120] add CFBundleVersion and CFBundleShortVersionString --- ios/NotificationServiceExtension/Info.plist | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 57421ebf9b75..ccc7422fe3b4 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -9,5 +9,9 @@ NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).NotificationService + CFBundleVersion + 1.4.23.0 + CFBundleShortVersionString + 1.4.23 - + \ No newline at end of file From 16344ae8110ad3439e95c5a7c00a0fb93f33ba71 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Tue, 9 Jan 2024 12:57:46 -0500 Subject: [PATCH 110/120] update NotificationServiceExtension version --- .github/libs/nativeVersionUpdater.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/libs/nativeVersionUpdater.js b/.github/libs/nativeVersionUpdater.js index 07d36d823c78..ab129f4eb04a 100644 --- a/.github/libs/nativeVersionUpdater.js +++ b/.github/libs/nativeVersionUpdater.js @@ -10,6 +10,7 @@ const getBuildVersion = require('semver/functions/prerelease'); const BUILD_GRADLE_PATH = process.env.NODE_ENV === 'test' ? path.resolve(__dirname, '../../android/app/build.gradle') : './android/app/build.gradle'; const PLIST_PATH = './ios/NewExpensify/Info.plist'; const PLIST_PATH_TEST = './ios/NewExpensifyTests/Info.plist'; +const PLIST_PATH_NSE = './ios/NotificationServiceExtension/Info.plist'; exports.BUILD_GRADLE_PATH = BUILD_GRADLE_PATH; exports.PLIST_PATH = PLIST_PATH; @@ -81,8 +82,10 @@ exports.updateiOSVersion = function updateiOSVersion(version) { // Update Plists execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${shortVersion}" ${PLIST_PATH}`); execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${shortVersion}" ${PLIST_PATH_TEST}`); + execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${shortVersion}" ${PLIST_PATH_NSE}`); execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${cfVersion}" ${PLIST_PATH}`); execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${cfVersion}" ${PLIST_PATH_TEST}`); + execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${cfVersion}" ${PLIST_PATH_NSE}`); // Return the cfVersion so we can set the NEW_IOS_VERSION in ios.yml return cfVersion; From 497babd2a2e65042eacda68625a37bf2e5a6613e Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Tue, 9 Jan 2024 14:31:30 -0500 Subject: [PATCH 111/120] build gh actions --- .github/actions/javascript/bumpVersion/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js index 31a4c6d4246a..1132c137061e 100644 --- a/.github/actions/javascript/bumpVersion/index.js +++ b/.github/actions/javascript/bumpVersion/index.js @@ -19,6 +19,7 @@ const getBuildVersion = __nccwpck_require__(4016); const BUILD_GRADLE_PATH = process.env.NODE_ENV === 'test' ? path.resolve(__dirname, '../../android/app/build.gradle') : './android/app/build.gradle'; const PLIST_PATH = './ios/NewExpensify/Info.plist'; const PLIST_PATH_TEST = './ios/NewExpensifyTests/Info.plist'; +const PLIST_PATH_NSE = './ios/NotificationServiceExtension/Info.plist'; exports.BUILD_GRADLE_PATH = BUILD_GRADLE_PATH; exports.PLIST_PATH = PLIST_PATH; @@ -90,8 +91,10 @@ exports.updateiOSVersion = function updateiOSVersion(version) { // Update Plists execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${shortVersion}" ${PLIST_PATH}`); execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${shortVersion}" ${PLIST_PATH_TEST}`); + execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${shortVersion}" ${PLIST_PATH_NSE}`); execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${cfVersion}" ${PLIST_PATH}`); execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${cfVersion}" ${PLIST_PATH_TEST}`); + execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${cfVersion}" ${PLIST_PATH_NSE}`); // Return the cfVersion so we can set the NEW_IOS_VERSION in ios.yml return cfVersion; From 5994dbd87b421f90d64b62f9ca577f36e527cc9d Mon Sep 17 00:00:00 2001 From: situchan Date: Wed, 10 Jan 2024 02:48:18 +0600 Subject: [PATCH 112/120] fix forwarding ref in FAB --- src/components/FloatingActionButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index 2e9996a92f87..5f75bf535319 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -1,5 +1,5 @@ import type {ForwardedRef} from 'react'; -import React, {useEffect, useRef} from 'react'; +import React, {forwardRef, useEffect, useRef} from 'react'; import type {GestureResponderEvent, Role} from 'react-native'; import {Platform, View} from 'react-native'; import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; @@ -134,4 +134,4 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo FloatingActionButton.displayName = 'FloatingActionButton'; -export default FloatingActionButton; +export default forwardRef(FloatingActionButton); From 3acb14a49438b486e9e77ffa1c6f797cccfa2ea2 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Tue, 9 Jan 2024 21:21:40 +0000 Subject: [PATCH 113/120] Update version to 1.4.23-1 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 395e87664e99..7947803db0db 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -96,8 +96,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042300 - versionName "1.4.23-0" + versionCode 1001042301 + versionName "1.4.23-1" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ff0d5c910e6e..bb0f5fb45fd7 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.23.0 + 1.4.23.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 80cb37367088..64119cf8c667 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.23.0 + 1.4.23.1 diff --git a/package-lock.json b/package-lock.json index 0490c1c6eb34..a0c2d3ede6ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.23-0", + "version": "1.4.23-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.23-0", + "version": "1.4.23-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b3809fa7e414..aaaa78237618 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.23-0", + "version": "1.4.23-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From be0cfd9a8939f34900101e15ea9221801c8569b5 Mon Sep 17 00:00:00 2001 From: James Dean Date: Tue, 9 Jan 2024 13:22:18 -0800 Subject: [PATCH 114/120] Update Referral-Program.md Updating to more SEO-friendly copy with suggestions from FPS. --- .../get-paid-back/Referral-Program.md | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/docs/articles/expensify-classic/get-paid-back/Referral-Program.md b/docs/articles/expensify-classic/get-paid-back/Referral-Program.md index 4cc646c613a1..24605dd17d3f 100644 --- a/docs/articles/expensify-classic/get-paid-back/Referral-Program.md +++ b/docs/articles/expensify-classic/get-paid-back/Referral-Program.md @@ -1,56 +1,49 @@ --- -title: Expensify Referral Program -description: Send your joining link, submit a receipt or invoice, and we'll pay you if your referral adopts Expensify. +title: Earn money with Expensify referrals +description: Get paid with the Expensify referral program! Share your link, earn $250 per successful sign-up, and enjoy unlimited income potential. It’s that easy. redirect_from: articles/other/Referral-Program/ --- -# About +# Earn money with Expensify referrals -Expensify has grown thanks to our users who love Expensify so much that they tell their friends, colleagues, managers, and fellow business founders to use it, too. +Picture this: You've found Expensify and it's transformed your approach to expense management and financial organization. You love it so much that you can't help but recommend it to friends, family, and colleagues. Wouldn’t it be nice if you could get rewarded just for spreading the word? -As a thank you, every time you bring a new user into the platform who directly or indirectly leads to the adoption of a paid annual plan on Expensify, you will earn $250. +With Expensify referrals, you can. Every time someone you invite to the platform signs up for a paid annual plan on Expensify, you’ll earn $250. Think of it as a thank-you gift from us to you! -# How to get paid for referring people to Expensify +## How to get paid for Expensify referrals -1. Submit a report or invoice, or share your referral link with anyone you know who is spending too much time on expenses, or works at a company that could benefit from using Expensify. +Here are a few easy ways to get paid for Expensify friend referrals: -2. You will get $250 for any referred business that commits to an annual subscription, has 2 or more active users, and makes two monthly payments. +- Submit an expense report to your boss (even just one receipt!) +- Send an invoice to a client or customer +- Share your referral link with a friend + - To find your referral link, open your Expensify mobile app and go to **Settings > Refer a friend, earn cash! > Share invite link**. -That’s right! You can refer anyone working at any company you know. +**If the person you referred commits to an annual subscription with two or more active users and makes two monthly payments, you’ll get $250. Cha-ching!** -If their company goes on to become an Expensify customer with an annual subscription, and you are the earliest recorded referrer of a user on that company’s paid Expensify Policy, you'll get paid a referral reward. +## Who can you refer? -The best way to start is to submit any receipt to your manager (you'll get paid back and set yourself up for $250 if they start a subscription: win-win!) +You can refer anyone who might benefit from Expensify. Seriously. Anybody. -Referral rewards for the Spring/Summer 2023 campaign will be paid by direct deposit. +Know a small business owner? Refer them! An [accountant](https://use.expensify.com/accountants-program)? Refer them! A best friend from childhood who keeps losing paper receipts? Refer them! -{% include faq-begin.md %} +Plus, you can [refer an unlimited amount of new users](https://use.expensify.com/blog/earn-50000-by-referring-your-friends-to-expensify/) with the Expensify referral program, so your earning potential is truly sky-high. -- **How will I know if I am the first person to refer a company to Expensify?** +## Common questions about Expensify benefits -Successful referrers are notified after their referral pays for 2 months of an annual subscription. We will check for the earliest recorded referrer of a user on the policy, and if that is you, then we will let you know. +Still have questions about the Expensify referral program? We’ve got answers. Check out our FAQ below. -- **How will you pay me if I am successful?** +### How will I know if I am the first person to refer someone to Expensify? -In the Spring 2023 campaign, Expensify will be paying successful referrers via direct deposit to the Deposit-Only account you have on file. Referral payouts will happen once a month for the duration of the campaign. If you do not have a Deposit-Only account at the time of your referral payout, your deposit will be processed in the next batch. +You’ll know if you’re the first person to refer someone to Expensify if we reach out to let you know that they’ve successfully adopted Expensify and have paid for two months of an annual subscription. -Learn how to add a Deposit-Only account [here](https://community.expensify.com/discussion/4641/how-to-add-a-deposit-only-bank-account-both-personal-and-business). +Simply put, we check for the earliest recorded referrer of a member on the workspace, and if that’s you, then we’ll let you know. -- **I’m outside of the US, how do I get paid?** +### My referral wasn’t counted! How can I appeal? -While our referral payouts are in USD, you will be able to get paid via a Wise Borderless account. Learn more [here](https://community.expensify.com/discussion/5940/how-to-get-reimbursed-outside-the-us-with-wise-for-non-us-employees). +If you think your Expensify friend referral wasn’t counted, please send a message to concierge@expensify.com with the email of the person you referred. Our team will review the referral and get back to you. -- **My referral wasn’t counted! How can I appeal?** +## Share the Expensify love — and get paid in the process -Expensify reserves the right to modify the terms of the referral program at any time, and pays out referral bonuses for eligible companies at its own discretion. - -Please send a message to concierge@expensify.com with the billing owner of the company you have referred and our team will review the referral and get back to you. - -- **Where can I find my referral link?** - -Expensify members who are opted-in for our newsletters will have received an email containing their unique referral link. - -On the mobile app, go to **Settings** > **Invite a Friend** > **Share Invite Link** to retrieve your referral link. - -{% include faq-end.md %} +Who needs a side hustle when you have Expensify? With Expensify benefits, it’s not just about managing your expenses — it's about expanding your income too. Share your Expensify referral link now or send over an invoice to unlock unlimited earning potential. From 6cead54f66c6ae4a7536060adf42bd18551fd639 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Tue, 9 Jan 2024 21:47:10 +0000 Subject: [PATCH 115/120] Update version to 1.4.23-2 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7947803db0db..bf11a970145d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -96,8 +96,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042301 - versionName "1.4.23-1" + versionCode 1001042302 + versionName "1.4.23-2" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index bb0f5fb45fd7..ce787768bb02 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.23.1 + 1.4.23.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 64119cf8c667..608b9ecbe60f 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.23.1 + 1.4.23.2 diff --git a/package-lock.json b/package-lock.json index a0c2d3ede6ef..3caa3329fec5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.23-1", + "version": "1.4.23-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.23-1", + "version": "1.4.23-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index aaaa78237618..8f510f6ec9d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.23-1", + "version": "1.4.23-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From d6da26903049564899a5149325a275f5cc70bd1c Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 9 Jan 2024 22:56:14 +0100 Subject: [PATCH 116/120] Allow admins/managers to see RBR errors on reports --- src/components/ReportActionItem/MoneyRequestView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 37ff163f23c8..183134af04aa 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -209,7 +209,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate let hasErrors = false; if (hasReceipt) { receiptURIs = ReceiptUtils.getThumbnailAndImageURIs(transaction); - hasErrors = canEditReceipt && TransactionUtils.hasMissingSmartscanFields(transaction); + hasErrors = canEdit && TransactionUtils.hasMissingSmartscanFields(transaction); } const pendingAction = lodashGet(transaction, 'pendingAction'); From 9ec49ace31be72fdfa678e9cf9bc3b2f60edcf0d Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Wed, 10 Jan 2024 04:40:15 +0100 Subject: [PATCH 117/120] rename style container for the native TextInput --- src/components/TextInput/BaseTextInput/index.native.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index f4cc1ee9e0ba..d19d835d68bb 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -37,6 +37,7 @@ function BaseTextInput( errorText = '', icon = null, textInputContainerStyles, + touchableInputWrapperStyle, containerStyles, inputStyle, forceActiveLabel = false, @@ -273,7 +274,7 @@ function BaseTextInput( style={[ autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, typeof maxHeight === 'number' ? maxHeight : 0), !isMultiline && styles.componentHeightLarge, - containerStyles, + touchableInputWrapperStyle, ]} > Date: Wed, 10 Jan 2024 03:50:11 +0000 Subject: [PATCH 118/120] Update version to 1.4.23-3 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index bf11a970145d..73bf6646f5d4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -96,8 +96,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042302 - versionName "1.4.23-2" + versionCode 1001042303 + versionName "1.4.23-3" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ce787768bb02..fa5ecd656ee3 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.23.2 + 1.4.23.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 608b9ecbe60f..ab6974829a7a 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.23.2 + 1.4.23.3 diff --git a/package-lock.json b/package-lock.json index 3caa3329fec5..e4418403ff87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.23-2", + "version": "1.4.23-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.23-2", + "version": "1.4.23-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8f510f6ec9d9..ebbb559da480 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.23-2", + "version": "1.4.23-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From fc3e51bb18e7a29a09b63b0bcdb1f21b806339df Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 10 Jan 2024 05:31:08 +0000 Subject: [PATCH 119/120] Update version to 1.4.23-4 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 73bf6646f5d4..3073fcce6044 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -96,8 +96,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042303 - versionName "1.4.23-3" + versionCode 1001042304 + versionName "1.4.23-4" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index fa5ecd656ee3..a15de82fdd47 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.23.3 + 1.4.23.4 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index ab6974829a7a..56b121a6d6e2 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.23.3 + 1.4.23.4 diff --git a/package-lock.json b/package-lock.json index e4418403ff87..5178948f0973 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.23-3", + "version": "1.4.23-4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.23-3", + "version": "1.4.23-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ebbb559da480..2d3bd42ebb61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.23-3", + "version": "1.4.23-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 76e5b1cbd80e7dc55c52f47c5572b2984a7f7ed6 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 10 Jan 2024 05:53:26 +0000 Subject: [PATCH 120/120] Update version to 1.4.24-0 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 4 ++-- ios/NewExpensifyTests/Info.plist | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 3073fcce6044..645f36ef876a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -96,8 +96,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042304 - versionName "1.4.23-4" + versionCode 1001042400 + versionName "1.4.24-0" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index a15de82fdd47..443e35990496 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.23 + 1.4.24 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.23.4 + 1.4.24.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 56b121a6d6e2..7756d9837e9a 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.23 + 1.4.24 CFBundleSignature ???? CFBundleVersion - 1.4.23.4 + 1.4.24.0 diff --git a/package-lock.json b/package-lock.json index 5178948f0973..b1af06e89fed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.23-4", + "version": "1.4.24-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.23-4", + "version": "1.4.24-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2d3bd42ebb61..192f42ee45fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.23-4", + "version": "1.4.24-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",