From 3f9b1816e0787ae85be72b92dd415ebcc29ec9ad Mon Sep 17 00:00:00 2001 From: Srikar Parsi Date: Thu, 15 Jun 2023 16:55:04 -0400 Subject: [PATCH 001/325] Convert Date Picker to Functional Component --- src/components/DatePicker/index.js | 73 +++++++++++++----------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js index e90fd32f9952..945de49dd786 100644 --- a/src/components/DatePicker/index.js +++ b/src/components/DatePicker/index.js @@ -1,10 +1,10 @@ -import React from 'react'; import moment from 'moment'; +import React, { useEffect, useRef } from 'react'; import _ from 'underscore'; -import TextInput from '../TextInput'; import CONST from '../../CONST'; -import {propTypes, defaultProps} from './datepickerPropTypes'; -import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions'; +import TextInput from '../TextInput'; +import withWindowDimensions, { windowDimensionsPropTypes } from '../withWindowDimensions'; +import { defaultProps, propTypes } from './datepickerPropTypes'; import './styles.css'; const datePickerPropTypes = { @@ -12,37 +12,30 @@ const datePickerPropTypes = { ...windowDimensionsPropTypes, }; -class DatePicker extends React.Component { - constructor(props) { - super(props); +function DatePicker(props) { + const inputRef = useRef(null); + const defaultValue = props.defaultValue ? moment(props.defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : ''; - this.setDate = this.setDate.bind(this); - this.showDatepicker = this.showDatepicker.bind(this); - - this.defaultValue = props.defaultValue ? moment(props.defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : ''; - } - - componentDidMount() { - // Adds nice native datepicker on web/desktop. Not possible to set this through props - this.inputRef.setAttribute('type', 'date'); - this.inputRef.setAttribute('max', moment(this.props.maxDate).format(CONST.DATE.MOMENT_FORMAT_STRING)); - this.inputRef.setAttribute('min', moment(this.props.minDate).format(CONST.DATE.MOMENT_FORMAT_STRING)); - this.inputRef.classList.add('expensify-datepicker'); - } + useEffect(() => { + inputRef.setAttribute('type', 'date'); + inputRef.setAttribute('max', moment(props.maxDate).format(CONST.DATE.MOMENT_FORMAT_STRING)); + inputRef.setAttribute('min', moment(props.minDate).format(CONST.DATE.MOMENT_FORMAT_STRING)); + inputRef.classList.add('expensify-datepicker'); + }, []); /** * Trigger the `onChange` handler when the user input has a complete date or is cleared * @param {String} text */ - setDate(text) { + const setDate = (text) => { if (!text) { - this.props.onInputChange(''); + props.onInputChange(''); return; } const asMoment = moment(text, true); if (asMoment.isValid()) { - this.props.onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING)); + props.onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING)); } } @@ -51,38 +44,36 @@ class DatePicker extends React.Component { * On mWeb the user needs to tap on the field again in order to bring the datepicker. But our current styles * don't make this very obvious. To avoid confusion we open the datepicker when the user focuses the field */ - showDatepicker() { - if (!this.inputRef) { + const showDatepicker = () => { + if (!inputRef.current) { return; } - this.inputRef.click(); + inputRef.current.click(); } - render() { return ( { - this.inputRef = el; + inputRef.current = el; - if (_.isFunction(this.props.innerRef)) { - this.props.innerRef(el); + if (_.isFunction(props.innerRef)) { + props.innerRef(el); } }} - onFocus={this.showDatepicker} - label={this.props.label} - onInputChange={this.setDate} - value={this.props.value} - defaultValue={this.defaultValue} - placeholder={this.props.placeholder} - errorText={this.props.errorText} - containerStyles={this.props.containerStyles} - disabled={this.props.disabled} - onBlur={this.props.onBlur} + onFocus={showDatepicker} + label={props.label} + onInputChange={setDate} + value={props.value} + defaultValue={defaultValue} + placeholder={props.placeholder} + errorText={props.errorText} + containerStyles={props.containerStyles} + disabled={props.disabled} + onBlur={props.onBlur} /> ); - } } DatePicker.propTypes = datePickerPropTypes; From 6c2c14092a994ee99839db751c576e550879f7a1 Mon Sep 17 00:00:00 2001 From: Edu Date: Fri, 9 Jun 2023 16:42:12 +0200 Subject: [PATCH 002/325] push to page menu item --- src/ROUTES.js | 5 +- src/components/AddressSearch/index.js | 28 +++- src/components/CountryPicker.js | 102 +++++++------- src/components/MenuItemWithTopDescription.js | 19 ++- src/components/OptionsSelectorWithSearch.js | 105 +++++++++++++++ src/components/StatePicker.js | 127 +++++++++++------- src/components/TextInput/BaseTextInput.js | 1 + .../TextInput/baseTextInputPropTypes.js | 5 + .../AppNavigator/ModalStackNavigators.js | 22 +++ .../Navigators/RightModalNavigator.js | 10 ++ src/libs/Navigation/linkingConfig.js | 10 ++ src/libs/actions/PersonalDetails.js | 15 +++ src/pages/CountrySelectorPage.js | 83 ++++++++++++ src/pages/ReimbursementAccount/AddressForm.js | 5 +- src/pages/ReimbursementAccount/CompanyStep.js | 4 +- .../ReimbursementAccount/RequestorStep.js | 2 +- src/pages/StateSelectorPage.js | 83 ++++++++++++ .../Profile/PersonalDetails/AddressPage.js | 111 ++++++++------- src/styles/styles.js | 5 + 19 files changed, 580 insertions(+), 162 deletions(-) create mode 100644 src/components/OptionsSelectorWithSearch.js create mode 100644 src/pages/CountrySelectorPage.js create mode 100644 src/pages/StateSelectorPage.js diff --git a/src/ROUTES.js b/src/ROUTES.js index 966c3d0c5a1a..4cc2632e1bf8 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -70,7 +70,10 @@ export default { getReportShareCodeRoute: (reportID) => `r/${reportID}/details/shareCode`, SELECT_YEAR: 'select-year', getYearSelectionRoute: (minYear, maxYear, currYear, backTo) => `select-year?min=${minYear}&max=${maxYear}&year=${currYear}&backTo=${backTo}`, - + SETTINGS_SELECT_COUNTRY: 'select-country', + getCountrySelectionRoute: (countryISO, backTo) => `select-country?countryISO=${countryISO}&backTo=${backTo}`, + SETTINGS_USA_STATES: 'select-usa-states', + getUsaStateSelectionRoute: (stateISO, backTo) => `select-usa-states?stateISO=${stateISO}&backTo=${backTo}`, /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ CONCIERGE: 'concierge', diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 9699eb9aab94..cc1115c964b3 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -48,6 +48,12 @@ const propTypes = { /** A callback function when the value of this field has changed */ onInputChange: PropTypes.func.isRequired, + /** A callback when a new country has changed */ + onCountryChange: PropTypes.func, + + /** A callback when a new state has changed */ + onStateChange: PropTypes.func, + /** Customize the TextInput container */ // eslint-disable-next-line react/forbid-prop-types containerStyles: PropTypes.arrayOf(PropTypes.object), @@ -73,6 +79,8 @@ const defaultProps = { inputID: undefined, shouldSaveDraft: false, onBlur: () => {}, + onCountryChange: () => {}, + onStateChange: () => {}, errorText: '', hint: '', value: undefined, @@ -160,9 +168,18 @@ function AddressSearch(props) { state: state || stateAutoCompleteFallback, }; + const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country); + if (isValidCountryCode) { + values.country = country; + if (props.onCountryChange) { + props.onCountryChange(country); + } + } + // If the address is not in the US, use the full length state name since we're displaying the address's // state / province in a TextInput instead of in a picker. - if (country !== CONST.COUNTRY.US) { + const isUS = country === CONST.COUNTRY.US; + if (isUS) { values.state = longStateName; } @@ -172,17 +189,16 @@ function AddressSearch(props) { values.state = stateFallback; } + if (props.onStateChange) { + props.onStateChange(isUS ? state : longStateName, isUS); + } + // Not all pages define the Address Line 2 field, so in that case we append any additional address details // (e.g. Apt #) to Address Line 1 if (subpremise && typeof props.renamedInputKeys.street2 === 'undefined') { values.street += `, ${subpremise}`; } - const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country); - if (isValidCountryCode) { - values.country = country; - } - if (props.inputID) { _.each(values, (value, key) => { const inputKey = lodashGet(props.renamedInputKeys, key, key); diff --git a/src/components/CountryPicker.js b/src/components/CountryPicker.js index 61bfd26a0e67..8c13ae99d39b 100644 --- a/src/components/CountryPicker.js +++ b/src/components/CountryPicker.js @@ -1,67 +1,79 @@ -import _ from 'underscore'; -import React, {forwardRef} from 'react'; +import React, {useCallback, useRef, useEffect} from 'react'; +import {View} from 'react-native'; import PropTypes from 'prop-types'; -import Picker from './Picker'; +import sizes from '../styles/variables'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import * as PersonalDetails from '../libs/actions/PersonalDetails'; +import Navigation from '../libs/Navigation/Navigation'; +import ROUTES from '../ROUTES'; +import FormHelpMessage from './FormHelpMessage'; const propTypes = { - /** The label for the field */ - label: PropTypes.string, + /** The ISO code of the country */ + countryISO: PropTypes.string, - /** A callback method that is called when the value changes and it receives the selected value as an argument. */ - onInputChange: PropTypes.func.isRequired, + /** The ISO selected from CountrySelector */ + selectedCountryISO: PropTypes.string, - /** The value that needs to be selected */ - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** The ID used to uniquely identify the input in a form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - shouldSaveDraft: PropTypes.bool, - - /** Callback that is called when the text input is blurred */ - onBlur: PropTypes.func, - - /** Error text to display */ + /** Form Error description */ errorText: PropTypes.string, ...withLocalizePropTypes, }; const defaultProps = { - label: '', - value: undefined, + countryISO: '', + selectedCountryISO: undefined, errorText: '', - shouldSaveDraft: false, - inputID: undefined, - onBlur: () => {}, }; -const CountryPicker = forwardRef((props, ref) => { - const COUNTRIES = _.map(props.translate('allCountries'), (countryName, countryISO) => ({ - value: countryISO, - label: countryName, - })); +function BaseCountryPicker(props) { + const countryTitle = useRef({title: '', iso: ''}); + const countryISO = props.countryISO; + const selectedCountryISO = props.selectedCountryISO; + const onInputChange = props.onInputChange; + useEffect(() => { + if (!selectedCountryISO || selectedCountryISO === countryTitle.current.iso) { + return; + } + countryTitle.current = {title: PersonalDetails.getCountryNameBy(selectedCountryISO || countryISO), iso: selectedCountryISO || countryISO}; + + // Needed to call onInputChange, so Form can update the validation and values + onInputChange(countryTitle.current.iso); + }, [countryISO, selectedCountryISO, onInputChange]); + + const navigateToCountrySelector = useCallback(() => { + Navigation.navigate(ROUTES.getCountrySelectionRoute(selectedCountryISO || countryISO, Navigation.getActiveRoute())); + }, [countryISO, selectedCountryISO]); + const descStyle = countryTitle.current.title.length === 0 ? {fontSize: sizes.fontSizeNormal} : null; return ( - + + + + ); -}); +} + +BaseCountryPicker.propTypes = propTypes; +BaseCountryPicker.defaultProps = defaultProps; + +const CountryPicker = React.forwardRef((props, ref) => ( + +)); -CountryPicker.propTypes = propTypes; -CountryPicker.defaultProps = defaultProps; CountryPicker.displayName = 'CountryPicker'; export default withLocalize(CountryPicker); diff --git a/src/components/MenuItemWithTopDescription.js b/src/components/MenuItemWithTopDescription.js index 4bd426d80f0c..31997525d512 100644 --- a/src/components/MenuItemWithTopDescription.js +++ b/src/components/MenuItemWithTopDescription.js @@ -6,16 +6,15 @@ const propTypes = { ...menuItemPropTypes, }; -function MenuItemWithTopDescription(props) { - return ( - - ); -} +const MenuItemWithTopDescription = React.forwardRef((props, ref) => ( + +)); MenuItemWithTopDescription.propTypes = propTypes; MenuItemWithTopDescription.displayName = 'MenuItemWithTopDescription'; diff --git a/src/components/OptionsSelectorWithSearch.js b/src/components/OptionsSelectorWithSearch.js new file mode 100644 index 000000000000..313170f75733 --- /dev/null +++ b/src/components/OptionsSelectorWithSearch.js @@ -0,0 +1,105 @@ +import _ from 'underscore'; +import React, {useState} from 'react'; +import PropTypes from 'prop-types'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import ScreenWrapper from './ScreenWrapper'; +import HeaderWithBackButton from './HeaderWithBackButton'; +import * as Expensicons from './Icon/Expensicons'; +import themeColors from '../styles/themes/default'; +import OptionsSelector from './OptionsSelector'; +import styles from '../styles/styles'; + +const propTypes = { + /** Title of the page */ + title: PropTypes.string.isRequired, + + /** Function to call when the back button is pressed */ + onBackButtonPress: PropTypes.func.isRequired, + + /** Text to display in the search input label */ + textSearchLabel: PropTypes.string.isRequired, + + /** Placeholder text to display in the search input */ + placeholder: PropTypes.string.isRequired, + + /** Function to call when a row is selected */ + onSelectRow: PropTypes.func.isRequired, + + /** Initial option to display as selected */ + initialOption: PropTypes.string, + + data: PropTypes.arrayOf( + PropTypes.shape({ + /** Text to display for the option */ + text: PropTypes.string.isRequired, + + /** Value of the option */ + value: PropTypes.string.isRequired, + + /** Key to use for the option in the list */ + keyForList: PropTypes.string.isRequired, + }), + ).isRequired, + + ...withLocalizePropTypes, +}; + +const defaultProps = { + initialOption: '', +}; + +const greenCheckmark = {src: Expensicons.Checkmark, color: themeColors.success}; + +function filterOptions(searchValue, data) { + const trimmedSearchValue = searchValue.trim(); + if (trimmedSearchValue.length === 0) { + return []; + } + + return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase())); +} + +function OptionsSelectorWithSearch(props) { + const [searchValue, setSearchValue] = useState(''); + const translate = props.translate; + const initialOption = props.initialOption; + + const filteredData = filterOptions(searchValue, props.data); + const headerMessage = searchValue.trim() && !filteredData.length ? translate('common.noResultsFound') : ''; + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + <> + + + + )} + + ); +} + +OptionsSelectorWithSearch.propTypes = propTypes; +OptionsSelectorWithSearch.defaultProps = defaultProps; +OptionsSelectorWithSearch.displayName = 'OptionsSelectorWithSearch'; + +export {greenCheckmark}; + +export default withLocalize(OptionsSelectorWithSearch); diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js index 1d18652a625f..fd920e6719fa 100644 --- a/src/components/StatePicker.js +++ b/src/components/StatePicker.js @@ -1,67 +1,102 @@ -import _ from 'underscore'; -import React, {forwardRef} from 'react'; +import lodashGet from 'lodash/get'; +import React, {useState, useEffect, useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import {useRoute} from '@react-navigation/native'; import PropTypes from 'prop-types'; -import Picker from './Picker'; +import compose from '../libs/compose'; +import withNavigation from './withNavigation'; +import sizes from '../styles/variables'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import Navigation from '../libs/Navigation/Navigation'; +import ROUTES from '../ROUTES'; +import FormHelpMessage from './FormHelpMessage'; const propTypes = { - /** The label for the field */ - label: PropTypes.string, - - /** A callback method that is called when the value changes and it receives the selected value as an argument. */ - onInputChange: PropTypes.func.isRequired, - - /** The value that needs to be selected */ - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - shouldSaveDraft: PropTypes.bool, - - /** Callback that is called when the text input is blurred */ - onBlur: PropTypes.func, + /** Current State from user address */ + stateISO: PropTypes.string, /** Error text to display */ errorText: PropTypes.string, + /** Default value to display */ + defaultValue: PropTypes.string, + ...withLocalizePropTypes, }; const defaultProps = { - label: '', - value: undefined, + stateISO: '', errorText: '', - shouldSaveDraft: false, - inputID: undefined, - onBlur: () => {}, + defaultValue: '', }; -const StatePicker = forwardRef((props, ref) => { - const STATES = _.chain(props.translate('allStates')) - .sortBy((state) => state.stateName.toLowerCase()) - .map((state) => ({value: state.stateISO, label: state.stateName})) - .value(); +function BaseStatePicker(props) { + const route = useRoute(); + const stateISO = props.stateISO; + const [stateTitle, setStateTitle] = useState(stateISO); + const paramStateISO = lodashGet(route, 'params.stateISO'); + const navigation = props.navigation; + const onInputChange = props.onInputChange; + const defaultValue = props.defaultValue; + const translate = props.translate; + + useEffect(() => { + if (!paramStateISO || paramStateISO === stateTitle) { + return; + } + + setStateTitle(paramStateISO); + + // Needed to call onInputChange, so Form can update the validation and values + onInputChange(paramStateISO); + + navigation.setParams({stateISO: null}); + }, [paramStateISO, stateTitle, onInputChange, navigation]); + + const navigateToCountrySelector = useCallback(() => { + Navigation.navigate(ROUTES.getUsaStateSelectionRoute(stateTitle || stateISO, Navigation.getActiveRoute())); + }, [stateTitle, stateISO]); + + const title = useMemo(() => { + const allStates = translate('allStates'); + if (!stateTitle) { + return defaultValue ? allStates[defaultValue].stateName : ''; + } + if (allStates[stateTitle]) { + return allStates[stateTitle].stateName; + } + + return stateTitle; + }, [translate, stateTitle, defaultValue]); + const descStyle = title.length === 0 ? {fontSize: sizes.fontSizeNormal} : null; return ( - + + + + ); -}); +} + +BaseStatePicker.propTypes = propTypes; +BaseStatePicker.defaultProps = defaultProps; + +const StatePicker = React.forwardRef((props, ref) => ( + +)); -StatePicker.propTypes = propTypes; -StatePicker.defaultProps = defaultProps; StatePicker.displayName = 'StatePicker'; -export default withLocalize(StatePicker); +export default compose(withLocalize, withNavigation)(StatePicker); diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 91ab2162674f..fa5c02c81a68 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -366,6 +366,7 @@ class BaseTextInput extends Component { {!_.isEmpty(inputHelpText) && ( diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index 2e278bab5d69..a082afbea5c7 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import stylePropTypes from '../../styles/stylePropTypes'; const propTypes = { /** Input label */ @@ -58,6 +59,9 @@ const propTypes = { /** Hint text to display below the TextInput */ hint: PropTypes.string, + /** Style the Hint container */ + hintContainerStyle: stylePropTypes, + /** Prefix character */ prefixCharacter: PropTypes.string, @@ -114,6 +118,7 @@ const defaultProps = { shouldSaveDraft: false, maxLength: null, hint: '', + hintContainerStyle: [], prefixCharacter: '', onInputChange: () => {}, shouldDelayFocus: false, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index b983ffd14968..61ff2f16e84c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -729,6 +729,26 @@ const FlagCommentStackNavigator = createModalStackNavigator([ }, ]); +const CountrySelectorStackNavigator = createModalStackNavigator([ + { + getComponent: () => { + const CountrySelectorPage = require('../../../pages/CountrySelectorPage').default; + return CountrySelectorPage; + }, + name: 'CountrySelector_Root', + }, +]); + +const UsaStateSelectorStackNavigator = createModalStackNavigator([ + { + getComponent: () => { + const usaStateSelectorPage = require('../../../pages/StateSelectorPage').default; + return usaStateSelectorPage; + }, + name: 'CountrySelector_Root', + }, +]); + export { IOUBillStackNavigator, IOURequestModalStackNavigator, @@ -752,4 +772,6 @@ export { WalletStatementStackNavigator, YearPickerStackNavigator, FlagCommentStackNavigator, + CountrySelectorStackNavigator, + UsaStateSelectorStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js index cb82795936c2..4b13d9288e75 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js @@ -114,6 +114,16 @@ function RigthModalNavigator() { options={defaultModalScreenOptions} component={ModalStackNavigators.FlagCommentStackNavigator} /> + + ); } diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 37506c0460ce..542daa4367c4 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -342,6 +342,16 @@ export default { FlagComment_Root: ROUTES.FLAG_COMMENT, }, }, + Select_Country: { + screens: { + CountrySelector_Root: ROUTES.SETTINGS_SELECT_COUNTRY, + }, + }, + Select_USA_State: { + screens: { + CountrySelector_Root: ROUTES.SETTINGS_USA_STATES, + }, + }, }, }, }, diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 53680f65a1ec..70fa6173c633 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -95,6 +95,20 @@ function getCountryISO(countryName) { return _.findKey(CONST.ALL_COUNTRIES, (country) => country === countryName) || ''; } +/** + * Returns the name of the country associated with the provided ISO code. + * If the provided code is invalid, an empty string is returned. + * + * @param {string} countryISO The ISO code of the country to look up. + * @returns {string} The name of the country associated with the provided ISO code. + */ +function getCountryNameBy(countryISO) { + if (_.isEmpty(countryISO) || countryISO.length !== 2) { + return countryISO; + } + + return CONST.ALL_COUNTRIES[countryISO] || ''; +} /** * @param {String} pronouns */ @@ -481,4 +495,5 @@ export { updateAutomaticTimezone, updateSelectedTimezone, getCountryISO, + getCountryNameBy, }; diff --git a/src/pages/CountrySelectorPage.js b/src/pages/CountrySelectorPage.js new file mode 100644 index 000000000000..3776052976c3 --- /dev/null +++ b/src/pages/CountrySelectorPage.js @@ -0,0 +1,83 @@ +import _ from 'underscore'; +import lodashGet from 'lodash/get'; +import React, {useCallback, useMemo} from 'react'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import ROUTES from '../ROUTES'; +import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; +import Navigation from '../libs/Navigation/Navigation'; +import compose from '../libs/compose'; +import ONYXKEYS from '../ONYXKEYS'; +import OptionsSelectorWithSearch, {greenCheckmark} from '../components/OptionsSelectorWithSearch'; + +const propTypes = { + route: PropTypes.shape({ + params: PropTypes.shape({ + backTo: PropTypes.string, + }), + }).isRequired, + + /** User's private personal details */ + privatePersonalDetails: PropTypes.shape({ + /** User's home address */ + address: PropTypes.shape({ + country: PropTypes.string, + }), + }), + + ...withLocalizePropTypes, +}; + +const defaultProps = { + privatePersonalDetails: { + address: { + state: '', + }, + }, +}; + +function CountrySelectorPage(props) { + const translate = props.translate; + const route = props.route; + const currentCountry = route.params.countryISO || lodashGet(props.privatePersonalDetails, 'address.country'); + + const countries = useMemo( + () => + _.map(translate('allCountries'), (countryName, countryISO) => ({ + value: countryISO, + keyForList: countryISO, + text: countryName, + customIcon: currentCountry === countryISO ? greenCheckmark : undefined, + })), + [translate, currentCountry], + ); + + const updateCountry = useCallback((selectedCountry) => { + Navigation.goBack(`${ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS}?countryISO=${selectedCountry.value}`, true); + }, []); + + return ( + Navigation.goBack(`${route.params.backTo}`)} + textSearchLabel={translate('common.country')} + placeholder={translate('pronounsPage.placeholderText')} + onSelectRow={updateCountry} + initialOption={currentCountry} + /> + ); +} + +CountrySelectorPage.propTypes = propTypes; +CountrySelectorPage.defaultProps = defaultProps; +CountrySelectorPage.displayName = 'CountrySelectorPage'; + +export default compose( + withLocalize, + withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, + }), +)(CountrySelectorPage); diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 136befbe697c..4f700d7c23a1 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -117,9 +117,10 @@ function AddressForm(props) { defaultValue={props.defaultValues.city} onChangeText={(value) => props.onFieldChange({city: value})} errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} - containerStyles={[styles.mt4]} + containerStyles={styles.mt4} /> - + + {this.props.translate('companyStep.subtitle')} - + + _.map(translate('allStates'), (state) => ({ + value: state.stateISO, + keyForList: state.stateISO, + text: state.stateName, + customIcon: currentCountryState === state.stateISO ? greenCheckmark : undefined, + })), + [translate, currentCountryState], + ); + + const updateCountryState = useCallback((selectedState) => { + Navigation.goBack(`${ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS}?stateISO=${selectedState.value}`, true); + }, []); + + return ( + Navigation.goBack(`${route.params.backTo}`)} + textSearchLabel={translate('common.state')} + placeholder={translate('pronounsPage.placeholderText')} + onSelectRow={updateCountryState} + initialOption={currentCountryState} + data={countryStates} + /> + ); +} + +StateSelectorPage.propTypes = propTypes; +StateSelectorPage.defaultProps = defaultProps; +StateSelectorPage.displayName = 'StateSelectorPage'; + +export default compose( + withLocalize, + withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, + }), +)(StateSelectorPage); diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index d26960e406f5..9ac74209693d 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import _ from 'underscore'; -import React, {useState, useCallback} from 'react'; +import React, {useState, useCallback, useEffect} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; @@ -60,17 +60,29 @@ function updateAddress(values) { PersonalDetails.updateAddress(values.addressLine1.trim(), values.addressLine2.trim(), values.city.trim(), values.state.trim(), values.zipPostCode.trim().toUpperCase(), values.country); } -function AddressPage(props) { - const {translate} = props; - const [countryISO, setCountryISO] = useState(PersonalDetails.getCountryISO(lodashGet(props.privatePersonalDetails, 'address.country')) || CONST.COUNTRY.US); +function AddressPage({translate, route, navigation, privatePersonalDetails}) { + const [countryISO, setCountryISO] = useState(PersonalDetails.getCountryISO(lodashGet(privatePersonalDetails, 'address.country')) || CONST.COUNTRY.US); const isUSAForm = countryISO === CONST.COUNTRY.US; const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [countryISO, 'samples'], ''); const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); - const address = lodashGet(props.privatePersonalDetails, 'address') || {}; + const address = lodashGet(privatePersonalDetails, 'address') || {}; const [street1, street2] = (address.street || '').split('\n'); + const onCountryUpdate = (newCountry) => { + setCountryISO(newCountry); + }; + const onCountryStateUpdate = (selectedCountryState) => { + navigation.setParams({stateISO: selectedCountryState}); + }; + + useEffect(() => { + const currentCountryISO = lodashGet(route, 'params.countryISO'); + if (currentCountryISO) { + setCountryISO(currentCountryISO); + } + }, [route, navigation]); /** * @param {Function} translate - translate function * @param {Boolean} isUSAForm - selected country ISO code is US @@ -123,24 +135,26 @@ function AddressPage(props) { return ( Navigation.goBack(ROUTES.SETTINGS_PERSONAL_DETAILS)} />
- - - - - + + + + - - {isUSAForm ? ( + {isUSAForm ? ( + - ) : ( - - )} - - + + ) : ( - - - - + )} + + + +
); diff --git a/src/styles/styles.js b/src/styles/styles.js index eb54f1dc5ca0..4b2815639470 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3385,6 +3385,11 @@ const styles = { maxWidth: 375, }, + formSpaceVertical: { + height: 20, + width: 1, + }, + taskCheckbox: { height: 16, width: 16, From c9c320a43a2065397dde9f15013aabae9346f2df Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 19 Jun 2023 12:00:51 +0200 Subject: [PATCH 003/325] Fixed comments --- src/components/AddressSearch/index.js | 2 +- src/components/CountryPicker.js | 5 ++++- src/components/OptionsSelectorWithSearch.js | 6 +++++- src/components/StatePicker.js | 7 ++++--- src/pages/CountrySelectorPage.js | 3 +++ src/pages/StateSelectorPage.js | 3 +++ src/pages/settings/Profile/PersonalDetails/AddressPage.js | 2 +- 7 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index cc1115c964b3..89b05260341a 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -179,7 +179,7 @@ function AddressSearch(props) { // If the address is not in the US, use the full length state name since we're displaying the address's // state / province in a TextInput instead of in a picker. const isUS = country === CONST.COUNTRY.US; - if (isUS) { + if (!isUS) { values.state = longStateName; } diff --git a/src/components/CountryPicker.js b/src/components/CountryPicker.js index 8c13ae99d39b..41e7e2565379 100644 --- a/src/components/CountryPicker.js +++ b/src/components/CountryPicker.js @@ -2,6 +2,7 @@ import React, {useCallback, useRef, useEffect} from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; import sizes from '../styles/variables'; +import styles from '../styles/styles'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import * as PersonalDetails from '../libs/actions/PersonalDetails'; @@ -58,7 +59,9 @@ function BaseCountryPicker(props) { description={props.translate('common.country')} onPress={navigateToCountrySelector} /> - + + +
); } diff --git a/src/components/OptionsSelectorWithSearch.js b/src/components/OptionsSelectorWithSearch.js index 313170f75733..945037cd140e 100644 --- a/src/components/OptionsSelectorWithSearch.js +++ b/src/components/OptionsSelectorWithSearch.js @@ -25,6 +25,9 @@ const propTypes = { /** Function to call when a row is selected */ onSelectRow: PropTypes.func.isRequired, + /** Initial value to display in the search input */ + initialSearchValue: PropTypes.string, + /** Initial option to display as selected */ initialOption: PropTypes.string, @@ -45,6 +48,7 @@ const propTypes = { }; const defaultProps = { + initialSearchValue: '', initialOption: '', }; @@ -60,7 +64,7 @@ function filterOptions(searchValue, data) { } function OptionsSelectorWithSearch(props) { - const [searchValue, setSearchValue] = useState(''); + const [searchValue, setSearchValue] = useState(props.initialSearchValue); const translate = props.translate; const initialOption = props.initialOption; diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js index fd920e6719fa..6e1d81e0afb7 100644 --- a/src/components/StatePicker.js +++ b/src/components/StatePicker.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import compose from '../libs/compose'; import withNavigation from './withNavigation'; import sizes from '../styles/variables'; +import styles from '../styles/styles'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import Navigation from '../libs/Navigation/Navigation'; @@ -50,8 +51,6 @@ function BaseStatePicker(props) { // Needed to call onInputChange, so Form can update the validation and values onInputChange(paramStateISO); - - navigation.setParams({stateISO: null}); }, [paramStateISO, stateTitle, onInputChange, navigation]); const navigateToCountrySelector = useCallback(() => { @@ -81,7 +80,9 @@ function BaseStatePicker(props) { descriptionTextStyle={descStyle} onPress={navigateToCountrySelector} /> - + + +
); } diff --git a/src/pages/CountrySelectorPage.js b/src/pages/CountrySelectorPage.js index 3776052976c3..fc493d0dce63 100644 --- a/src/pages/CountrySelectorPage.js +++ b/src/pages/CountrySelectorPage.js @@ -3,6 +3,7 @@ import lodashGet from 'lodash/get'; import React, {useCallback, useMemo} from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; +import * as PersonalDetails from '../libs/actions/PersonalDetails'; import ROUTES from '../ROUTES'; import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; import Navigation from '../libs/Navigation/Navigation'; @@ -40,6 +41,7 @@ function CountrySelectorPage(props) { const translate = props.translate; const route = props.route; const currentCountry = route.params.countryISO || lodashGet(props.privatePersonalDetails, 'address.country'); + const selectedSearchCountry = PersonalDetails.getCountryNameBy(currentCountry); const countries = useMemo( () => @@ -64,6 +66,7 @@ function CountrySelectorPage(props) { textSearchLabel={translate('common.country')} placeholder={translate('pronounsPage.placeholderText')} onSelectRow={updateCountry} + initialSearchValue={selectedSearchCountry} initialOption={currentCountry} /> ); diff --git a/src/pages/StateSelectorPage.js b/src/pages/StateSelectorPage.js index 4a70d0775b5d..00dcb445899c 100644 --- a/src/pages/StateSelectorPage.js +++ b/src/pages/StateSelectorPage.js @@ -40,6 +40,8 @@ function StateSelectorPage(props) { const translate = props.translate; const route = props.route; const currentCountryState = !_.isEmpty(route.params) ? route.params.stateISO : lodashGet(props.privatePersonalDetails, 'address.state'); + const allStates = translate('allStates'); + const selectedSearchState = !_.isEmpty(currentCountryState) ? allStates[currentCountryState].stateName : ''; const countryStates = useMemo( () => @@ -63,6 +65,7 @@ function StateSelectorPage(props) { textSearchLabel={translate('common.state')} placeholder={translate('pronounsPage.placeholderText')} onSelectRow={updateCountryState} + initialSearchValue={selectedSearchState} initialOption={currentCountryState} data={countryStates} /> diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index 9ac74209693d..29fe43ac46cf 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -147,7 +147,7 @@ function AddressPage({translate, route, navigation, privatePersonalDetails}) { submitButtonText={translate('common.save')} enabledWhenOffline > - + Date: Fri, 9 Jun 2023 16:42:12 +0200 Subject: [PATCH 004/325] push to page menu item --- src/components/OptionsSelectorWithSearch.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/OptionsSelectorWithSearch.js b/src/components/OptionsSelectorWithSearch.js index 945037cd140e..e956dc048705 100644 --- a/src/components/OptionsSelectorWithSearch.js +++ b/src/components/OptionsSelectorWithSearch.js @@ -49,6 +49,7 @@ const propTypes = { const defaultProps = { initialSearchValue: '', + initialOption: '', }; From 062bc8a3987605a271013593a295b52de525d175 Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 19 Jun 2023 12:00:51 +0200 Subject: [PATCH 005/325] Fixed comments --- src/components/OptionsSelectorWithSearch.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/OptionsSelectorWithSearch.js b/src/components/OptionsSelectorWithSearch.js index e956dc048705..945037cd140e 100644 --- a/src/components/OptionsSelectorWithSearch.js +++ b/src/components/OptionsSelectorWithSearch.js @@ -49,7 +49,6 @@ const propTypes = { const defaultProps = { initialSearchValue: '', - initialOption: '', }; From cd6dba82b0c0a2b3d8edcc7692bee3304821de70 Mon Sep 17 00:00:00 2001 From: Nathalie Kuoch Date: Wed, 21 Jun 2023 15:35:49 +0200 Subject: [PATCH 006/325] Handle isWaitingForBankAccount when paying request --- src/CONST.js | 3 +++ src/components/AddPlaidBankAccount.js | 2 +- src/components/LHNOptionsList/OptionRowLHN.js | 3 ++- src/components/MoneyRequestHeader.js | 11 ++++++-- src/components/ReportActionItem/IOUPreview.js | 10 +++++-- .../ReportActionItem/MoneyRequestAction.js | 2 +- .../ReportActionItem/ReportPreview.js | 7 +++-- src/languages/en.js | 2 ++ src/languages/es.js | 1 + src/libs/OptionsListUtils.js | 1 + src/libs/ReportActionsUtils.js | 1 + src/libs/ReportUtils.js | 16 ++++++++--- src/libs/SidebarUtils.js | 5 +++- src/libs/actions/Plaid.js | 4 ++- src/pages/ReimbursementAccount/CompanyStep.js | 4 --- src/pages/home/report/ReportActionItem.js | 10 +++++++ .../ReportActionItemReimbursementQueued.js | 27 +++++++++++++++++++ .../home/report/ReportActionItemSingle.js | 1 + src/pages/home/report/ReportActionsList.js | 1 + 19 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 src/pages/home/report/ReportActionItemReimbursementQueued.js diff --git a/src/CONST.js b/src/CONST.js index 50bfb5675bd8..a9a2b7acbd84 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -490,6 +490,8 @@ const CONST = { TASKEDITED: 'TASKEDITED', TASKCANCELED: 'TASKCANCELED', IOU: 'IOU', + REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED', + REIMBURSED: 'REIMBURSED', RENAMED: 'RENAMED', CHRONOSOOOLIST: 'CHRONOSOOOLIST', TASKCOMPLETED: 'TASKCOMPLETED', @@ -1043,6 +1045,7 @@ const CONST = { DECLINE: 'decline', CANCEL: 'cancel', DELETE: 'delete', + REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED', }, AMOUNT_MAX_LENGTH: 10, }, diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index 4739e9ed1f12..ff97c9be24a6 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -180,7 +180,7 @@ class AddPlaidBankAccount extends React.Component { token={token} onSuccess={({publicToken, metadata}) => { Log.info('[PlaidLink] Success!'); - BankAccounts.openPlaidBankAccountSelector(publicToken, metadata.institution.name, this.props.allowDebit); + BankAccounts.openPlaidBankAccountSelector(publicToken, metadata.institution.name, this.props.allowDebit, this.props.bankAccountID); }} onError={(error) => { Log.hmmm('[PlaidLink] Error: ', error.message); diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 20dfc639fcba..a0fe0dbf9903 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -87,7 +87,8 @@ function OptionRowLHN(props) { !hasBrickError && (optionItem.isUnreadWithMention || (optionItem.hasOutstandingIOU && !optionItem.isIOUReportOwner) || - (optionItem.isTaskReport && optionItem.isTaskAssignee && !optionItem.isTaskCompleted)); + (optionItem.isTaskReport && optionItem.isTaskAssignee && !optionItem.isTaskCompleted) || + (optionItem.isWaitingOnBankAccount && optionItem.isIOUReportOwner)); /** * Show the ReportActionContextMenu modal popover. diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 0976818f9545..43bfebfc70c5 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -87,9 +87,16 @@ function MoneyRequestHeader(props) { const policy = props.policies[`${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}`]; const isPayer = Policy.isAdminOfFreePolicy([policy]) || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(props.session, 'accountID', null) === moneyRequestReport.managerID); - const shouldShowSettlementButton = !isSettled && !props.isSingleTransactionView && isPayer; + const shouldShowSettlementButton = !isSettled && !props.isSingleTransactionView && isPayer && !props.report.isWaitingOnBankAccount; const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport); const shouldShowPaypal = Boolean(lodashGet(props.personalDetails, [moneyRequestReport.managerID, 'payPalMeAddress'])); + let description = `${props.translate('iou.amount')} • ${props.translate('iou.cash')}`; + if (isSettled) { + description += ` • ${props.translate('iou.settledExpensify')}`; + } else if (props.report.isWaitingOnBankAccount) { + description += ` • Waiting for credit account`; + } + return ( {CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency)} - {!props.iouReport.hasOutstandingIOU && !props.isBillSplit && ( + {!props.iouReport.hasOutstandingIOU && !props.isBillSplit && !props.iouReport.isWaitingOnBankAccount && ( {_.map(props.action.message, (message, index) => ( @@ -123,7 +126,7 @@ function ReportPreview(props) { ) : ( - {lodashGet(message, 'html', props.translate('iou.payerSettled', {amount: reportAmount}))} + {props.iouReport.isWaitingOnBankAccount ? props.translate('iou.waitingOnBankAccount', {submitterDisplayName}) : lodashGet(message, 'html', props.translate('iou.payerSettled', {amount: reportAmount}))} )} @@ -134,7 +137,7 @@ function ReportPreview(props) { /> ))} - {isCurrentUserManager && !ReportUtils.isSettled(props.iouReport.reportID) && ( + {isCurrentUserManager && !ReportUtils.isSettled(props.iouReport.reportID) && !props.chatReport.isWaitingOnBankAccount && ( `${payer} owes ${amount}`, payerPaidAmount: ({payer, amount}) => `${payer} paid ${amount}`, payerSettled: ({amount}) => `paid ${amount}`, + waitingOnBankAccount: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a `, noReimbursableExpenses: 'This report has an invalid amount', pendingConversionMessage: "Total will update when you're back online", threadRequestReportName: ({formattedAmount, comment}) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, diff --git a/src/languages/es.js b/src/languages/es.js index d54fa5110117..af01311ccb58 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -359,6 +359,7 @@ export default { payerOwesAmount: ({payer, amount}) => `${payer} debe ${amount}`, payerPaidAmount: ({payer, amount}) => `${payer} pagó ${amount}`, payerSettled: ({amount}) => `pagó ${amount}`, + waitingOnBankAccount: ({submitterDisplayName}) => `comenzó a establecerse, el pago se retiene hasta que ${submitterDisplayName} agrega una `, noReimbursableExpenses: 'El monto de este informe es inválido', pendingConversionMessage: 'El total se actualizará cuando estés online', threadRequestReportName: ({formattedAmount, comment}) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 611f44f751db..5a8b9270b19d 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -471,6 +471,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.keyForList = String(report.reportID); result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs || []); result.hasOutstandingIOU = report.hasOutstandingIOU; + result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; subtitle = ReportUtils.getChatRoomSubtitle(report); diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index f57084c9f0e0..6d244a3a5db6 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -88,6 +88,7 @@ function isSentMoneyReportAction(reportAction) { reportAction && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && lodashGet(reportAction, 'originalMessage.type') === CONST.IOU.REPORT_ACTION_TYPE.PAY && + lodashGet(reportAction, 'originalMessage.type') === CONST.IOU.REPORT_ACTION_TYPE.REIMBURSEMENTQUEUED && _.has(reportAction.originalMessage, 'IOUDetails') ); } diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 6f317cc08d29..843f22be6774 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -233,7 +233,13 @@ function canFlagReportAction(reportAction) { * @returns {Boolean} */ function isSettled(reportID) { - return !lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, 'hasOutstandingIOU']); + const report = lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}); + return !report.hasOutstandingIOU && !report.isWaitingOnBankAccount; +} + +function isCurrentUserSubmitter(reportID) { + const report = lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}); + return report.owner === sessionEmail; } /** @@ -916,7 +922,7 @@ function getMoneyRequestAction(reportAction = {}) { * @returns {Number} */ function getMoneyRequestTotal(report, moneyRequestReports = {}) { - if (report.hasOutstandingIOU || isMoneyRequestReport(report)) { + if (report.hasOutstandingIOU || report.isWaitingOnBankAccount || isMoneyRequestReport(report)) { const moneyRequestReport = moneyRequestReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`] || report; const total = lodashGet(moneyRequestReport, 'total', 0); @@ -1821,7 +1827,7 @@ function hasOutstandingIOU(report, iouReports) { } if (iouReport.ownerAccountID === currentUserAccountID) { - return false; + return report.isWaitingOnBankAccount; } return report.hasOutstandingIOU; @@ -1840,6 +1846,9 @@ function isIOUOwnedByCurrentUser(report, iouReports = {}) { return iouReport.ownerAccountID === currentUserAccountID; } } + if (report.isWaitingOnBankAccount) { + return report.ownerAccountID === currentUserAccountID; + } return false; } @@ -2300,6 +2309,7 @@ export { getIOUReportActionMessage, getDisplayNameForParticipant, isChatReport, + isCurrentUserSubmitter, isExpenseReport, isIOUReport, isTaskReport, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index a41249ebfa3d..b311f831cb1d 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -168,7 +168,7 @@ function getOrderedReportIDs(reportIDFromRoute) { return; } - if (report.hasOutstandingIOU && !ReportUtils.isIOUOwnedByCurrentUser(report, allReports)) { + if ((report.hasOutstandingIOU || report.isWaitingOnBankAccount) && !ReportUtils.isIOUOwnedByCurrentUser(report, allReports)) { outstandingIOUReports.push(report); return; } @@ -258,6 +258,7 @@ function getOptionData(reportID) { shouldShowSubscript: false, isPolicyExpenseChat: false, isMoneyRequestReport: false, + isWaitingOnBankAccount: false, }; const participantPersonalDetailList = _.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs, personalDetails)); @@ -289,6 +290,8 @@ function getOptionData(reportID) { result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs || []); result.hasOutstandingIOU = report.hasOutstandingIOU; result.parentReportID = report.parentReportID || null; + result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; + const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; const subtitle = ReportUtils.getChatRoomSubtitle(report); diff --git a/src/libs/actions/Plaid.js b/src/libs/actions/Plaid.js index 3155058624af..53763980d285 100644 --- a/src/libs/actions/Plaid.js +++ b/src/libs/actions/Plaid.js @@ -40,14 +40,16 @@ function openPlaidBankLogin(allowDebit, bankAccountID) { * @param {String} publicToken * @param {String} bankName * @param {Boolean} allowDebit + * @param {Number} bankAccountID */ -function openPlaidBankAccountSelector(publicToken, bankName, allowDebit) { +function openPlaidBankAccountSelector(publicToken, bankName, allowDebit, bankAccountID) { API.read( 'OpenPlaidBankAccountSelector', { publicToken, allowDebit, bank: bankName, + bankAccountID, }, { optimisticData: [ diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index e44d0562b58e..756aca8e244c 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -59,10 +59,6 @@ class CompanyStep extends React.Component { this.defaultWebsite = lodashGet(props, 'user.isFromPublicDomain', false) ? 'https://' : `https://www.${Str.extractEmailDomain(props.session.email, '')}`; } - componentWillUnmount() { - BankAccounts.resetReimbursementAccount(); - } - /** * @param {Array} fieldNames * diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 2303ee6c556f..eb3c35cff703 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -30,6 +30,7 @@ import * as ContextMenuActions from './ContextMenu/ContextMenuActions'; import * as EmojiPickerAction from '../../../libs/actions/EmojiPickerAction'; import {withBlockedFromConcierge, withNetwork, withPersonalDetails, withReportActionsDrafts} from '../../../components/OnyxProvider'; import RenameAction from '../../../components/ReportActionItem/RenameAction'; +import ReportActionItemReimbursementQueued from './ReportActionItemReimbursementQueued'; import InlineSystemMessage from '../../../components/InlineSystemMessage'; import styles from '../../../styles/styles'; import SelectionScraper from '../../../libs/SelectionScraper'; @@ -56,6 +57,7 @@ import TaskPreview from '../../../components/ReportActionItem/TaskPreview'; import TaskAction from '../../../components/ReportActionItem/TaskAction'; import * as Session from '../../../libs/actions/Session'; import {hideContextMenu} from './ContextMenu/ReportActionContextMenu'; +import * as PersonalDetailsUtils from "../../../libs/PersonalDetailsUtils"; const propTypes = { ...windowDimensionsPropTypes, @@ -275,6 +277,14 @@ function ReportActionItem(props) { isHovered={hovered} /> ); + } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetailsList, [props.report.ownerAccountID, 'displayName'], props.report.ownerEmail); + children = ( + + ); } else { const message = _.last(lodashGet(props.action, 'message', [{}])); const hasBeenFlagged = !_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], moderationDecision); diff --git a/src/pages/home/report/ReportActionItemReimbursementQueued.js b/src/pages/home/report/ReportActionItemReimbursementQueued.js new file mode 100644 index 000000000000..0336cadc460d --- /dev/null +++ b/src/pages/home/report/ReportActionItemReimbursementQueued.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Text from '../../../components/Text'; +import styles from '../../../styles/styles'; +import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; + +const propTypes = { + submitterDisplayName: PropTypes.string.isRequired, + isCurrentUserSubmitter: PropTypes.bool.isRequired, + + ...withLocalizePropTypes, +}; + +function ReportActionItemReimbursementQueued(props) { + // TODO: add click on Bank Account to open Plaid + return ( + + {props.translate('iou.waitingOnBankAccount', {submitterDisplayName: props.submitterDisplayName})} + {props.isCurrentUserSubmitter ? props.translate('common.bankAccount') : props.translate('common.bankAccount')}. + + ); +} + +ReportActionItemReimbursementQueued.propTypes = propTypes; +ReportActionItemReimbursementQueued.displayName = 'ReportActionItemReimbursementQueued'; + +export default withLocalize(ReportActionItemReimbursementQueued); diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index 3e49dfe621c4..667a154189eb 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -84,6 +84,7 @@ function ReportActionItemSingle(props) { ] : props.action.person; + return ( Date: Thu, 22 Jun 2023 09:54:54 +0200 Subject: [PATCH 007/325] Handle reimbursed action --- .../ReportActionItem/ReportPreview.js | 2 +- src/languages/en.js | 2 ++ src/languages/es.js | 3 ++ src/libs/ReportActionsUtils.js | 1 - src/libs/ReportUtils.js | 28 ++++++++----------- src/libs/SidebarUtils.js | 22 ++++++++++----- .../actions/ReimbursementAccount/store.js | 10 ++++++- src/pages/home/report/ReportActionItem.js | 13 ++++++++- .../home/report/ReportActionItemReimbursed.js | 27 ++++++++++++++++++ .../ReportActionItemReimbursementQueued.js | 13 +++++++-- .../home/report/ReportActionItemSingle.js | 1 - 11 files changed, 90 insertions(+), 32 deletions(-) create mode 100644 src/pages/home/report/ReportActionItemReimbursed.js diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 500d9b3eadb3..cdb050477ce3 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -126,7 +126,7 @@ function ReportPreview(props) { ) : ( - {props.iouReport.isWaitingOnBankAccount ? props.translate('iou.waitingOnBankAccount', {submitterDisplayName}) : lodashGet(message, 'html', props.translate('iou.payerSettled', {amount: reportAmount}))} + {props.iouReport.isWaitingOnBankAccount ? `${props.translate('iou.waitingOnBankAccount', {submitterDisplayName})} ${props.translate('common.bankAccount')}` : lodashGet(message, 'html', props.translate('iou.payerSettled', {amount: reportAmount}))} )} diff --git a/src/languages/en.js b/src/languages/en.js index c78f973232a4..eecf4f86e37e 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -361,7 +361,9 @@ export default { payerOwesAmount: ({payer, amount}) => `${payer} owes ${amount}`, payerPaidAmount: ({payer, amount}) => `${payer} paid ${amount}`, payerSettled: ({amount}) => `paid ${amount}`, + payerSettledUp: ({amount}) => `settled up ${amount}`, waitingOnBankAccount: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a `, + afterAddedBankAccount: ({submitterDisplayName}) => `after ${submitterDisplayName} added a bank account`, noReimbursableExpenses: 'This report has an invalid amount', pendingConversionMessage: "Total will update when you're back online", threadRequestReportName: ({formattedAmount, comment}) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, diff --git a/src/languages/es.js b/src/languages/es.js index af01311ccb58..1da1cdf74965 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -346,6 +346,7 @@ export default { sendMoney: 'Enviar dinero', pay: 'Pagar', viewDetails: 'Ver detalles', + pending: 'Pendiente', settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', settledPaypalMe: 'Pagado con PayPal.me', @@ -359,7 +360,9 @@ export default { payerOwesAmount: ({payer, amount}) => `${payer} debe ${amount}`, payerPaidAmount: ({payer, amount}) => `${payer} pagó ${amount}`, payerSettled: ({amount}) => `pagó ${amount}`, + payerSettledUp: ({amount}) => `pagó ${amount}`, waitingOnBankAccount: ({submitterDisplayName}) => `comenzó a establecerse, el pago se retiene hasta que ${submitterDisplayName} agrega una `, + afterAddedBankAccount: ({submitterDisplayName}) => `después ${submitterDisplayName} agregó una cuenta bancaria`, noReimbursableExpenses: 'El monto de este informe es inválido', pendingConversionMessage: 'El total se actualizará cuando estés online', threadRequestReportName: ({formattedAmount, comment}) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 6d244a3a5db6..f57084c9f0e0 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -88,7 +88,6 @@ function isSentMoneyReportAction(reportAction) { reportAction && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && lodashGet(reportAction, 'originalMessage.type') === CONST.IOU.REPORT_ACTION_TYPE.PAY && - lodashGet(reportAction, 'originalMessage.type') === CONST.IOU.REPORT_ACTION_TYPE.REIMBURSEMENTQUEUED && _.has(reportAction.originalMessage, 'IOUDetails') ); } diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 843f22be6774..8b7b2436fc1b 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -239,7 +239,7 @@ function isSettled(reportID) { function isCurrentUserSubmitter(reportID) { const report = lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}); - return report.owner === sessionEmail; + return report.ownerEmail === sessionEmail; } /** @@ -1817,20 +1817,16 @@ function isUnreadWithMention(report) { * @returns {boolean} */ function hasOutstandingIOU(report, iouReports) { - if (!report || !report.iouReportID || _.isUndefined(report.hasOutstandingIOU)) { - return false; - } - - const iouReport = iouReports && iouReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; - if (!iouReport || !iouReport.ownerAccountID) { - return false; - } + if (report.iouReportID) { + const iouReport = iouReports && iouReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; + if (!iouReport || !iouReport.ownerAccountID) { + return false; + } - if (iouReport.ownerAccountID === currentUserAccountID) { - return report.isWaitingOnBankAccount; + return iouReport.ownerAccountID === currentUserAccountID ? iouReport.isWaitingOnBankAccount : iouReport.hasOutstandingIOU; } - return report.hasOutstandingIOU; + return report.ownerAccountID === currentUserAccountID ? report.isWaitingOnBankAccount : report.hasOutstandingIOU; } /** @@ -1840,16 +1836,14 @@ function hasOutstandingIOU(report, iouReports) { * @returns {Boolean} */ function isIOUOwnedByCurrentUser(report, iouReports = {}) { - if (report.hasOutstandingIOU) { + if (report.iouReportID) { const iouReport = iouReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; if (iouReport) { return iouReport.ownerAccountID === currentUserAccountID; } } - if (report.isWaitingOnBankAccount) { - return report.ownerAccountID === currentUserAccountID; - } - return false; + + return report.ownerAccountID === currentUserAccountID; } /** diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index b311f831cb1d..fc0f6d114ea2 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -148,31 +148,38 @@ function getOrderedReportIDs(reportIDFromRoute) { // The LHN is split into five distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order: // 1. Pinned - Always sorted by reportDisplayName - // 2. Outstanding IOUs - Always sorted by iouReportAmount with the largest amounts at the top of the group - // 3. Drafts - Always sorted by reportDisplayName - // 4. Non-archived reports + // 2. Waiting on bank account - Always sorted by reportDisplayName + // 3. Outstanding IOUs - Always sorted by iouReportAmount with the largest amounts at the top of the group + // 4. Drafts - Always sorted by reportDisplayName + // 5. Non-archived reports // - Sorted by lastVisibleActionCreated in default (most recent) view mode // - Sorted by reportDisplayName in GSD (focus) view mode - // 5. Archived reports + // 6. Archived reports // - Sorted by lastVisibleActionCreated in default (most recent) view mode // - Sorted by reportDisplayName in GSD (focus) view mode let pinnedReports = []; let outstandingIOUReports = []; + let waitingOnBankAccountReports = []; let draftReports = []; let nonArchivedReports = []; let archivedReports = []; - _.each(reportsToDisplay, (report) => { if (report.isPinned) { pinnedReports.push(report); return; } - if ((report.hasOutstandingIOU || report.isWaitingOnBankAccount) && !ReportUtils.isIOUOwnedByCurrentUser(report, allReports)) { + const isIOUOwnedByCurrentUser = ReportUtils.isIOUOwnedByCurrentUser(report, allReports); + if (report.hasOutstandingIOU && !isIOUOwnedByCurrentUser) { outstandingIOUReports.push(report); return; } + if (report.isWaitingOnBankAccount && isIOUOwnedByCurrentUser) { + waitingOnBankAccountReports.push(report); + return; + } + if (report.hasDraft) { draftReports.push(report); return; @@ -193,6 +200,7 @@ function getOrderedReportIDs(reportIDFromRoute) { // Sort each group of reports accordingly pinnedReports = _.sortBy(pinnedReports, (report) => report.displayName.toLowerCase()); + waitingOnBankAccountReports = _.sortBy(waitingOnBankAccountReports, (report) => report.displayName.toLowerCase()); outstandingIOUReports = lodashOrderBy(outstandingIOUReports, ['iouReportAmount', (report) => report.displayName.toLowerCase()], ['desc', 'asc']); draftReports = _.sortBy(draftReports, (report) => report.displayName.toLowerCase()); nonArchivedReports = isInDefaultMode @@ -207,7 +215,7 @@ function getOrderedReportIDs(reportIDFromRoute) { // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - return _.pluck([].concat(pinnedReports).concat(outstandingIOUReports).concat(draftReports).concat(nonArchivedReports).concat(archivedReports), 'reportID'); + return _.pluck([].concat(pinnedReports).concat(waitingOnBankAccountReports).concat(outstandingIOUReports).concat(draftReports).concat(nonArchivedReports).concat(archivedReports), 'reportID'); } /** diff --git a/src/libs/actions/ReimbursementAccount/store.js b/src/libs/actions/ReimbursementAccount/store.js index d1e41ba7f8ec..5e69467932b9 100644 --- a/src/libs/actions/ReimbursementAccount/store.js +++ b/src/libs/actions/ReimbursementAccount/store.js @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; import ONYXKEYS from '../../../ONYXKEYS'; +import BankAccount from "../../models/BankAccount"; /** Reimbursement account actively being set up */ let reimbursementAccountInSetup = {}; @@ -43,6 +44,13 @@ function getBankAccountList() { return bankAccountList; } +function hasCreditBankAccount() { + return _.some(bankAccountList, (bankAccountJSON) => { + const bankAccount = new BankAccount(bankAccountJSON); + return bankAccount.isDefaultCredit(); + }); +} + function getCredentials() { return credentials; } @@ -51,4 +59,4 @@ function getReimbursementAccountWorkspaceID() { return reimbursementAccountWorkspaceID; } -export {getReimbursementAccountInSetup, getBankAccountList, getCredentials, getReimbursementAccountWorkspaceID}; +export {getReimbursementAccountInSetup, getBankAccountList, getCredentials, getReimbursementAccountWorkspaceID, hasCreditBankAccount}; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index eb3c35cff703..4bb5009dc7de 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -57,7 +57,9 @@ import TaskPreview from '../../../components/ReportActionItem/TaskPreview'; import TaskAction from '../../../components/ReportActionItem/TaskAction'; import * as Session from '../../../libs/actions/Session'; import {hideContextMenu} from './ContextMenu/ReportActionContextMenu'; -import * as PersonalDetailsUtils from "../../../libs/PersonalDetailsUtils"; +import * as PersonalDetailsUtils from '../../../libs/PersonalDetailsUtils'; +import ReportActionItemReimbursed from './ReportActionItemReimbursed'; +import * as CurrencyUtils from '../../../libs/CurrencyUtils'; const propTypes = { ...windowDimensionsPropTypes, @@ -285,6 +287,15 @@ function ReportActionItem(props) { isCurrentUserSubmitter={ReportUtils.isCurrentUserSubmitter(props.report.reportID)} /> ); + } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSED) { + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetailsList, [props.report.ownerAccountID, 'displayName'], props.report.ownerEmail); + children = ( + + ); } else { const message = _.last(lodashGet(props.action, 'message', [{}])); const hasBeenFlagged = !_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], moderationDecision); diff --git a/src/pages/home/report/ReportActionItemReimbursed.js b/src/pages/home/report/ReportActionItemReimbursed.js new file mode 100644 index 000000000000..b2b3c3947dec --- /dev/null +++ b/src/pages/home/report/ReportActionItemReimbursed.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Text from '../../../components/Text'; +import styles from '../../../styles/styles'; +import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; + +const propTypes = { + amount: PropTypes.string.isRequired, + submitterDisplayName: PropTypes.string.isRequired, + isFromSubmitterAddingBankAccount: PropTypes.bool.isRequired, + + ...withLocalizePropTypes, +}; + +function ReportActionItemReimbursed(props) { + return ( + + {props.translate('iou.payerSettledUp', {amount: props.amount})}{' '} + {props.isFromSubmitterAddingBankAccount ? props.translate('iou.afterAddedBankAccount', {submitterDisplayName: props.submitterDisplayName}) : ''} + + ); +} + +ReportActionItemReimbursed.propTypes = propTypes; +ReportActionItemReimbursed.displayName = 'ReportActionItemAddedBankAccount'; + +export default withLocalize(ReportActionItemReimbursed); diff --git a/src/pages/home/report/ReportActionItemReimbursementQueued.js b/src/pages/home/report/ReportActionItemReimbursementQueued.js index 0336cadc460d..4b11afe3be11 100644 --- a/src/pages/home/report/ReportActionItemReimbursementQueued.js +++ b/src/pages/home/report/ReportActionItemReimbursementQueued.js @@ -3,20 +3,27 @@ import PropTypes from 'prop-types'; import Text from '../../../components/Text'; import styles from '../../../styles/styles'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; +import TextLink from "../../../components/TextLink"; +import * as BankAccounts from '../../../libs/actions/BankAccounts'; +import * as store from "../../../libs/actions/ReimbursementAccount/store"; const propTypes = { submitterDisplayName: PropTypes.string.isRequired, isCurrentUserSubmitter: PropTypes.bool.isRequired, - ...withLocalizePropTypes, }; function ReportActionItemReimbursementQueued(props) { - // TODO: add click on Bank Account to open Plaid + const shouldSubmitterAddBankAccount = props.isCurrentUserSubmitter && !store.hasCreditBankAccount(); + return ( {props.translate('iou.waitingOnBankAccount', {submitterDisplayName: props.submitterDisplayName})} - {props.isCurrentUserSubmitter ? props.translate('common.bankAccount') : props.translate('common.bankAccount')}. + {shouldSubmitterAddBankAccount ? ( + + {props.translate('common.bankAccount')} + + ) : props.translate('common.bankAccount')}. ); } diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index 667a154189eb..3e49dfe621c4 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -84,7 +84,6 @@ function ReportActionItemSingle(props) { ] : props.action.person; - return ( Date: Thu, 22 Jun 2023 15:51:58 +0200 Subject: [PATCH 008/325] Some cleaning --- src/CONST.js | 1 - src/components/ReportActionItem/ReportPreview.js | 4 +++- src/libs/SidebarUtils.js | 5 ++++- src/libs/actions/ReimbursementAccount/store.js | 3 ++- src/pages/home/report/ReportActionItemReimbursed.js | 2 +- .../report/ReportActionItemReimbursementQueued.js | 13 ++++++++----- src/pages/home/report/ReportActionsList.js | 1 - 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index a9a2b7acbd84..6a4b11c49f04 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -1045,7 +1045,6 @@ const CONST = { DECLINE: 'decline', CANCEL: 'cancel', DELETE: 'delete', - REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED', }, AMOUNT_MAX_LENGTH: 10, }, diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index cdb050477ce3..97c0696f1021 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -126,7 +126,9 @@ function ReportPreview(props) { ) : ( - {props.iouReport.isWaitingOnBankAccount ? `${props.translate('iou.waitingOnBankAccount', {submitterDisplayName})} ${props.translate('common.bankAccount')}` : lodashGet(message, 'html', props.translate('iou.payerSettled', {amount: reportAmount}))} + {props.iouReport.isWaitingOnBankAccount + ? `${props.translate('iou.waitingOnBankAccount', {submitterDisplayName})} ${props.translate('common.bankAccount')}` + : lodashGet(message, 'html', props.translate('iou.payerSettled', {amount: reportAmount}))} )} diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index fc0f6d114ea2..5fa3bd635a89 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -215,7 +215,10 @@ function getOrderedReportIDs(reportIDFromRoute) { // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - return _.pluck([].concat(pinnedReports).concat(waitingOnBankAccountReports).concat(outstandingIOUReports).concat(draftReports).concat(nonArchivedReports).concat(archivedReports), 'reportID'); + return _.pluck( + [].concat(pinnedReports).concat(waitingOnBankAccountReports).concat(outstandingIOUReports).concat(draftReports).concat(nonArchivedReports).concat(archivedReports), + 'reportID', + ); } /** diff --git a/src/libs/actions/ReimbursementAccount/store.js b/src/libs/actions/ReimbursementAccount/store.js index 5e69467932b9..422c0ffc43dd 100644 --- a/src/libs/actions/ReimbursementAccount/store.js +++ b/src/libs/actions/ReimbursementAccount/store.js @@ -1,7 +1,8 @@ import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; +import _ from 'underscore'; import ONYXKEYS from '../../../ONYXKEYS'; -import BankAccount from "../../models/BankAccount"; +import BankAccount from '../../models/BankAccount'; /** Reimbursement account actively being set up */ let reimbursementAccountInSetup = {}; diff --git a/src/pages/home/report/ReportActionItemReimbursed.js b/src/pages/home/report/ReportActionItemReimbursed.js index b2b3c3947dec..1fca16dade58 100644 --- a/src/pages/home/report/ReportActionItemReimbursed.js +++ b/src/pages/home/report/ReportActionItemReimbursed.js @@ -14,7 +14,7 @@ const propTypes = { function ReportActionItemReimbursed(props) { return ( - + {props.translate('iou.payerSettledUp', {amount: props.amount})}{' '} {props.isFromSubmitterAddingBankAccount ? props.translate('iou.afterAddedBankAccount', {submitterDisplayName: props.submitterDisplayName}) : ''} diff --git a/src/pages/home/report/ReportActionItemReimbursementQueued.js b/src/pages/home/report/ReportActionItemReimbursementQueued.js index 4b11afe3be11..3691cc15d936 100644 --- a/src/pages/home/report/ReportActionItemReimbursementQueued.js +++ b/src/pages/home/report/ReportActionItemReimbursementQueued.js @@ -3,9 +3,9 @@ import PropTypes from 'prop-types'; import Text from '../../../components/Text'; import styles from '../../../styles/styles'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; -import TextLink from "../../../components/TextLink"; +import TextLink from '../../../components/TextLink'; import * as BankAccounts from '../../../libs/actions/BankAccounts'; -import * as store from "../../../libs/actions/ReimbursementAccount/store"; +import * as store from '../../../libs/actions/ReimbursementAccount/store'; const propTypes = { submitterDisplayName: PropTypes.string.isRequired, @@ -17,13 +17,16 @@ function ReportActionItemReimbursementQueued(props) { const shouldSubmitterAddBankAccount = props.isCurrentUserSubmitter && !store.hasCreditBankAccount(); return ( - + {props.translate('iou.waitingOnBankAccount', {submitterDisplayName: props.submitterDisplayName})} {shouldSubmitterAddBankAccount ? ( - {props.translate('common.bankAccount')} + {props.translate('common.bankAccount')} - ) : props.translate('common.bankAccount')}. + ) : ( + props.translate('common.bankAccount') + )} + . ); } diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index a322c4b51626..c3288c3eeb6a 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -148,7 +148,6 @@ function ReportActionsList(props) { // To notify there something changes we can use extraData prop to flatlist const extraData = [props.isSmallScreenWidth ? props.newMarkerReportActionID : undefined, ReportUtils.isArchivedRoom(props.report)]; const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(props.personalDetailsList, props.report, props.currentUserPersonalDetails.accountID); - return ( Date: Mon, 26 Jun 2023 01:11:43 -0700 Subject: [PATCH 009/325] add ts guidelines --- .../PROPTYPES_CONVERSION_TABLE.md | 18 + contributingGuides/TS_CHEATSHEET.md | 183 ++++++++++ contributingGuides/TS_STYLE.md | 335 ++++++++++++++++++ 3 files changed, 536 insertions(+) create mode 100644 contributingGuides/PROPTYPES_CONVERSION_TABLE.md create mode 100644 contributingGuides/TS_CHEATSHEET.md create mode 100644 contributingGuides/TS_STYLE.md diff --git a/contributingGuides/PROPTYPES_CONVERSION_TABLE.md b/contributingGuides/PROPTYPES_CONVERSION_TABLE.md new file mode 100644 index 000000000000..9149a6883151 --- /dev/null +++ b/contributingGuides/PROPTYPES_CONVERSION_TABLE.md @@ -0,0 +1,18 @@ +# Expensify PropTypes Conversation Table + +| PropTypes | TypeScript | Instructions | +| -------------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PropTypes.any` | `T`, `Record` or `any` | Figure out what would be the correct data type and use it.

If you know that it's a object but isn't possible to determine the internal structure, use `Record`. | +| `PropTypes.array` or `PropTypes.arrayOf(T)` | `T[]` or `Array` | Convert to `T[]` or `Array`, where `T` is the data type of the array.

If `T` isn't a primitive type, create a separate `type` for the object structure of your prop and use it. | +| `PropTypes.bool` | `boolean` | Convert to `boolean`. | +| `PropTypes.func` | `(arg1: Type1, arg2, Type2...) => ReturnType` | Convert to the function signature. | +| `PropTypes.number` | `number` | Convert to `number`. | +| `PropTypes.object`, `PropTypes.shape(T)` or `PropTypes.exact(T)` | `T` | If `T` isn't a primitive type, create a separate `type` for the `T` object structure of your prop and use it.

If you want an object but isn't possible to determine the internal structure, use `Record`. | +| `PropTypes.objectOf(T)` | `Record` | Convert to a `Record` where `T` is the data type of your dictionary.

If `T` isn't a primitive type, create a separate `type` for the object structure and use it. | +| `PropTypes.string` | `string` | Convert to `string`. | +| `PropTypes.node` | `React.ReactNode` | Convert to `React.ReactNode`. `ReactNode` includes `ReactElement` as well as other types such as `strings`, `numbers`, `arrays` of the same, `null`, and `undefined` In other words, anything that can be rendered in React is a `ReactNode`. | +| `PropTypes.element` | `React.ReactElement` | Convert to `React.ReactElement`. | +| `PropTypes.symbol` | `symbol` | Convert to `symbol`. | +| `PropTypes.elementType` | `React.ElementType` | Convert to `React.ElementType`. | +| `PropTypes.instanceOf(T)` | `T` | Convert to `T`. | +| `PropTypes.oneOf([T, U, ...])` or `PropTypes.oneOfType([T, U, ...])` | `T \| U \| ...` | Convert to a union type e.g. `T \| U \| ...`. | diff --git a/contributingGuides/TS_CHEATSHEET.md b/contributingGuides/TS_CHEATSHEET.md new file mode 100644 index 000000000000..e056d36ddb07 --- /dev/null +++ b/contributingGuides/TS_CHEATSHEET.md @@ -0,0 +1,183 @@ +# Expensify TypeScript React Native CheatSheet + +## Table of Contents + +- [1.1 `props.children`](#convension-children) +- [1.2 `forwardRef`](#forwardRef) +- [1.3 Animated styles](#animated-style) +- [1.4 Style Props](#style-props) +- [1.5 Render Prop](#render-prop) +- [1.6 Type Narrowing](#type-narrowing) +- [1.7 Errors in Type Catch Clauses](#try-catch-errors) + +## CheatSheet + +- [1.1](#convension-children) **`props.children`** + + ```tsx + type WrapperComponentProps = { + children?: React.ReactNode; + }; + + function WrapperComponent({ children }: Props) { + return {children}; + } + + function App() { + return ( + + + + ); + } + ``` + +- [1.2](#forwardRef) **`forwardRef`** + + ```ts + import { forwardRef, useRef, ReactNode } from "react"; + import { TextInput, View } from "react-native"; + + export type CustomButtonProps = { + label: string; + children?: ReactNode; + }; + + const CustomTextInput = forwardRef( + (props, ref) => { + return ( + + + {props.children} + + ); + } + ); + + function ParentComponent() { + const ref = useRef; + return ; + } + ``` + +- [1.3](#animated-style) **Animated styles** + + ```ts + import {useRef} from 'react'; + import {Animated, StyleProp, ViewStyle} from 'react-native'; + + type MyComponentProps = { + style?: Animated.WithAnimatedValue>; + }; + + function MyComponent({ style }: Props) { + return ; + } + + function MyComponent() { + const anim = useRef(new Animated.Value(0)).current; + return ; + } + ``` + +- [1.4](#style-props) **Style Props** + + When converting or typing style props, use `StyleProp` type where `T` is the type of styles related to the component your prop is going to apply. + + ```tsx + import { StyleProp, ViewStyle, TextStyle, ImageStyle } from "react-native"; + + type MyComponentProps = { + containerStyle?: StyleProp; + textStyle?: StyleProp; + imageStyle?: StyleProp; + }; + + function MyComponentProps({ containerStyle, textStyle, imageStyle }: MyComponentProps) = { + + Sample Image + + + } + ``` + +- [1.5](#render-prop) **Render Prop** + + ```tsx + type ParentComponentProps = { + children: (label: string) => React.ReactNode; + }; + + function ParentComponent({ children }: Props) { + return children("String being injected"); + } + + function App() { + return ( + + {(label) => ( + + {label} + + )} + + ); + } + ``` + +- [1.6](#type-narrowing) **Type Narrowing** Narrow types down using `typeof` or custom type guards. + + ```ts + type Manager = { + role: "manager"; + team: string; + }; + + type Engineer = { + role: "engineer"; + language: "ts" | "js" | "php"; + }; + + function introduce(employee: Manager | Engineer) { + console.log(employee.team); // TypeScript errors: Property 'team' does not exist on type 'Manager | Engineer'. + + if (employee.role === "manager") { + console.log(`I manage ${employee.team}`); // employee: Manager + } else { + console.log(`I write ${employee.language}`); // employee: Engineer + } + } + ``` + + In the above code, type narrowing is used to determine whether an employee object is a Manager or an Engineer based on the role property, allowing safe access to the `team` property for managers and the `language` property for engineers. + + We can also create a custom type guard function. + + ```ts + function isManager(employee: Manager | Engineer): employee is Manager { + return employee.role === "manager"; + } + + function introduce(employee: Manager | Engineer) { + if (isManager(employee)) { + console.log(`I manage ${employee.team}`); // employee: Manager + } + } + ``` + + In the above code, `employee is Manager` is Type Predicate. It signifies that the return type of `isManager` is a `boolean`, indicating whether a value passed to the function is of a certain type (e.g. `Manager`). + +- [1.7] **Error in Try Catch Clauses** + + Errors in try/catch clauses are typed as unknown, if the developer needs to use the error data they must conditionally check the type of the data first. Use type narrowing + + ```ts + try { + .... + } catch (e) { // `e` is `unknown`. + if (e instanceof Error) { + // you can access properties on Error + console.error(e.message); + } + } + ``` diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md new file mode 100644 index 000000000000..5b7d5290b079 --- /dev/null +++ b/contributingGuides/TS_STYLE.md @@ -0,0 +1,335 @@ +# Expensify TypeScript Style Guide + +## Other Expensify Resources on TypeScript + +- [Expensify TypeScript React Native CheatSheet](./TS_CHEATSHEET.md) +- [Expensify TypeScript PropTypes Conversion Table](./PROPTYPES_CONVERSION_TABLE.md) + +## Learning Sources + +### Quickest way to learn TypeScript + +- Get up to speed quickly + - [TypeScript playground](https://www.typescriptlang.org/play?q=231#example) + - Go though all examples on the playground. Click on "Example" tab on the top +- Handy Reference + - [TypeScript CheatSheet](https://www.typescriptlang.org/static/TypeScript%20Types-ae199d69aeecf7d4a2704a528d0fd3f9.png) + - [Type](https://www.typescriptlang.org/static/TypeScript%20Types-ae199d69aeecf7d4a2704a528d0fd3f9.png) + - [Control Flow Analysis](https://www.typescriptlang.org/static/TypeScript%20Control%20Flow%20Analysis-8a549253ad8470850b77c4c5c351d457.png) +- TypeScript with React + - [React TypeScript CheatSheet](https://react-typescript-cheatsheet.netlify.app/) + - [List of built-in utility types](https://react-typescript-cheatsheet.netlify.app/docs/basic/troubleshooting/utilities) + - [HOC CheatSheet](https://react-typescript-cheatsheet.netlify.app/docs/hoc/) + +## Table of Contents + +- [1.1 Naming Conventions](#convension-naming-convension) +- [1.2 `d.ts` Extension](#convensions-d-ts-extension) +- [1.3 Type Alias vs. Interface](#convensions-type-alias-vs-interface) +- [1.4 Enum vs. Union Type](#convensions-enum-vs-union-type) +- [1.5 `unknown` vs. `any`](#convensions-unknown-vs-any) +- [1.6 `T[]` vs. `Array`](#convensions-array) +- [1.7 @ts-ignore](#convension-ts-ignore) +- [1.8 Optional chaining and nullish coalescing](#convension-ts-nullish-coalescing) +- [1.9 Type Inference](#convension-type-inference) +- [1.10 JSDoc](#conventions-jsdoc) +- [1.11 `propTypes` and `defaultProps`](#convension-proptypes-and-defaultprops) +- [1.12 Utility Types](#convension-utility-types) +- [1.13 `object` Type](#convension-object-type) +- [1.14 Export Prop Types](#convension-export-prop-types) +- [Communication Items](#communication-items) +- [Migration Guidelines](#migration-guidelines) + +## Exception to Rules + +Most of the rules are enforced in ESLint or checked by TypeScript. If you think your particular situation warrants an exception, post the context in the `#expensify-open-source` Slack channel with your message prefixed with `TS EXCEPTION:`. Internal engineers will assess the case and suggest alternative or grants an exception. When an exception is granted, link the relevant Slack conversation in your pull request. Suppress ESLint or TypeScript warnings/errors with comments if necessary. + +This rule will apply until the migration is done. After the migration, exceptions are assessed and granted by PR reviewers. + +## Conventions + + + +- [1.1](#convension-naming-convension) **Naming Conventions**: Use PascalCase for type names. Do not postfix type aliases with `Type` + + ```ts + // bad + type foo = ...; + type BAR = ...; + type PersonType = ...; + + // good + type Foo = ...; + type Bar = ...; + type Person = ...; + ``` + + + +- [1.2](#convensions-d-ts-extension) **`d.ts` Extension**: Do not use `d.ts` file extension even when a file contains only type declarations. + + > Why? Type errors in `d.ts` files are not checked by TypeScript [^1]. + +[^1]: This is because `skipLibCheck` TypeScript configuration is set to `true` in this project. + + + +- [1.3](#convensions-type-alias-vs-interface) **Type Alias vs. Interface**: Do not use `interface`. Use `type`. + + > Why? In TypeScript, `type` and `interface` can be used interchangeably to declare types. Use `type` for consistency. + + ```ts + // bad + interface Person { + name: string; + } + + // good + type Person = { + name: string; + }; + ``` + + + +- [1.4](#convensions-enum-vs-union-type) **Enum vs. Union Type**: Do not use `enum`. Use union types. + + > Why? Enums come with several [pitfalls](https://blog.logrocket.com/why-typescript-enums-suck/). Most enum use cases can be replaced with union types. + + ```ts + // Most simple form of union type. + type Color = "red" | "green" | "blue"; + function printColors(color: Color) { + console.log(color); + } + + // When the values need to be iterated upon. + import { TupleToUnion } from "type-fest"; + + const COLORS = ["red", "green", "blue"] as const; + type Color = TupleToUnion; // type: 'red' | 'green' | 'blue' + + for (const colors of color) { + printColor(color); + } + + // When the values should be accessed through object keys. (i.e. `COLORS.Red` vs. `"red"`) + import { ValueOf } from "type-fest"; + + const COLORS = { + Red: "red", + Green: "green", + Blue: "blue", + } as const; + type Color = ValueOf; // type: 'red' | 'green' | 'blue' + + printColor(COLORS.Red); + ``` + + + +- [1.5](#convensions-unknown-vs-any) **`unknown` vs. `any`**: Don't use `any`. Use `unknown` if type is not known beforehand + + > Why? `any` type bypasses type checking. `unknown` is type safe as `unknown` type needs to be type narrowed before being used. + + ```ts + const value: unknown = JSON.parse(someJson); + if (typeof value === 'string') {...} + else if (isPerson(value)) {...} + ... + ``` + + + +- [1.6](#convensions-array) **`T[]` vs. `Array`**: Use T[] or readonly T[] for simple types (i.e. types which are just primitive names or type references). Use Array or ReadonlyArray for all other types (union types, intersection types, object types, function types, etc). + + ```ts + // Array + const a: Array = ["a", "b"]; + const b: Array<{ prop: string }> = [{ prop: "a" }]; + const c: Array<() => void> = [() => {}]; + + // T[] + const d: MyType[] = ["a", "b"]; + const e: string[] = ["a", "b"]; + const f: readonly string[] = ["a", "b"]; + ``` + + + +- [1.7](#convension-ts-ignore) **@ts-ignore**: Do not use `@ts-ignore` or its variant `@ts-nocheck` to suppress warnings and errors. Use `@ts-expect-error` during the migration for type errors that should be handled later. + + + +- [1.8](#convension-ts-nullish-coalescing) **Optional chaining and nullish coalescing**: Use optional chaining and nullish coalescing instead of the `get` lodash function. + + ```ts + // Bad + import { get } from "lodash"; + const name = lodashGet(user, "name", "default name"); + + // Good + const name = user?.name ?? "default name"; + ``` + + + +- [1.9](#convension-type-inference) **Type Inference**: When possible, allow the compiler to infer type of variables. + + ```ts + // Bad + const foo: string = "foo"; + const [counter, setCounter] = useState(0); + + // Good + const foo = "foo"; + const [counter, setCounter] = useState(0); + const [username, setUsername] = useState(undefined); // Username is a union type of string and undefined, and its type cannot be interred from the default value of undefined + ``` + + For function return types, default to always typing them unless a function is simple enough to reason about its return type. + + > Why? Explicit return type helps catch errors when implementation of the function changes. It also makes it easy to read code even when TypeScript intellisense is not provided. + + ```ts + function simpleFunction(name: string) { + return `hello, ${name}`; + } + + function complicatedFunction(name: string): boolean { + // ... some complex logic here ... + return foo; + } + ``` + + + +- [1.10](#conventions-jsdoc) **JSDoc**: Omit comments that are redundant with TypeScript. Do not declare types in `@param` or `@return` blocks. Do not write `@implements`, `@enum`, `@private`, `@override` + + ```ts + // bad + /** + * @param {number} age + * @returns {boolean} Whether the person is a legal drinking age or nots + */ + function canDrink(age: number): boolean { + return age >= 21; + } + + // good + /** + * @param age + * @returns Whether the person is a legal drinking age or nots + */ + ``` + + + +- [1.11](#convension-proptypes-and-defaultprops) **`propTypes` and `defaultProps`**: Do not use them. Use object destructing to assign default values if necessary. + + > Refer to [the propTypes Migration Table](./PROPTYPES_CONVERSION_TABLE.md) on how to type props based on existing `propTypes`. + + ```tsx + type GreetingProps = { + greeting: string; + name: string; + }; + + function Greeting({ greeting = "hello", name = "world" }: ComponentProps) { + {`${greeting}, ${name}`}; + } + ``` + + + +- [1.12](#convension-utility-types) **Utility Types**: Use types from [TypeScript utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) and [`type-fest`](https://github.com/sindresorhus/type-fest) when possible. + + ```ts + type Foo = { + bar: string; + }; + + // good + type ReadOnlyFoo = Readonly; + + // bad + type ReadOnlyFoo = { + readonly [Property in keyof Foo]: Foo[Property]; + }; + ``` + + + +- [1.13](#convension-object-type) **`object`**: Don't use `object` type. + +> Why? `object` refers to "any non-primitive type," not "any object". Typing "any non-primitive value" is not commonly needed. + +```ts +// bad +const foo: object = [1, 2, 3]; // TypeScript does not error +``` + +If you know that the type of data is an object but don't know what properties or values it has beforehand, use `Record`. + + + +- [1.14](#convension-export-prop-types) **Prop Types**: Define and export prop types for components. Use exported prop types instead of grabbing the prop type from a component. + +> Why? Exporting prop types aids reusability. + +```tsx +// MyComponent.tsx +export type MyComponentProps = { + foo: string; +}; + +export default function MyComponent({ foo }: MyComponentProps) { + return {foo}; +} + +// bad +import { ComponentProps } from "React"; +import MyComponent from "./MyComponent"; +type MyComponentProps = ComponentProps; + +// good +import MyComponent, { MyComponentProps } from "./MyComponent"; +``` + +## Communication Items + +> Comment in the `#expensify-open-source` Slack channel if any of the following situations are encountered. Each comment should be prefixed with `TS ATTENTION:`. Internal engineers will access each situation and prescribe solutions to each case. Internal engineers should refer to general solutions to each situation that follows each list item. + +- I think types definitions in a third party library is incomplete or incorrect + + When the library indeed contains incorrect type definitions and it cannot be updated, use module argumentation to correct them. + + ```ts + declare module "external-library-name" { + interface LibraryComponentProps { + // Add or modify typings + additionalProp: string; + } + } + ``` + +## Migration Guidelines + +> This section contains instructions that are applicable during the migration. + +- Found type bugs. Now what? + + If TypeScript migration uncovers a bug that has been “invisible,” there are two options an author of a migration PR can take + + - Fix issues if they are minor. Document each fix in the PR comment + - Suppress a TypeScript error stemming from the bug with `@ts-expect-error`. Create a separate GH issue. Prefix the issue title with `[TS ERROR #]`. Cross-link the migration PR and the created GH issue + + The `@ts-expect-error` annotation tells the TS compiler to ignore any errors in the line that follows it. However, if there's no error in the line, TypeScript will also raise an error. + + ```ts + // @ts-expect-error + const x: number = "This is a string"; // No TS error raised + + // @ts-expect-error + const y: number = 123; // TS error: Unused '@ts-expect-error' directive. + ``` From c218adc03e7df487008269745275ab5797ec836f Mon Sep 17 00:00:00 2001 From: Hayata Suenaga Date: Mon, 26 Jun 2023 01:55:07 -0700 Subject: [PATCH 010/325] fix issues --- contributingGuides/TS_CHEATSHEET.md | 22 +++++++++++--- contributingGuides/TS_STYLE.md | 45 +++++++++++++++-------------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/contributingGuides/TS_CHEATSHEET.md b/contributingGuides/TS_CHEATSHEET.md index e056d36ddb07..c5a6723da459 100644 --- a/contributingGuides/TS_CHEATSHEET.md +++ b/contributingGuides/TS_CHEATSHEET.md @@ -2,17 +2,19 @@ ## Table of Contents -- [1.1 `props.children`](#convension-children) +- [1.1 `props.children`](#children-prop) - [1.2 `forwardRef`](#forwardRef) - [1.3 Animated styles](#animated-style) - [1.4 Style Props](#style-props) - [1.5 Render Prop](#render-prop) - [1.6 Type Narrowing](#type-narrowing) -- [1.7 Errors in Type Catch Clauses](#try-catch-errors) +- [1.7 Errors in Type Catch Clauses](#try-catch-clauses) ## CheatSheet -- [1.1](#convension-children) **`props.children`** + + +- [1.1](#children-prop) **`props.children`** ```tsx type WrapperComponentProps = { @@ -32,6 +34,8 @@ } ``` + + - [1.2](#forwardRef) **`forwardRef`** ```ts @@ -60,6 +64,8 @@ } ``` + + - [1.3](#animated-style) **Animated styles** ```ts @@ -80,6 +86,8 @@ } ``` + + - [1.4](#style-props) **Style Props** When converting or typing style props, use `StyleProp` type where `T` is the type of styles related to the component your prop is going to apply. @@ -101,6 +109,8 @@ } ``` + + - [1.5](#render-prop) **Render Prop** ```tsx @@ -125,6 +135,8 @@ } ``` + + - [1.6](#type-narrowing) **Type Narrowing** Narrow types down using `typeof` or custom type guards. ```ts @@ -167,7 +179,9 @@ In the above code, `employee is Manager` is Type Predicate. It signifies that the return type of `isManager` is a `boolean`, indicating whether a value passed to the function is of a certain type (e.g. `Manager`). -- [1.7] **Error in Try Catch Clauses** + + +- [1.7](#try-catch-clauses) **Error in Try Catch Clauses** Errors in try/catch clauses are typed as unknown, if the developer needs to use the error data they must conditionally check the type of the data first. Use type narrowing diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index 5b7d5290b079..9923e6317283 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -1,11 +1,33 @@ # Expensify TypeScript Style Guide +## Table of Contents + +- [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript) +- [Learning Resources](#learning-resources) +- [Exception to Rules](#exception-to-rules) +- [1.1 Naming Conventions](#convension-naming-convension) +- [1.2 `d.ts` Extension](#convensions-d-ts-extension) +- [1.3 Type Alias vs. Interface](#convensions-type-alias-vs-interface) +- [1.4 Enum vs. Union Type](#convensions-enum-vs-union-type) +- [1.5 `unknown` vs. `any`](#convensions-unknown-vs-any) +- [1.6 `T[]` vs. `Array`](#convensions-array) +- [1.7 @ts-ignore](#convension-ts-ignore) +- [1.8 Optional chaining and nullish coalescing](#convension-ts-nullish-coalescing) +- [1.9 Type Inference](#convension-type-inference) +- [1.10 JSDoc](#conventions-jsdoc) +- [1.11 `propTypes` and `defaultProps`](#convension-proptypes-and-defaultprops) +- [1.12 Utility Types](#convension-utility-types) +- [1.13 `object` Type](#convension-object-type) +- [1.14 Export Prop Types](#convension-export-prop-types) +- [Communication Items](#communication-items) +- [Migration Guidelines](#migration-guidelines) + ## Other Expensify Resources on TypeScript - [Expensify TypeScript React Native CheatSheet](./TS_CHEATSHEET.md) - [Expensify TypeScript PropTypes Conversion Table](./PROPTYPES_CONVERSION_TABLE.md) -## Learning Sources +## Learning Resources ### Quickest way to learn TypeScript @@ -21,32 +43,13 @@ - [List of built-in utility types](https://react-typescript-cheatsheet.netlify.app/docs/basic/troubleshooting/utilities) - [HOC CheatSheet](https://react-typescript-cheatsheet.netlify.app/docs/hoc/) -## Table of Contents - -- [1.1 Naming Conventions](#convension-naming-convension) -- [1.2 `d.ts` Extension](#convensions-d-ts-extension) -- [1.3 Type Alias vs. Interface](#convensions-type-alias-vs-interface) -- [1.4 Enum vs. Union Type](#convensions-enum-vs-union-type) -- [1.5 `unknown` vs. `any`](#convensions-unknown-vs-any) -- [1.6 `T[]` vs. `Array`](#convensions-array) -- [1.7 @ts-ignore](#convension-ts-ignore) -- [1.8 Optional chaining and nullish coalescing](#convension-ts-nullish-coalescing) -- [1.9 Type Inference](#convension-type-inference) -- [1.10 JSDoc](#conventions-jsdoc) -- [1.11 `propTypes` and `defaultProps`](#convension-proptypes-and-defaultprops) -- [1.12 Utility Types](#convension-utility-types) -- [1.13 `object` Type](#convension-object-type) -- [1.14 Export Prop Types](#convension-export-prop-types) -- [Communication Items](#communication-items) -- [Migration Guidelines](#migration-guidelines) - ## Exception to Rules Most of the rules are enforced in ESLint or checked by TypeScript. If you think your particular situation warrants an exception, post the context in the `#expensify-open-source` Slack channel with your message prefixed with `TS EXCEPTION:`. Internal engineers will assess the case and suggest alternative or grants an exception. When an exception is granted, link the relevant Slack conversation in your pull request. Suppress ESLint or TypeScript warnings/errors with comments if necessary. This rule will apply until the migration is done. After the migration, exceptions are assessed and granted by PR reviewers. -## Conventions +## Guidelines From 474125f035d2e49bf6e195d7804ee79c39da9486 Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 26 Jun 2023 17:04:08 +0200 Subject: [PATCH 011/325] Made array containerStyle prop --- src/pages/ReimbursementAccount/AddressForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 4f700d7c23a1..39fa73b97076 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -117,7 +117,7 @@ function AddressForm(props) { defaultValue={props.defaultValues.city} onChangeText={(value) => props.onFieldChange({city: value})} errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} - containerStyles={styles.mt4} + containerStyles={[styles.mt4]} /> From b40a302beeae3dedd272730b457c7fe1f65f9c35 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga Date: Mon, 26 Jun 2023 22:26:33 -0700 Subject: [PATCH 012/325] add section on file organization --- contributingGuides/TS_STYLE.md | 129 +++++++++++++++++++++++++-------- 1 file changed, 97 insertions(+), 32 deletions(-) diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index 9923e6317283..8fa8827e4762 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -19,6 +19,7 @@ - [1.12 Utility Types](#convension-utility-types) - [1.13 `object` Type](#convension-object-type) - [1.14 Export Prop Types](#convension-export-prop-types) +- [1.15 File Organization](#convension-file-organization) - [Communication Items](#communication-items) - [Migration Guidelines](#migration-guidelines) @@ -265,39 +266,102 @@ This rule will apply until the migration is done. After the migration, exception - [1.13](#convension-object-type) **`object`**: Don't use `object` type. -> Why? `object` refers to "any non-primitive type," not "any object". Typing "any non-primitive value" is not commonly needed. + > Why? `object` refers to "any non-primitive type," not "any object". Typing "any non-primitive value" is not commonly needed. -```ts -// bad -const foo: object = [1, 2, 3]; // TypeScript does not error -``` + ```ts + // bad + const foo: object = [1, 2, 3]; // TypeScript does not error + ``` + + If you know that the type of data is an object but don't know what properties or values it has beforehand, use `Record`. -If you know that the type of data is an object but don't know what properties or values it has beforehand, use `Record`. + > Even though `string` is specified as a key, `Record` type can still accepts objects whose keys are numbers or symbols. This is because number and + + ```ts + function logObject(object: Record) { + for (const [key, value] of Object.entries(object)) { + console.log(`${key}: ${value}`); + } + } + ``` - [1.14](#convension-export-prop-types) **Prop Types**: Define and export prop types for components. Use exported prop types instead of grabbing the prop type from a component. -> Why? Exporting prop types aids reusability. + > Why? Exporting prop types aids reusability. + + ```tsx + // MyComponent.tsx + export type MyComponentProps = { + foo: string; + }; + + export default function MyComponent({ foo }: MyComponentProps) { + return {foo}; + } -```tsx -// MyComponent.tsx -export type MyComponentProps = { - foo: string; -}; + // bad + import { ComponentProps } from "React"; + import MyComponent from "./MyComponent"; + type MyComponentProps = ComponentProps; -export default function MyComponent({ foo }: MyComponentProps) { - return {foo}; -} + // good + import MyComponent, { MyComponentProps } from "./MyComponent"; + ``` -// bad -import { ComponentProps } from "React"; -import MyComponent from "./MyComponent"; -type MyComponentProps = ComponentProps; + -// good -import MyComponent, { MyComponentProps } from "./MyComponent"; -``` +- [1.15](#convension-file-organization) **File organization**: In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files. + + > Why? To encourage consistent API across platform-specific implementations. + + ```ts + // types.ts + type GreetingModule = { + sayHello: () => boolean; + sayGoodbye: () => boolean; + }; + + // index.native.ts + import { GreetingModule } from "./types.ts"; + function sayHello() { + console.log("hello from mobile code"); + } + function sayGoodbye() { + console.log("goodbye from mobile code"); + } + const Greeting: GreetingModule = { + sayHello, + sayGoodbye, + }; + export default Greeting; + + // index.ts + import { GreetingModule } from "./types.ts"; + ... + const Greeting: GreetingModule = { + ... + ``` + + ```ts + // types.ts + export type MyComponentProps = { + foo: string; + } + + // index.ios.ts + import { MyComponentProps } from ./types.ts; + + export MyComponentProps; + export default function MyComponent({ foo }: MyComponentProps) {...} + + // index.ts + import { MyComponentProps } from ./types.ts; + + export MyComponentProps; + export default function MyComponent({ foo }: MyComponentProps) {...} + ``` ## Communication Items @@ -305,16 +369,16 @@ import MyComponent, { MyComponentProps } from "./MyComponent"; - I think types definitions in a third party library is incomplete or incorrect - When the library indeed contains incorrect type definitions and it cannot be updated, use module argumentation to correct them. +When the library indeed contains incorrect type definitions and it cannot be updated, use module augmentation to correct them. All module augmentation code should be contained in `global.d.ts`. - ```ts - declare module "external-library-name" { - interface LibraryComponentProps { - // Add or modify typings - additionalProp: string; - } +```ts +declare module "external-library-name" { + interface LibraryComponentProps { + // Add or modify typings + additionalProp: string; } - ``` +} +``` ## Migration Guidelines @@ -325,13 +389,14 @@ import MyComponent, { MyComponentProps } from "./MyComponent"; If TypeScript migration uncovers a bug that has been “invisible,” there are two options an author of a migration PR can take - Fix issues if they are minor. Document each fix in the PR comment - - Suppress a TypeScript error stemming from the bug with `@ts-expect-error`. Create a separate GH issue. Prefix the issue title with `[TS ERROR #]`. Cross-link the migration PR and the created GH issue + - Suppress a TypeScript error stemming from the bug with `@ts-expect-error`. Create a separate GH issue. Prefix the issue title with `[TS ERROR #]`. Cross-link the migration PR and the created GH issue. On the line below `@ts-expect-error`, put down the GH issue number prefixed with `TODO:`. The `@ts-expect-error` annotation tells the TS compiler to ignore any errors in the line that follows it. However, if there's no error in the line, TypeScript will also raise an error. ```ts // @ts-expect-error - const x: number = "This is a string"; // No TS error raised + // TODO: #21647 + const x: number = "123"; // No TS error raised // @ts-expect-error const y: number = 123; // TS error: Unused '@ts-expect-error' directive. From 0418d9a64bd1482e62cde3e343684389b35453f6 Mon Sep 17 00:00:00 2001 From: Nathalie Kuoch Date: Tue, 27 Jun 2023 11:26:16 +0200 Subject: [PATCH 013/325] Add translations --- src/components/MoneyRequestHeader.js | 2 +- src/languages/en.js | 3 +-- src/languages/es.js | 5 ++--- src/pages/home/report/ReportActionItemReimbursed.js | 3 +-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 43bfebfc70c5..14ca3493599b 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -94,7 +94,7 @@ function MoneyRequestHeader(props) { if (isSettled) { description += ` • ${props.translate('iou.settledExpensify')}`; } else if (props.report.isWaitingOnBankAccount) { - description += ` • Waiting for credit account`; + description += ` • ${props.translate('iou.pending')}`; } return ( diff --git a/src/languages/en.js b/src/languages/en.js index eecf4f86e37e..9973b362c100 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -361,9 +361,8 @@ export default { payerOwesAmount: ({payer, amount}) => `${payer} owes ${amount}`, payerPaidAmount: ({payer, amount}) => `${payer} paid ${amount}`, payerSettled: ({amount}) => `paid ${amount}`, - payerSettledUp: ({amount}) => `settled up ${amount}`, waitingOnBankAccount: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a `, - afterAddedBankAccount: ({submitterDisplayName}) => `after ${submitterDisplayName} added a bank account`, + settledAfterAddedBankAccount: ({submitterDisplayName}) => `Payment settled after ${submitterDisplayName} added a bank account`, noReimbursableExpenses: 'This report has an invalid amount', pendingConversionMessage: "Total will update when you're back online", threadRequestReportName: ({formattedAmount, comment}) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, diff --git a/src/languages/es.js b/src/languages/es.js index 1da1cdf74965..f10af2f308e4 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -360,9 +360,8 @@ export default { payerOwesAmount: ({payer, amount}) => `${payer} debe ${amount}`, payerPaidAmount: ({payer, amount}) => `${payer} pagó ${amount}`, payerSettled: ({amount}) => `pagó ${amount}`, - payerSettledUp: ({amount}) => `pagó ${amount}`, - waitingOnBankAccount: ({submitterDisplayName}) => `comenzó a establecerse, el pago se retiene hasta que ${submitterDisplayName} agrega una `, - afterAddedBankAccount: ({submitterDisplayName}) => `después ${submitterDisplayName} agregó una cuenta bancaria`, + waitingOnBankAccount: ({submitterDisplayName}) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} agregue una `, + settledAfterAddedBankAccount: ({submitterDisplayName}) => `Pago realizado cuando ${submitterDisplayName} agregó su cuenta bancaria`, noReimbursableExpenses: 'El monto de este informe es inválido', pendingConversionMessage: 'El total se actualizará cuando estés online', threadRequestReportName: ({formattedAmount, comment}) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, diff --git a/src/pages/home/report/ReportActionItemReimbursed.js b/src/pages/home/report/ReportActionItemReimbursed.js index 1fca16dade58..127a0ccf38ee 100644 --- a/src/pages/home/report/ReportActionItemReimbursed.js +++ b/src/pages/home/report/ReportActionItemReimbursed.js @@ -15,8 +15,7 @@ const propTypes = { function ReportActionItemReimbursed(props) { return ( - {props.translate('iou.payerSettledUp', {amount: props.amount})}{' '} - {props.isFromSubmitterAddingBankAccount ? props.translate('iou.afterAddedBankAccount', {submitterDisplayName: props.submitterDisplayName}) : ''} + {props.isFromSubmitterAddingBankAccount ? props.translate('iou.settledAfterAddedBankAccount', {submitterDisplayName: props.submitterDisplayName}) : props.translate('iou.payerSettled', {amount: props.amount})} ); } From b8198c8ab30ca2546cff4cd465282c07ec74b776 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 27 Jun 2023 13:19:38 +0200 Subject: [PATCH 014/325] add writeCapability to create new room --- src/libs/ReportUtils.js | 3 +++ src/libs/actions/Report.js | 5 ++++- src/pages/workspace/WorkspaceNewRoomPage.js | 21 +++++++++++++++++---- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 83fae8287c74..f3161cbc1ad0 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1499,6 +1499,7 @@ function buildOptimisticTaskReportAction(taskReportID, actionName, message = '') * @param {Boolean} isOwnPolicyExpenseChat * @param {String} oldPolicyName * @param {String} visibility + * @param {String} writeCapability * @param {String} notificationPreference * @param {String} parentReportActionID * @param {String} parentReportID @@ -1514,6 +1515,7 @@ function buildOptimisticChatReport( isOwnPolicyExpenseChat = false, oldPolicyName = '', visibility = undefined, + writeCapability = CONST.REPORT.WRITE_CAPABILITIES.ALL, notificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, parentReportActionID = '', parentReportID = '', @@ -1545,6 +1547,7 @@ function buildOptimisticChatReport( statusNum: 0, visibility, welcomeMessage: '', + writeCapability, }; } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 77ff1485a3d1..5243714eec97 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1198,8 +1198,9 @@ function navigateToConciergeChat() { * @param {String} reportName * @param {String} visibility * @param {Array} policyMembers + * @param {String} writeCapability */ -function addPolicyReport(policyID, reportName, visibility, policyMembers) { +function addPolicyReport(policyID, reportName, visibility, policyMembers, writeCapability) { // The participants include the current user (admin) and the employees. Participants must not be empty. const participants = _.unique([currentUserAccountID, ...policyMembers]); const policyReport = ReportUtils.buildOptimisticChatReport( @@ -1212,6 +1213,7 @@ function addPolicyReport(policyID, reportName, visibility, policyMembers) { false, '', visibility, + writeCapability, // The room might contain all policy members so notifying always should be opt-in only. CONST.REPORT.NOTIFICATION_PREFERENCE.DAILY, @@ -1278,6 +1280,7 @@ function addPolicyReport(policyID, reportName, visibility, policyMembers) { visibility, reportID: policyReport.reportID, createdReportActionID: createdReportAction.reportActionID, + writeCapability, }, {optimisticData, successData, failureData}, ); diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 361610266b42..acceb0cb7d17 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -81,9 +81,9 @@ class WorkspaceNewRoomPage extends React.Component { /** * @param {Object} values - form input values passed by the Form component */ - submit(values) { - const policyMembers = _.map(_.keys(this.props.allPolicyMembers[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${values.policyID}`]), (accountID) => Number(accountID)); - Report.addPolicyReport(values.policyID, values.roomName, values.visibility, policyMembers); + submit({policyID, roomName, visibility, writeCapability}) { + const policyMembers = _.map(_.keys(this.props.allPolicyMembers[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`]), (accountID) => Number(accountID)); + Report.addPolicyReport(policyID, roomName, visibility, policyMembers, writeCapability); } /** @@ -132,6 +132,11 @@ class WorkspaceNewRoomPage extends React.Component { return null; } + const writeCapabilityOptions = _.map(CONST.REPORT.WRITE_CAPABILITIES, (value) => ({ + value, + label: this.props.translate(`writeCapabilityPage.writeCapability.${value}`), + })); + // Workspaces are policies with type === 'free' const workspaceOptions = _.map( _.filter(this.props.policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE), @@ -169,7 +174,15 @@ class WorkspaceNewRoomPage extends React.Component { shouldDelayFocus={shouldDelayFocus} /> - + + + + Date: Tue, 27 Jun 2023 18:40:35 +0200 Subject: [PATCH 015/325] update admin room welcome message --- src/languages/en.js | 4 ++-- src/libs/ReportUtils.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index 17653692828e..65d79033e173 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -285,8 +285,8 @@ export default { beginningOfArchivedRoomPartTwo: ", there's nothing to see here.", beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}) => `Collaboration with everyone at ${domainRoom} starts here! 🎉\nUse `, beginningOfChatHistoryDomainRoomPartTwo: ' to chat with colleagues, share tips, and ask questions.', - beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}) => `Collaboration among ${workspaceName} admins starts here! 🎉\nUse `, - beginningOfChatHistoryAdminRoomPartTwo: ' to chat about topics such as workspace configurations and more.', + beginningOfChatHistoryAdminRoomPartOne: () => 'Use ', + beginningOfChatHistoryAdminRoomPartTwo: ({workspaceName}) => ` to hear about important announcements related to ${workspaceName}`, beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}) => `Collaboration between all ${workspaceName} members starts here! 🎉\nUse `, beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}) => ` to chat about anything ${workspaceName} related.`, beginningOfChatHistoryUserRoomPartOne: 'Collaboration starts here! 🎉\nUse this space to chat about anything ', diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index f3161cbc1ad0..7c1e18612070 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -572,8 +572,8 @@ function getRoomWelcomeMessage(report) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report.reportName}); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartTwo'); } else if (isAdminRoom(report)) { - welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne', {workspaceName}); - welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartTwo'); + welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne'); + welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartTwo', {workspaceName}); } else if (isAnnounceRoom(report)) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartOne', {workspaceName}); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartTwo', {workspaceName}); From 43a46cc6cbc6040a91f9b6dfa5cd6409e2d04140 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 27 Jun 2023 19:40:47 +0200 Subject: [PATCH 016/325] filter out admin-only rooms for members --- src/libs/ReportUtils.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 7c1e18612070..87ed25866e46 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1953,6 +1953,14 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, iouRep return false; } + // TODO: Add description + if (report.writeCapability === CONST.REPORT.WRITE_CAPABILITIES.ADMINS) { + const linkedWorkspace = _.find(policies, (policy) => policy && policy.id === report.policyID); + const shouldAllowShare = lodashGet(linkedWorkspace, 'role', '') === CONST.POLICY.ROLE.ADMIN; + + return shouldAllowShare; + } + return true; } From 8135994196745fe2bd31d6e75de78fcfc7bc57be Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 27 Jun 2023 19:41:50 +0200 Subject: [PATCH 017/325] Revert "update admin room welcome message" This reverts commit 823d28a2689971bcfd8fc7f59c1f0c925b09d186. --- src/languages/en.js | 4 ++-- src/libs/ReportUtils.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index 65d79033e173..17653692828e 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -285,8 +285,8 @@ export default { beginningOfArchivedRoomPartTwo: ", there's nothing to see here.", beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}) => `Collaboration with everyone at ${domainRoom} starts here! 🎉\nUse `, beginningOfChatHistoryDomainRoomPartTwo: ' to chat with colleagues, share tips, and ask questions.', - beginningOfChatHistoryAdminRoomPartOne: () => 'Use ', - beginningOfChatHistoryAdminRoomPartTwo: ({workspaceName}) => ` to hear about important announcements related to ${workspaceName}`, + beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}) => `Collaboration among ${workspaceName} admins starts here! 🎉\nUse `, + beginningOfChatHistoryAdminRoomPartTwo: ' to chat about topics such as workspace configurations and more.', beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}) => `Collaboration between all ${workspaceName} members starts here! 🎉\nUse `, beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}) => ` to chat about anything ${workspaceName} related.`, beginningOfChatHistoryUserRoomPartOne: 'Collaboration starts here! 🎉\nUse this space to chat about anything ', diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 87ed25866e46..4ce66e869b49 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -572,8 +572,8 @@ function getRoomWelcomeMessage(report) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report.reportName}); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartTwo'); } else if (isAdminRoom(report)) { - welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne'); - welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartTwo', {workspaceName}); + welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne', {workspaceName}); + welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartTwo'); } else if (isAnnounceRoom(report)) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartOne', {workspaceName}); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartTwo', {workspaceName}); From a93731147dd13f3f36ff1aacfb6e94208c8649ba Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 27 Jun 2023 19:56:02 +0200 Subject: [PATCH 018/325] update admin room welcome message --- src/languages/en.js | 2 ++ src/libs/ReportUtils.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/languages/en.js b/src/languages/en.js index 17653692828e..8a8cdbef8f50 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -287,6 +287,8 @@ export default { beginningOfChatHistoryDomainRoomPartTwo: ' to chat with colleagues, share tips, and ask questions.', beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}) => `Collaboration among ${workspaceName} admins starts here! 🎉\nUse `, beginningOfChatHistoryAdminRoomPartTwo: ' to chat about topics such as workspace configurations and more.', + beginningOfChatHistoryAdminOnlyPostingRoomPartOne: 'Use ', + beginningOfChatHistoryAdminOnlyPostingRoomPartTwo: ({workspaceName}) => ` to hear about important announcements related to ${workspaceName}`, beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}) => `Collaboration between all ${workspaceName} members starts here! 🎉\nUse `, beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}) => ` to chat about anything ${workspaceName} related.`, beginningOfChatHistoryUserRoomPartOne: 'Collaboration starts here! 🎉\nUse this space to chat about anything ', diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 4ce66e869b49..beb6db24a555 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -272,6 +272,18 @@ function isAdminRoom(report) { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS; } +/** + * + * @param {Object} report + * @param {String} report.writeCapability + * @returns {Boolean} + */ +function isAdminsOnlyPostingRoom(report) { + const writeCapability = lodashGet(report, 'writeCapability', CONST.REPORT.WRITE_CAPABILITIES.ALL); + + return writeCapability === CONST.REPORT.WRITE_CAPABILITIES.ADMINS; +} + /** * Whether the provided report is a Announce room * @param {Object} report @@ -574,6 +586,9 @@ function getRoomWelcomeMessage(report) { } else if (isAdminRoom(report)) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne', {workspaceName}); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartTwo'); + } else if (isAdminsOnlyPostingRoom(report)) { + welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminOnlyPostingRoomPartOne'); + welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminOnlyPostingRoomPartTwo', {workspaceName}); } else if (isAnnounceRoom(report)) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartOne', {workspaceName}); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartTwo', {workspaceName}); From 42e21d765026f98b6fd3efed508d5b3b138588c5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 27 Jun 2023 20:07:21 +0200 Subject: [PATCH 019/325] fix reports visibility --- src/libs/OptionsListUtils.js | 6 +++++- src/libs/ReportUtils.js | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index f4f48654e3b7..df24d9655de9 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -595,6 +595,7 @@ function getOptions( includeThreads = false, includeTasks = false, includeMoneyRequests = false, + isShareDestination = false, }, ) { if (!isPersonalDetailsReady(personalDetails)) { @@ -619,7 +620,9 @@ function getOptions( const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchInputValue; // Filter out all the reports that shouldn't be displayed - const filteredReports = _.filter(reports, (report) => ReportUtils.shouldReportBeInOptionList(report, Navigation.getReportIDFromRoute(), false, iouReports, betas, policies)); + const filteredReports = _.filter(reports, (report) => + ReportUtils.shouldReportBeInOptionList(report, Navigation.getReportIDFromRoute(), false, iouReports, betas, policies, isShareDestination), + ); // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) @@ -950,6 +953,7 @@ function getShareDestinationOptions(reports, personalDetails, betas = [], search includePersonalDetails: true, excludeLogins, includeOwnedWorkspaceChats, + isShareDestination: true, }); } diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index beb6db24a555..c9461763377e 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1914,9 +1914,10 @@ function canSeeDefaultRoom(report, policies, betas) { * @param {Object} iouReports * @param {String[]} betas * @param {Object} policies + * @param {Boolean} isShareDestination * @returns {boolean} */ -function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, iouReports, betas, policies) { +function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, iouReports, betas, policies, isShareDestination = false) { const isInDefaultMode = !isInGSDMode; // Exclude reports that have no data because there wouldn't be anything to show in the option item. @@ -1969,7 +1970,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, iouRep } // TODO: Add description - if (report.writeCapability === CONST.REPORT.WRITE_CAPABILITIES.ADMINS) { + if (isShareDestination && report.writeCapability === CONST.REPORT.WRITE_CAPABILITIES.ADMINS) { const linkedWorkspace = _.find(policies, (policy) => policy && policy.id === report.policyID); const shouldAllowShare = lodashGet(linkedWorkspace, 'role', '') === CONST.POLICY.ROLE.ADMIN; From 3cacb877f2836b6c6e61f35600d47e451413c715 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 27 Jun 2023 20:22:12 +0200 Subject: [PATCH 020/325] remove extra space for offline indicator --- src/pages/home/report/ReportFooter.js | 4 +--- src/styles/styles.js | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js index 92838ecf9451..6200da2f02bb 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.js @@ -68,9 +68,7 @@ function ReportFooter(props) { {(isArchivedRoom || hideComposer) && ( {isArchivedRoom && } - {!props.isSmallScreenWidth && ( - {hideComposer && } - )} + {!props.isSmallScreenWidth && } )} {!hideComposer && (props.shouldShowComposeInput || !props.isSmallScreenWidth) && ( diff --git a/src/styles/styles.js b/src/styles/styles.js index 74066af5e20a..4fb925308cad 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -803,10 +803,6 @@ const styles = { paddingBottom: 5, }, - offlineIndicatorRow: { - height: 25, - }, - // Actions actionAvatar: { borderRadius: 20, From 842bc5fbc29054dacd3297d3f9f150e1451a6432 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga Date: Tue, 27 Jun 2023 16:56:34 -0700 Subject: [PATCH 021/325] address PR reviews --- contributingGuides/TS_CHEATSHEET.md | 8 ++++---- contributingGuides/TS_STYLE.md | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/contributingGuides/TS_CHEATSHEET.md b/contributingGuides/TS_CHEATSHEET.md index c5a6723da459..cff733a4985d 100644 --- a/contributingGuides/TS_CHEATSHEET.md +++ b/contributingGuides/TS_CHEATSHEET.md @@ -90,7 +90,7 @@ - [1.4](#style-props) **Style Props** - When converting or typing style props, use `StyleProp` type where `T` is the type of styles related to the component your prop is going to apply. + Use `StyleProp` to type style props. For pass-through style props, use types exported from `react-native` for the type parameter (e.g. `ViewStyle`). ```tsx import { StyleProp, ViewStyle, TextStyle, ImageStyle } from "react-native"; @@ -118,7 +118,7 @@ children: (label: string) => React.ReactNode; }; - function ParentComponent({ children }: Props) { + function ParentComponent({ children }: ParentComponentProps) { return children("String being injected"); } @@ -177,13 +177,13 @@ } ``` - In the above code, `employee is Manager` is Type Predicate. It signifies that the return type of `isManager` is a `boolean`, indicating whether a value passed to the function is of a certain type (e.g. `Manager`). + In the above code, `employee is Manager` is a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates). It means that the return type of `isManager` is a `boolean` that indicates whether a value passed to the function is of a certain type (e.g. `Manager`). - [1.7](#try-catch-clauses) **Error in Try Catch Clauses** - Errors in try/catch clauses are typed as unknown, if the developer needs to use the error data they must conditionally check the type of the data first. Use type narrowing + Errors in try/catch clauses are inferred as `unknown`. If the error dat needs to be accessed, the type of the error needs to be checked and narrowed down. ```ts try { diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index 8fa8827e4762..619fa4a10bb3 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -132,7 +132,7 @@ This rule will apply until the migration is done. After the migration, exception -- [1.5](#convensions-unknown-vs-any) **`unknown` vs. `any`**: Don't use `any`. Use `unknown` if type is not known beforehand +- [1.5](#convensions-unknown-vs-any) **`unknown` vs. `any`**: Don't use `any`. Use `unknown` if type is not known beforehand. > Why? `any` type bypasses type checking. `unknown` is type safe as `unknown` type needs to be type narrowed before being used. @@ -145,7 +145,7 @@ This rule will apply until the migration is done. After the migration, exception -- [1.6](#convensions-array) **`T[]` vs. `Array`**: Use T[] or readonly T[] for simple types (i.e. types which are just primitive names or type references). Use Array or ReadonlyArray for all other types (union types, intersection types, object types, function types, etc). +- [1.6](#convensions-array) **`T[]` vs. `Array`**: Use `T[]` or `readonly T[]` for simple types (i.e. types which are just primitive names or type references). Use `Array` or `ReadonlyArray` for all other types (union types, intersection types, object types, function types, etc). ```ts // Array @@ -234,13 +234,18 @@ This rule will apply until the migration is done. After the migration, exception > Refer to [the propTypes Migration Table](./PROPTYPES_CONVERSION_TABLE.md) on how to type props based on existing `propTypes`. ```tsx - type GreetingProps = { - greeting: string; - name: string; + type MyComponentProps = { + requiredProp: string; + optionalPropWithDefaultValue?: number; + optionalProp?: boolean; }; - function Greeting({ greeting = "hello", name = "world" }: ComponentProps) { - {`${greeting}, ${name}`}; + function MyComponent({ + requiredProp, + optionalPropWithDefaultValue = 42, + optionalProp, + }: MyComponentProps) { + // component's code } ``` From d89fc4c60be97d18e588868341a2e8eb403f27d5 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga Date: Tue, 27 Jun 2023 21:21:12 -0700 Subject: [PATCH 022/325] add conversion example code --- .../PROPTYPES_CONVERSION_TABLE.md | 113 ++++++++++++++++++ contributingGuides/TS_CHEATSHEET.md | 4 +- contributingGuides/TS_STYLE.md | 63 ++++++---- 3 files changed, 153 insertions(+), 27 deletions(-) diff --git a/contributingGuides/PROPTYPES_CONVERSION_TABLE.md b/contributingGuides/PROPTYPES_CONVERSION_TABLE.md index 9149a6883151..c45e71218df1 100644 --- a/contributingGuides/PROPTYPES_CONVERSION_TABLE.md +++ b/contributingGuides/PROPTYPES_CONVERSION_TABLE.md @@ -1,5 +1,38 @@ # Expensify PropTypes Conversation Table +## Table of Contents + +- [Important Considerations](#important-considerations) + - [Don't Rely on `isRequired`](#dont-rely-on-isrequired) +- [PropTypes Conversion Table](#proptypes-conversion-table) +- [Conversion Example](#conversion-example) + +## Important Considerations + +### Don't Rely on `isRequired` + +Regardless of `isRequired` is present or not on props in `PropTypes`, read through the component implementation to check if props without `isRequired` can actually be optional. The use of `isRequired` is not consistent in the current codebase. Just because `isRequired` is not present, it does not necessarily mean that the prop is optional. + +One trick is to mark the prop in question with optional modifier `?`. See if the "possibly `undefined`" error is raised by TypeScript. If any error is raised, the implementation assumes the prop not to be optional. + +```ts +// Before +const propTypes = { + isVisible: PropTypes.bool.isRequired, + // `confirmText` prop is not marked as required here, theoretically it is optional. + confirmText: PropTypes.string, +}; + +// After +type Props = { + isVisible: boolean; + // Consider it as required unless you have proof that it is indeed an optional prop. + confirmText: string; // vs. confirmText?: string; +}; +``` + +## PropTypes Conversion Table + | PropTypes | TypeScript | Instructions | | -------------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PropTypes.any` | `T`, `Record` or `any` | Figure out what would be the correct data type and use it.

If you know that it's a object but isn't possible to determine the internal structure, use `Record`. | @@ -16,3 +49,83 @@ | `PropTypes.elementType` | `React.ElementType` | Convert to `React.ElementType`. | | `PropTypes.instanceOf(T)` | `T` | Convert to `T`. | | `PropTypes.oneOf([T, U, ...])` or `PropTypes.oneOfType([T, U, ...])` | `T \| U \| ...` | Convert to a union type e.g. `T \| U \| ...`. | + +## Conversion Example + +```ts +// Before +const propTypes = { + unknownData: PropTypes.any, + anotherUnknownData: PropTypes.any, + indexes: PropTypes.arrayOf(PropTypes.number), + items: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string, + label: PropTypes.string, + }) + ), + shouldShowIcon: PropTypes.bool, + onChangeText: PropTypes.func, + count: PropTypes.number, + session: PropTypes.shape({ + authToken: PropTypes.string, + accountID: PropTypes.number, + }), + errors: PropTypes.objectOf(PropTypes.string), + inputs: PropTypes.objectOf( + PropTypes.shape({ + id: PropTypes.string, + label: PropTypes.string, + }) + ), + label: PropTypes.string, + anchor: PropTypes.node, + footer: PropTypes.element, + uniqSymbol: PropTypes.symbol, + icon: PropTypes.elementType, + date: PropTypes.instanceOf(Date), + size: PropTypes.oneOf(["small", "medium", "large"]), +}; + +// After +type Item = { + value: string; + label: string; +}; + +type Session = { + authToken: string; + accountID: number; +}; + +type Input = { + id: string; + label: string; +}; + +type Size = "small" | "medium" | "large"; + +type Props = { + unknownData: string[]; + + // It's not possible to infer the data as it can be anything because of reasons X, Y and Z. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + anotherUnknownData: any; + + indexes: number[]; + items: Item[]; + shouldShowIcon: boolean; + onChangeText: (value: string) => void; + count: number; + session: Session; + errors: Record; + inputs: Record; + label: string; + anchor: React.ReactNode; + footer: React.ReactElement; + uniqSymbol: symbol; + icon: React.ElementType; + date: Date; + size: Size; +}; +``` diff --git a/contributingGuides/TS_CHEATSHEET.md b/contributingGuides/TS_CHEATSHEET.md index cff733a4985d..5322d250f730 100644 --- a/contributingGuides/TS_CHEATSHEET.md +++ b/contributingGuides/TS_CHEATSHEET.md @@ -8,7 +8,7 @@ - [1.4 Style Props](#style-props) - [1.5 Render Prop](#render-prop) - [1.6 Type Narrowing](#type-narrowing) -- [1.7 Errors in Type Catch Clauses](#try-catch-clauses) +- [1.7 Errors in Try-Catch Clauses](#try-catch-clauses) ## CheatSheet @@ -181,7 +181,7 @@ -- [1.7](#try-catch-clauses) **Error in Try Catch Clauses** +- [1.7](#try-catch-clauses) **Error in Try-Catch Clauses** Errors in try/catch clauses are inferred as `unknown`. If the error dat needs to be accessed, the type of the error needs to be checked and narrowed down. diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index 619fa4a10bb3..73f6ac1b9bb4 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -5,21 +5,22 @@ - [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript) - [Learning Resources](#learning-resources) - [Exception to Rules](#exception-to-rules) -- [1.1 Naming Conventions](#convension-naming-convension) -- [1.2 `d.ts` Extension](#convensions-d-ts-extension) -- [1.3 Type Alias vs. Interface](#convensions-type-alias-vs-interface) -- [1.4 Enum vs. Union Type](#convensions-enum-vs-union-type) -- [1.5 `unknown` vs. `any`](#convensions-unknown-vs-any) -- [1.6 `T[]` vs. `Array`](#convensions-array) -- [1.7 @ts-ignore](#convension-ts-ignore) -- [1.8 Optional chaining and nullish coalescing](#convension-ts-nullish-coalescing) -- [1.9 Type Inference](#convension-type-inference) -- [1.10 JSDoc](#conventions-jsdoc) -- [1.11 `propTypes` and `defaultProps`](#convension-proptypes-and-defaultprops) -- [1.12 Utility Types](#convension-utility-types) -- [1.13 `object` Type](#convension-object-type) -- [1.14 Export Prop Types](#convension-export-prop-types) -- [1.15 File Organization](#convension-file-organization) +- [Guidelines](#guidelines) + - [1.1 Naming Conventions](#convension-naming-convension) + - [1.2 `d.ts` Extension](#convensions-d-ts-extension) + - [1.3 Type Alias vs. Interface](#convensions-type-alias-vs-interface) + - [1.4 Enum vs. Union Type](#convensions-enum-vs-union-type) + - [1.5 `unknown` vs. `any`](#convensions-unknown-vs-any) + - [1.6 `T[]` vs. `Array`](#convensions-array) + - [1.7 @ts-ignore](#convension-ts-ignore) + - [1.8 Optional chaining and nullish coalescing](#convension-ts-nullish-coalescing) + - [1.9 Type Inference](#convension-type-inference) + - [1.10 JSDoc](#conventions-jsdoc) + - [1.11 `propTypes` and `defaultProps`](#convension-proptypes-and-defaultprops) + - [1.12 Utility Types](#convension-utility-types) + - [1.13 `object` Type](#convension-object-type) + - [1.14 Export Prop Types](#convension-export-prop-types) + - [1.15 File Organization](#convension-file-organization) - [Communication Items](#communication-items) - [Migration Guidelines](#migration-guidelines) @@ -321,34 +322,46 @@ This rule will apply until the migration is done. After the migration, exception > Why? To encourage consistent API across platform-specific implementations. + Utility module example + ```ts // types.ts type GreetingModule = { - sayHello: () => boolean; - sayGoodbye: () => boolean; + getHello: () => string; + getGoodbye: () => string; }; // index.native.ts import { GreetingModule } from "./types.ts"; - function sayHello() { - console.log("hello from mobile code"); + function getHello() { + return "hello from mobile code"; } - function sayGoodbye() { - console.log("goodbye from mobile code"); + function getGoodbye() { + return "goodbye from mobile code"; } const Greeting: GreetingModule = { - sayHello, - sayGoodbye, + getHello, + getGoodbye, }; export default Greeting; // index.ts import { GreetingModule } from "./types.ts"; - ... + function getHello() { + return "hello from other platform code"; + } + function getGoodbye() { + return "goodbye from other platform code"; + } const Greeting: GreetingModule = { - ... + getHello, + getGoodbye, + }; + export default Greeting; ``` + Component module example + ```ts // types.ts export type MyComponentProps = { From 42ef63272f034999ca906571617b7998531bfa33 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga Date: Wed, 28 Jun 2023 00:04:03 -0700 Subject: [PATCH 023/325] add more sections --- contributingGuides/TS_STYLE.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index 73f6ac1b9bb4..e31436f65357 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -5,6 +5,7 @@ - [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript) - [Learning Resources](#learning-resources) - [Exception to Rules](#exception-to-rules) +- [General Rules](#general-rules) - [Guidelines](#guidelines) - [1.1 Naming Conventions](#convension-naming-convension) - [1.2 `d.ts` Extension](#convensions-d-ts-extension) @@ -51,6 +52,17 @@ Most of the rules are enforced in ESLint or checked by TypeScript. If you think This rule will apply until the migration is done. After the migration, exceptions are assessed and granted by PR reviewers. +## General Rules + +Strive to type as strictly as possible. + +```ts +type Foo = { + fetchingStatus: "loading" | "success" | "error"; // vs. fetchingStatus: string; + person: { name: string; age: number }; // vs. person: Record; +}; +``` + ## Guidelines @@ -381,6 +393,18 @@ This rule will apply until the migration is done. After the migration, exception export default function MyComponent({ foo }: MyComponentProps) {...} ``` + + +- [1.16] **Reusable Types**: Reusable type definitions, such as models (e.g. Report), must have their own file and be placed under `src/types/`. The type should be exported as a default export. + + ```ts + // src/types/Report.ts + + type Report = {...}; + + export default Report; + ``` + ## Communication Items > Comment in the `#expensify-open-source` Slack channel if any of the following situations are encountered. Each comment should be prefixed with `TS ATTENTION:`. Internal engineers will access each situation and prescribe solutions to each case. Internal engineers should refer to general solutions to each situation that follows each list item. From 74cc95a8b735e31abc2920590320532da3dc1eaa Mon Sep 17 00:00:00 2001 From: Edu Date: Wed, 28 Jun 2023 13:15:55 +0200 Subject: [PATCH 024/325] handling navigation and state update --- src/ROUTES.js | 2 +- src/components/StatePicker.js | 22 ++++++----- src/hooks/useNavigationStorage.js | 37 +++++++++++++++++++ src/pages/ReimbursementAccount/AddressForm.js | 12 +++++- src/pages/ReimbursementAccount/CompanyStep.js | 9 ++++- .../ReimbursementAccountPage.js | 1 + src/pages/StateSelectorPage.js | 16 +++++--- .../Profile/PersonalDetails/AddressPage.js | 6 ++- .../PersonalDetailsInitialPage.js | 5 +++ 9 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 src/hooks/useNavigationStorage.js diff --git a/src/ROUTES.js b/src/ROUTES.js index f784c41db6d8..2ac62585d3eb 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -72,7 +72,7 @@ export default { SETTINGS_SELECT_COUNTRY: 'select-country', getCountrySelectionRoute: (countryISO, backTo) => `select-country?countryISO=${countryISO}&backTo=${backTo}`, SETTINGS_USA_STATES: 'select-usa-states', - getUsaStateSelectionRoute: (stateISO, backTo) => `select-usa-states?stateISO=${stateISO}&backTo=${backTo}`, + getUsaStateSelectionRoute: (stateISO, key, backTo) => `select-usa-states?stateISO=${stateISO}&key=${key}&backTo=${encodeURIComponent(backTo)}`, /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ CONCIERGE: 'concierge', diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js index 6e1d81e0afb7..2659eb1600d1 100644 --- a/src/components/StatePicker.js +++ b/src/components/StatePicker.js @@ -1,8 +1,8 @@ -import lodashGet from 'lodash/get'; import React, {useState, useEffect, useCallback, useMemo} from 'react'; import {View} from 'react-native'; import {useRoute} from '@react-navigation/native'; import PropTypes from 'prop-types'; +import useNavigationStorage from '../hooks/useNavigationStorage'; import compose from '../libs/compose'; import withNavigation from './withNavigation'; import sizes from '../styles/variables'; @@ -34,28 +34,30 @@ const defaultProps = { function BaseStatePicker(props) { const route = useRoute(); + const defaultValue = props.defaultValue; + const [collect] = useNavigationStorage(props.inputID, defaultValue); const stateISO = props.stateISO; - const [stateTitle, setStateTitle] = useState(stateISO); - const paramStateISO = lodashGet(route, 'params.stateISO'); - const navigation = props.navigation; + const paramStateISO = collect(); + const [stateTitle, setStateTitle] = useState(stateISO || paramStateISO); const onInputChange = props.onInputChange; - const defaultValue = props.defaultValue; const translate = props.translate; useEffect(() => { - if (!paramStateISO || paramStateISO === stateTitle) { + if (!paramStateISO) { return; } - setStateTitle(paramStateISO); // Needed to call onInputChange, so Form can update the validation and values onInputChange(paramStateISO); - }, [paramStateISO, stateTitle, onInputChange, navigation]); + // onInputChange isn't a stable function, so we can't add it to the dependency array + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [paramStateISO]); const navigateToCountrySelector = useCallback(() => { - Navigation.navigate(ROUTES.getUsaStateSelectionRoute(stateTitle || stateISO, Navigation.getActiveRoute())); - }, [stateTitle, stateISO]); + // Try first using the route.path so I can keep any query params + Navigation.navigate(ROUTES.getUsaStateSelectionRoute(stateTitle || stateISO, props.inputID, route.path || Navigation.getActiveRoute())); + }, [stateTitle, stateISO, route.path, props.inputID]); const title = useMemo(() => { const allStates = translate('allStates'); diff --git a/src/hooks/useNavigationStorage.js b/src/hooks/useNavigationStorage.js new file mode 100644 index 000000000000..8fbf46a6fbfc --- /dev/null +++ b/src/hooks/useNavigationStorage.js @@ -0,0 +1,37 @@ +import {useEffect} from 'react'; + +const storage = new Map(); + +/** + * Clears the navigation storage, call it before to navigate to a new page + */ +function clearNavigationStorage() { + storage.clear(); +} + +/** + * Saves a value into the navigation storage. Use it for class components + * @param {String} key + * @param {any} value + */ +function saveIntoStorage(key, value) { + storage.set(key, value); +} + +export default function useNavigationStorage(key = 'input', initialValue = null) { + useEffect(() => { + if (!initialValue || storage.has(key)) { + return; + } + storage.set(key, initialValue); + }, [key, initialValue]); + + const collect = () => storage.get(key); + const save = (value) => { + storage.set(key, value); + }; + + return [collect, save]; +} + +export {clearNavigationStorage, saveIntoStorage}; diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 39fa73b97076..845609d69d43 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -14,6 +14,12 @@ const propTypes = { /** Callback fired when a field changes. Passes args as {[fieldName]: val} */ onFieldChange: PropTypes.func, + /** Callback fired when a Address search changes the Country. */ + onCountryChange: PropTypes.func, + + /** Callback fired when a Address search changes the State. */ + onStateChange: PropTypes.func, + /** Default values */ defaultValues: PropTypes.shape({ /** Address street field */ @@ -89,6 +95,8 @@ const defaultProps = { }, shouldSaveDraft: false, onFieldChange: () => {}, + onCountryChange: () => {}, + onStateChange: () => {}, }; function AddressForm(props) { @@ -103,6 +111,8 @@ function AddressForm(props) { value={props.values.street} defaultValue={props.defaultValues.street} onInputChange={props.onFieldChange} + onCountryChange={props.onCountryChange} + onStateChange={props.onStateChange} errorText={props.errors.street ? props.translate('bankAccount.error.addressStreet') : ''} hint={props.translate('common.noPO')} renamedInputKeys={props.inputKeys} @@ -125,7 +135,7 @@ function AddressForm(props) { inputID={props.inputKeys.state} shouldSaveDraft={props.shouldSaveDraft} value={props.values.state} - defaultValue={props.defaultValues.state} + defaultValue={props.defaultValues.state || ''} onInputChange={(value) => props.onFieldChange({state: value})} errorText={props.errors.state ? props.translate('bankAccount.error.addressState') : ''} /> diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 65e7a1fda1d2..774a8144a366 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -7,6 +7,7 @@ import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import {parsePhoneNumber} from 'awesome-phonenumber'; import HeaderWithBackButton from '../../components/HeaderWithBackButton'; +import StatePicker from '../../components/StatePicker'; import CONST from '../../CONST'; import * as BankAccounts from '../../libs/actions/BankAccounts'; import Text from '../../components/Text'; @@ -15,7 +16,6 @@ import TextInput from '../../components/TextInput'; import styles from '../../styles/styles'; import CheckboxWithLabel from '../../components/CheckboxWithLabel'; import TextLink from '../../components/TextLink'; -import StatePicker from '../../components/StatePicker'; import withLocalize from '../../components/withLocalize'; import * as ValidationUtils from '../../libs/ValidationUtils'; import compose from '../../libs/compose'; @@ -25,6 +25,7 @@ import AddressForm from './AddressForm'; import Form from '../../components/Form'; import ScreenWrapper from '../../components/ScreenWrapper'; import StepPropTypes from './StepPropTypes'; +import * as NavigationStorage from '../../hooks/useNavigationStorage'; const propTypes = { ...StepPropTypes, @@ -54,6 +55,7 @@ class CompanyStep extends React.Component { super(props); this.submit = this.submit.bind(this); + this.onCountryStateUpdate = this.onCountryStateUpdate.bind(this); this.validate = this.validate.bind(this); this.defaultWebsite = lodashGet(props, 'user.isFromPublicDomain', false) ? 'https://' : `https://www.${Str.extractEmailDomain(props.session.email, '')}`; @@ -63,6 +65,10 @@ class CompanyStep extends React.Component { BankAccounts.resetReimbursementAccount(); } + onCountryStateUpdate(selectedCountryState) { + NavigationStorage.saveIntoStorage('addressState', selectedCountryState); + } + /** * @param {Array} fieldNames * @@ -197,6 +203,7 @@ class CompanyStep extends React.Component { state: 'addressState', zipCode: 'addressZipCode', }} + onStateChange={this.onCountryStateUpdate} shouldSaveDraft streetTranslationKey="common.companyAddress" /> diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js index d12231e85370..a3d567bf82a3 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js @@ -418,6 +418,7 @@ class ReimbursementAccountPage extends React.Component { reimbursementAccountDraft={this.props.reimbursementAccountDraft} onBackButtonPress={this.goBack} getDefaultStateForField={this.getDefaultStateForField} + navigation={this.props.navigation} /> ); } diff --git a/src/pages/StateSelectorPage.js b/src/pages/StateSelectorPage.js index 00dcb445899c..994493c9f5a2 100644 --- a/src/pages/StateSelectorPage.js +++ b/src/pages/StateSelectorPage.js @@ -1,9 +1,8 @@ import _ from 'underscore'; -import lodashGet from 'lodash/get'; import React, {useCallback, useMemo} from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; -import ROUTES from '../ROUTES'; +import useNavigationStorage from '../hooks/useNavigationStorage'; import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; import Navigation from '../libs/Navigation/Navigation'; import compose from '../libs/compose'; @@ -39,7 +38,8 @@ const defaultProps = { function StateSelectorPage(props) { const translate = props.translate; const route = props.route; - const currentCountryState = !_.isEmpty(route.params) ? route.params.stateISO : lodashGet(props.privatePersonalDetails, 'address.state'); + const [collect, dispatch] = useNavigationStorage(route.params.key, null, 'page selector'); + const currentCountryState = collect(); const allStates = translate('allStates'); const selectedSearchState = !_.isEmpty(currentCountryState) ? allStates[currentCountryState].stateName : ''; @@ -54,9 +54,13 @@ function StateSelectorPage(props) { [translate, currentCountryState], ); - const updateCountryState = useCallback((selectedState) => { - Navigation.goBack(`${ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS}?stateISO=${selectedState.value}`, true); - }, []); + const updateCountryState = useCallback( + (selectedState) => { + dispatch(selectedState.value); + Navigation.goBack(`${decodeURIComponent(route.params.backTo)}`, true); + }, + [dispatch, route.params.backTo], + ); return ( { - navigation.setParams({stateISO: selectedCountryState}); + saveIntoStorage(selectedCountryState); }; useEffect(() => { @@ -83,6 +85,7 @@ function AddressPage({translate, route, navigation, privatePersonalDetails}) { setCountryISO(currentCountryISO); } }, [route, navigation]); + /** * @param {Function} translate - translate function * @param {Boolean} isUSAForm - selected country ISO code is US @@ -92,7 +95,6 @@ function AddressPage({translate, route, navigation, privatePersonalDetails}) { const validate = useCallback( (values) => { const errors = {}; - const requiredFields = ['addressLine1', 'city', 'country', 'state']; // Check "State" dropdown is a valid state if selected Country is USA. diff --git a/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js b/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js index a466d2091de1..2436b51ba073 100644 --- a/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js +++ b/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js @@ -14,6 +14,7 @@ import MenuItemWithTopDescription from '../../../../components/MenuItemWithTopDe import * as PersonalDetails from '../../../../libs/actions/PersonalDetails'; import ONYXKEYS from '../../../../ONYXKEYS'; import {withNetwork} from '../../../../components/OnyxProvider'; +import * as NavigationStorage from '../../../../hooks/useNavigationStorage'; const propTypes = { /* Onyx Props */ @@ -61,6 +62,10 @@ function PersonalDetailsInitialPage(props) { PersonalDetails.openPersonalDetailsPage(); }, [props.network.isOffline]); + useEffect(() => { + NavigationStorage.clearNavigationStorage(); + }, []); + const privateDetails = props.privatePersonalDetails || {}; const address = privateDetails.address || {}; const legalName = `${privateDetails.legalFirstName || ''} ${privateDetails.legalLastName || ''}`.trim(); From f01b1e07dbf831d7d9af850bfc7f60cfa485c878 Mon Sep 17 00:00:00 2001 From: Edu Date: Wed, 28 Jun 2023 13:45:44 +0200 Subject: [PATCH 025/325] improved navigation animation --- src/components/OptionsSelectorWithSearch.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/OptionsSelectorWithSearch.js b/src/components/OptionsSelectorWithSearch.js index 945037cd140e..2c5fe0fa660f 100644 --- a/src/components/OptionsSelectorWithSearch.js +++ b/src/components/OptionsSelectorWithSearch.js @@ -92,6 +92,7 @@ function OptionsSelectorWithSearch(props) { safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} shouldFocusOnSelectRow shouldHaveOptionSeparator + shouldDelayFocus initiallyFocusedOptionKey={initialOption} /> From fc6d0245c1784e24c8f704adbb1099ad2a3af0ff Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 28 Jun 2023 14:16:54 +0200 Subject: [PATCH 026/325] omit padding bottom of ReportActionItemCreated --- src/pages/home/report/ReportActionItemCreated.js | 7 ++++++- src/styles/utilities/spacing.js | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.js index e90e793dcfcb..5ff0a3dade97 100644 --- a/src/pages/home/report/ReportActionItemCreated.js +++ b/src/pages/home/report/ReportActionItemCreated.js @@ -53,6 +53,11 @@ function ReportActionItemCreated(props) { const icons = ReportUtils.getIcons(props.report, props.personalDetails); + const errors = lodashGet(props.report, 'errorFields.addWorkspaceRoom') || lodashGet(props.report, 'errorFields.createChat'); + const isArchivedRoom = ReportUtils.isArchivedRoom(props.report); + const hideComposer = ReportUtils.shouldHideComposer(props.report, errors); + const shouldOmitBottomSpace = props.report.lastMessageText === '' && (isArchivedRoom || hideComposer); + return ( ReportUtils.navigateToDetailsPage(props.report)} diff --git a/src/styles/utilities/spacing.js b/src/styles/utilities/spacing.js index e6823e43e921..79b0a10dde63 100644 --- a/src/styles/utilities/spacing.js +++ b/src/styles/utilities/spacing.js @@ -426,6 +426,10 @@ export default { paddingTop: 80, }, + pb0: { + paddingBottom: 0, + }, + pb1: { paddingBottom: 4, }, From f9a0756aaa75248bc5bd7a4d4b10e8ef605a7524 Mon Sep 17 00:00:00 2001 From: Nathalie Kuoch Date: Thu, 29 Jun 2023 16:21:21 +0200 Subject: [PATCH 027/325] New copies --- src/languages/en.js | 2 +- src/languages/es.js | 2 +- src/pages/home/report/ReportActionItemReimbursed.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index 9973b362c100..51be8ee2d454 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -362,7 +362,7 @@ export default { payerPaidAmount: ({payer, amount}) => `${payer} paid ${amount}`, payerSettled: ({amount}) => `paid ${amount}`, waitingOnBankAccount: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a `, - settledAfterAddedBankAccount: ({submitterDisplayName}) => `Payment settled after ${submitterDisplayName} added a bank account`, + settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`, noReimbursableExpenses: 'This report has an invalid amount', pendingConversionMessage: "Total will update when you're back online", threadRequestReportName: ({formattedAmount, comment}) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, diff --git a/src/languages/es.js b/src/languages/es.js index f10af2f308e4..44139591a34c 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -361,7 +361,7 @@ export default { payerPaidAmount: ({payer, amount}) => `${payer} pagó ${amount}`, payerSettled: ({amount}) => `pagó ${amount}`, waitingOnBankAccount: ({submitterDisplayName}) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} agregue una `, - settledAfterAddedBankAccount: ({submitterDisplayName}) => `Pago realizado cuando ${submitterDisplayName} agregó su cuenta bancaria`, + settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} agregó una cuenta bancaria. El pago de ${amount} se ha realizado.`, noReimbursableExpenses: 'El monto de este informe es inválido', pendingConversionMessage: 'El total se actualizará cuando estés online', threadRequestReportName: ({formattedAmount, comment}) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, diff --git a/src/pages/home/report/ReportActionItemReimbursed.js b/src/pages/home/report/ReportActionItemReimbursed.js index 127a0ccf38ee..7a6d0085eeaf 100644 --- a/src/pages/home/report/ReportActionItemReimbursed.js +++ b/src/pages/home/report/ReportActionItemReimbursed.js @@ -15,7 +15,7 @@ const propTypes = { function ReportActionItemReimbursed(props) { return ( - {props.isFromSubmitterAddingBankAccount ? props.translate('iou.settledAfterAddedBankAccount', {submitterDisplayName: props.submitterDisplayName}) : props.translate('iou.payerSettled', {amount: props.amount})} + {props.isFromSubmitterAddingBankAccount ? props.translate('iou.settledAfterAddedBankAccount', {submitterDisplayName: props.submitterDisplayName, amount: props.amount}) : props.translate('iou.payerSettled', {amount: props.amount})} ); } From e40d85c2d468386b5f206257c5f6005c03b703c3 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 30 Jun 2023 14:35:03 +0200 Subject: [PATCH 028/325] Apply destructuring assignment of values of validate method --- src/pages/workspace/WorkspaceNewRoomPage.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index acceb0cb7d17..74e80a7c2202 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -101,24 +101,24 @@ class WorkspaceNewRoomPage extends React.Component { * @param {Object} values - form input values passed by the Form component * @returns {Boolean} */ - validate(values) { + validate({policyID, roomName}) { const errors = {}; - if (!values.roomName || values.roomName === CONST.POLICY.ROOM_PREFIX) { + if (!roomName || roomName === CONST.POLICY.ROOM_PREFIX) { // We error if the user doesn't enter a room name or left blank ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.pleaseEnterRoomName'); - } else if (values.roomName !== CONST.POLICY.ROOM_PREFIX && !ValidationUtils.isValidRoomName(values.roomName)) { + } else if (roomName !== CONST.POLICY.ROOM_PREFIX && !ValidationUtils.isValidRoomName(roomName)) { // We error if the room name has invalid characters ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomNameInvalidError'); - } else if (ValidationUtils.isReservedRoomName(values.roomName)) { + } else if (ValidationUtils.isReservedRoomName(roomName)) { // Certain names are reserved for default rooms and should not be used for policy rooms. - ErrorUtils.addErrorMessage(errors, 'roomName', ['newRoomPage.roomNameReservedError', {reservedName: values.roomName}]); - } else if (ValidationUtils.isExistingRoomName(values.roomName, this.props.reports, values.policyID)) { + ErrorUtils.addErrorMessage(errors, 'roomName', ['newRoomPage.roomNameReservedError', {reservedName: roomName}]); + } else if (ValidationUtils.isExistingRoomName(roomName, this.props.reports, policyID)) { // Certain names are reserved for default rooms and should not be used for policy rooms. ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomAlreadyExistsError'); } - if (!values.policyID) { + if (!policyID) { errors.policyID = 'newRoomPage.pleaseSelectWorkspace'; } From e16e167d578fae04c278f83072c0a2c6179220bb Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 30 Jun 2023 14:35:25 +0200 Subject: [PATCH 029/325] Add JSDoc --- src/libs/ReportUtils.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index af6ea8adf0e3..252469eb02d1 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -275,7 +275,7 @@ function isAdminRoom(report) { } /** - * + * Whether the provided report is an Admin-only posting room * @param {Object} report * @param {String} report.writeCapability * @returns {Boolean} @@ -2023,12 +2023,12 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, iouRep return false; } - // TODO: Add description + // Exclude reports that are admin-only posting rooms, when the user tries to share something to the room, + // because, the user isn't an admin for a linked workspace. if (isShareDestination && report.writeCapability === CONST.REPORT.WRITE_CAPABILITIES.ADMINS) { const linkedWorkspace = _.find(policies, (policy) => policy && policy.id === report.policyID); - const shouldAllowShare = lodashGet(linkedWorkspace, 'role', '') === CONST.POLICY.ROLE.ADMIN; - return shouldAllowShare; + return lodashGet(linkedWorkspace, 'role', '') === CONST.POLICY.ROLE.ADMIN; } // Hide thread reports that haven't been commented on From 35807e85c443f440191e599b295f4f7a58120dd5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 30 Jun 2023 15:11:53 +0200 Subject: [PATCH 030/325] Add dummy translation --- src/languages/es.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/languages/es.js b/src/languages/es.js index 0c605be9c174..39b3fae8e6c7 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -285,6 +285,8 @@ export default { beginningOfChatHistoryDomainRoomPartTwo: ' para chatear con compañeros, compartir consejos o hacer una pregunta.', beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}) => `Este es el lugar para que los administradores de ${workspaceName} colaboren! 🎉\nUsa `, beginningOfChatHistoryAdminRoomPartTwo: ' para chatear sobre temas como la configuración del espacio de trabajo y mas.', + beginningOfChatHistoryAdminOnlyPostingRoomPartOne: 'Usar ', + beginningOfChatHistoryAdminOnlyPostingRoomPartTwo: ({workspaceName}) => ` para enterarse de anuncios importantes relacionados con ${workspaceName}`, beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}) => `Este es el lugar para que todos los miembros de ${workspaceName} colaboren! 🎉\nUsa `, beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}) => ` para chatear sobre cualquier cosa relacionada con ${workspaceName}.`, beginningOfChatHistoryUserRoomPartOne: 'Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ', From be5f7cc1c0ddc7b4a2ef43ab2d06445e6a67fb64 Mon Sep 17 00:00:00 2001 From: Nathalie Kuoch Date: Fri, 30 Jun 2023 15:35:16 +0200 Subject: [PATCH 031/325] js style --- src/pages/home/report/ReportActionItemReimbursed.js | 4 +++- .../home/report/ReportActionItemReimbursementQueued.js | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pages/home/report/ReportActionItemReimbursed.js b/src/pages/home/report/ReportActionItemReimbursed.js index 7a6d0085eeaf..2ef46852ca2f 100644 --- a/src/pages/home/report/ReportActionItemReimbursed.js +++ b/src/pages/home/report/ReportActionItemReimbursed.js @@ -15,7 +15,9 @@ const propTypes = { function ReportActionItemReimbursed(props) { return ( - {props.isFromSubmitterAddingBankAccount ? props.translate('iou.settledAfterAddedBankAccount', {submitterDisplayName: props.submitterDisplayName, amount: props.amount}) : props.translate('iou.payerSettled', {amount: props.amount})} + {props.isFromSubmitterAddingBankAccount + ? props.translate('iou.settledAfterAddedBankAccount', {submitterDisplayName: props.submitterDisplayName, amount: props.amount}) + : props.translate('iou.payerSettled', {amount: props.amount})} ); } diff --git a/src/pages/home/report/ReportActionItemReimbursementQueued.js b/src/pages/home/report/ReportActionItemReimbursementQueued.js index 7a0ea7c834f7..7cb23777e030 100644 --- a/src/pages/home/report/ReportActionItemReimbursementQueued.js +++ b/src/pages/home/report/ReportActionItemReimbursementQueued.js @@ -27,11 +27,7 @@ function ReportActionItemReimbursementQueued(props) { ); } - return ( - - {props.translate('iou.waitingOnBankAccount', {submitterDisplayName: props.submitterDisplayName})} - - ); + return {props.translate('iou.waitingOnBankAccount', {submitterDisplayName: props.submitterDisplayName})}; } ReportActionItemReimbursementQueued.propTypes = propTypes; From e526c7ee6c5f373550efe83c6244855ebff7b78b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Sat, 1 Jul 2023 12:48:01 +0200 Subject: [PATCH 032/325] Add real translations --- src/languages/es.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.js b/src/languages/es.js index 39b3fae8e6c7..8df91024663f 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -285,8 +285,8 @@ export default { beginningOfChatHistoryDomainRoomPartTwo: ' para chatear con compañeros, compartir consejos o hacer una pregunta.', beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}) => `Este es el lugar para que los administradores de ${workspaceName} colaboren! 🎉\nUsa `, beginningOfChatHistoryAdminRoomPartTwo: ' para chatear sobre temas como la configuración del espacio de trabajo y mas.', - beginningOfChatHistoryAdminOnlyPostingRoomPartOne: 'Usar ', - beginningOfChatHistoryAdminOnlyPostingRoomPartTwo: ({workspaceName}) => ` para enterarse de anuncios importantes relacionados con ${workspaceName}`, + beginningOfChatHistoryAdminOnlyPostingRoomPartOne: 'Utiliza ', + beginningOfChatHistoryAdminOnlyPostingRoomPartTwo: ({workspaceName}) => ` para enterarte de anuncios importantes relacionados con ${workspaceName}`, beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}) => `Este es el lugar para que todos los miembros de ${workspaceName} colaboren! 🎉\nUsa `, beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}) => ` para chatear sobre cualquier cosa relacionada con ${workspaceName}.`, beginningOfChatHistoryUserRoomPartOne: 'Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ', From de56cb2031a9762ad99e33737308fd261f9c7956 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga Date: Sun, 2 Jul 2023 23:57:02 -0700 Subject: [PATCH 033/325] address PR reviews --- contributingGuides/TS_CHEATSHEET.md | 18 ++++ contributingGuides/TS_STYLE.md | 126 +++++++++++++++------------- 2 files changed, 85 insertions(+), 59 deletions(-) diff --git a/contributingGuides/TS_CHEATSHEET.md b/contributingGuides/TS_CHEATSHEET.md index 5322d250f730..fa21abf140b6 100644 --- a/contributingGuides/TS_CHEATSHEET.md +++ b/contributingGuides/TS_CHEATSHEET.md @@ -9,6 +9,7 @@ - [1.5 Render Prop](#render-prop) - [1.6 Type Narrowing](#type-narrowing) - [1.7 Errors in Try-Catch Clauses](#try-catch-clauses) +- [1.8 Const Assertion](#const-assertion) ## CheatSheet @@ -195,3 +196,20 @@ } } ``` + + + +- [1.8](#const-assersion) **Use const assertions for rigorous typing** + + Use `as const` when you want to ensure that the types and values are as exact as possible and prevent unwanted mutations. + + ```ts + const greeting1 = "hello"; // type: string + const greeting2 = "goodbye" as const; // type: "goodbye" + + const person1 = { name: "Alice", age: 20 }; // type: { name: string, age: number } + const person2 = { name: "Bob", age: 30 } as const; // type: { readonly name: "Bob", readonly age; 30 } + + const array1 = ["hello", 1]; // type: (string | number)[] + const array2 = ["goodbye", 2]; // type: readonly ["goodbye", 2] + ``` diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index e31436f65357..c113cf44213b 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -7,22 +7,23 @@ - [Exception to Rules](#exception-to-rules) - [General Rules](#general-rules) - [Guidelines](#guidelines) - - [1.1 Naming Conventions](#convension-naming-convension) - - [1.2 `d.ts` Extension](#convensions-d-ts-extension) - - [1.3 Type Alias vs. Interface](#convensions-type-alias-vs-interface) - - [1.4 Enum vs. Union Type](#convensions-enum-vs-union-type) - - [1.5 `unknown` vs. `any`](#convensions-unknown-vs-any) - - [1.6 `T[]` vs. `Array`](#convensions-array) - - [1.7 @ts-ignore](#convension-ts-ignore) - - [1.8 Optional chaining and nullish coalescing](#convension-ts-nullish-coalescing) - - [1.9 Type Inference](#convension-type-inference) - - [1.10 JSDoc](#conventions-jsdoc) - - [1.11 `propTypes` and `defaultProps`](#convension-proptypes-and-defaultprops) - - [1.12 Utility Types](#convension-utility-types) - - [1.13 `object` Type](#convension-object-type) - - [1.14 Export Prop Types](#convension-export-prop-types) - - [1.15 File Organization](#convension-file-organization) -- [Communication Items](#communication-items) + - [1.1 Naming Conventions](#naming-conventions) + - [1.2 `d.ts` Extension](#d-ts-extension) + - [1.3 Type Alias vs. Interface](#type-alias-vs-interface) + - [1.4 Enum vs. Union Type](#enum-vs-union-type) + - [1.5 `unknown` vs. `any`](#unknown-vs-any) + - [1.6 `T[]` vs. `Array`](#array) + - [1.7 @ts-ignore](#ts-ignore) + - [1.8 Optional chaining and nullish coalescing](#ts-nullish-coalescing) + - [1.9 Type Inference](#type-inference) + - [1.10 JSDoc](#jsdoc) + - [1.11 `propTypes` and `defaultProps`](#proptypes-and-defaultprops) + - [1.12 Utility Types](#utility-types) + - [1.13 `object` Type](#object-type) + - [1.14 Export Prop Types](#export-prop-types) + - [1.15 File Organization](#file-organization) + - [1.16 Reusable Types](#reusable-types) +- [Communication Items](#items) - [Migration Guidelines](#migration-guidelines) ## Other Expensify Resources on TypeScript @@ -65,33 +66,35 @@ type Foo = { ## Guidelines - + -- [1.1](#convension-naming-convension) **Naming Conventions**: Use PascalCase for type names. Do not postfix type aliases with `Type` +- [1.1](#naming-conventions) **Naming Conventions**: Use PascalCase for type names. Do not postfix type aliases with `Type`. Use singular name for union types. eslint: [`@typescript-eslint/naming-convention`](https://typescript-eslint.io/rules/naming-convention/) ```ts // bad type foo = ...; type BAR = ...; type PersonType = ...; + type Colors = 'red' | 'blue' | 'green'; // good type Foo = ...; type Bar = ...; type Person = ...; + type Color = 'red' | 'blue' | 'green'; ``` - + -- [1.2](#convensions-d-ts-extension) **`d.ts` Extension**: Do not use `d.ts` file extension even when a file contains only type declarations. +- [1.2](#d-ts-extension) **`d.ts` Extension**: Do not use `d.ts` file extension even when a file contains only type declarations. > Why? Type errors in `d.ts` files are not checked by TypeScript [^1]. [^1]: This is because `skipLibCheck` TypeScript configuration is set to `true` in this project. - + -- [1.3](#convensions-type-alias-vs-interface) **Type Alias vs. Interface**: Do not use `interface`. Use `type`. +- [1.3](#type-alias-vs-interface) **Type Alias vs. Interface**: Do not use `interface`. Use `type`. eslint: [`@typescript-eslint/consistent-type-definitions`](https://typescript-eslint.io/rules/consistent-type-definitions/) > Why? In TypeScript, `type` and `interface` can be used interchangeably to declare types. Use `type` for consistency. @@ -107,9 +110,9 @@ type Foo = { }; ``` - + -- [1.4](#convensions-enum-vs-union-type) **Enum vs. Union Type**: Do not use `enum`. Use union types. +- [1.4](#enum-vs-union-type) **Enum vs. Union Type**: Do not use `enum`. Use union types. eslint: [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax) > Why? Enums come with several [pitfalls](https://blog.logrocket.com/why-typescript-enums-suck/). Most enum use cases can be replaced with union types. @@ -143,9 +146,9 @@ type Foo = { printColor(COLORS.Red); ``` - + -- [1.5](#convensions-unknown-vs-any) **`unknown` vs. `any`**: Don't use `any`. Use `unknown` if type is not known beforehand. +- [1.5](#unknown-vs-any) **`unknown` vs. `any`**: Don't use `any`. Use `unknown` if type is not known beforehand. eslint: [`@typescript-eslint/no-explicit-any`](https://typescript-eslint.io/rules/no-explicit-any/) > Why? `any` type bypasses type checking. `unknown` is type safe as `unknown` type needs to be type narrowed before being used. @@ -156,9 +159,9 @@ type Foo = { ... ``` - + -- [1.6](#convensions-array) **`T[]` vs. `Array`**: Use `T[]` or `readonly T[]` for simple types (i.e. types which are just primitive names or type references). Use `Array` or `ReadonlyArray` for all other types (union types, intersection types, object types, function types, etc). +- [1.6](#array) **`T[]` vs. `Array`**: Use `T[]` or `readonly T[]` for simple types (i.e. types which are just primitive names or type references). Use `Array` or `ReadonlyArray` for all other types (union types, intersection types, object types, function types, etc). eslint: [`@typescript-eslint/array-type`](https://typescript-eslint.io/rules/array-type/) ```ts // Array @@ -172,26 +175,28 @@ type Foo = { const f: readonly string[] = ["a", "b"]; ``` - + -- [1.7](#convension-ts-ignore) **@ts-ignore**: Do not use `@ts-ignore` or its variant `@ts-nocheck` to suppress warnings and errors. Use `@ts-expect-error` during the migration for type errors that should be handled later. +- [1.7](#ts-ignore) **@ts-ignore**: Do not use `@ts-ignore` or its variant `@ts-nocheck` to suppress warnings and errors. - + > Use `@ts-expect-error` during the migration for type errors that should be handled later. Refer to the [Migration Guidelines](#migration-guidelines) for specific instructions on how to deal with type errors during the migration. eslint: [`@typescript-eslint/ban-ts-comment`](https://typescript-eslint.io/rules/ban-ts-comment/) -- [1.8](#convension-ts-nullish-coalescing) **Optional chaining and nullish coalescing**: Use optional chaining and nullish coalescing instead of the `get` lodash function. + + +- [1.8](#ts-nullish-coalescing) **Optional chaining and nullish coalescing**: Use optional chaining and nullish coalescing instead of the `get` lodash function. eslint: [`no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports) ```ts // Bad - import { get } from "lodash"; + import lodashGet from "lodash/get"; const name = lodashGet(user, "name", "default name"); // Good const name = user?.name ?? "default name"; ``` - + -- [1.9](#convension-type-inference) **Type Inference**: When possible, allow the compiler to infer type of variables. +- [1.9](#type-inference) **Type Inference**: When possible, allow the compiler to infer type of variables. ```ts // Bad @@ -219,9 +224,9 @@ type Foo = { } ``` - + -- [1.10](#conventions-jsdoc) **JSDoc**: Omit comments that are redundant with TypeScript. Do not declare types in `@param` or `@return` blocks. Do not write `@implements`, `@enum`, `@private`, `@override` +- [1.10](#jsdoc) **JSDoc**: Omit comments that are redundant with TypeScript. Do not declare types in `@param` or `@return` blocks. Do not write `@implements`, `@enum`, `@private`, `@override`. eslint: [`jsdoc/no-types`](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.README/rules/no-types.md) ```ts // bad @@ -240,9 +245,9 @@ type Foo = { */ ``` - + -- [1.11](#convension-proptypes-and-defaultprops) **`propTypes` and `defaultProps`**: Do not use them. Use object destructing to assign default values if necessary. +- [1.11](#proptypes-and-defaultprops) **`propTypes` and `defaultProps`**: Do not use them. Use object destructing to assign default values if necessary. > Refer to [the propTypes Migration Table](./PROPTYPES_CONVERSION_TABLE.md) on how to type props based on existing `propTypes`. @@ -262,9 +267,9 @@ type Foo = { } ``` - + -- [1.12](#convension-utility-types) **Utility Types**: Use types from [TypeScript utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) and [`type-fest`](https://github.com/sindresorhus/type-fest) when possible. +- [1.12](#utility-types) **Utility Types**: Use types from [TypeScript utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) and [`type-fest`](https://github.com/sindresorhus/type-fest) when possible. ```ts type Foo = { @@ -280,9 +285,9 @@ type Foo = { }; ``` - + -- [1.13](#convension-object-type) **`object`**: Don't use `object` type. +- [1.13](#object-type) **`object`**: Don't use `object` type. eslint: [`@typescript-eslint/ban-types`](https://typescript-eslint.io/rules/ban-types/) > Why? `object` refers to "any non-primitive type," not "any object". Typing "any non-primitive value" is not commonly needed. @@ -293,7 +298,7 @@ type Foo = { If you know that the type of data is an object but don't know what properties or values it has beforehand, use `Record`. - > Even though `string` is specified as a key, `Record` type can still accepts objects whose keys are numbers or symbols. This is because number and + > Even though `string` is specified as a key, `Record` type can still accepts objects whose keys are numbers. This is because numbers are converted to strings when used as an object index. Note that you cannot use [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) for `Record`. ```ts function logObject(object: Record) { @@ -303,9 +308,9 @@ type Foo = { } ``` - + -- [1.14](#convension-export-prop-types) **Prop Types**: Define and export prop types for components. Use exported prop types instead of grabbing the prop type from a component. +- [1.14](#export-prop-types) **Prop Types**: Define and export prop types for components. Use exported prop types instead of grabbing the prop type from a component. > Why? Exporting prop types aids reusability. @@ -328,11 +333,11 @@ type Foo = { import MyComponent, { MyComponentProps } from "./MyComponent"; ``` - + -- [1.15](#convension-file-organization) **File organization**: In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files. +- [1.15](#file-organization) **File organization**: In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files. - > Why? To encourage consistent API across platform-specific implementations. + > Why? To encourage consistent API across platform-specific implementations. If you're migrating module that doesn't have a default implement (i.e. `index.ts`, e.g. `getPlatform`), refer to [Migration Guidelines](#migration-guidelines) for further information. Utility module example @@ -395,15 +400,15 @@ type Foo = { -- [1.16] **Reusable Types**: Reusable type definitions, such as models (e.g. Report), must have their own file and be placed under `src/types/`. The type should be exported as a default export. +- [1.16](#reusable-types) **Reusable Types**: Reusable type definitions, such as models (e.g. Report), must have their own file and be placed under `src/types/`. The type should be exported as a default export. - ```ts - // src/types/Report.ts +```ts +// src/types/Report.ts - type Report = {...}; +type Report = {...}; - export default Report; - ``` +export default Report; +``` ## Communication Items @@ -426,18 +431,21 @@ declare module "external-library-name" { > This section contains instructions that are applicable during the migration. +- If you're migrating a module that doesn't have a default implementation (i.e. `index.ts`, e.g. `getPlatform`), convert `index.website.js` to `index.ts`. Without `index.ts`, TypeScript cannot get type information where the module is imported. + +- Deprecate the usage of `underscore`. Use corresponding methods from `lodash`. eslint: [`no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports) + - Found type bugs. Now what? If TypeScript migration uncovers a bug that has been “invisible,” there are two options an author of a migration PR can take - Fix issues if they are minor. Document each fix in the PR comment - - Suppress a TypeScript error stemming from the bug with `@ts-expect-error`. Create a separate GH issue. Prefix the issue title with `[TS ERROR #]`. Cross-link the migration PR and the created GH issue. On the line below `@ts-expect-error`, put down the GH issue number prefixed with `TODO:`. + - Suppress a TypeScript error stemming from the bug with `@ts-expect-error`. Create a separate GH issue. Prefix the issue title with `[TS ERROR #]`. Cross-link the migration PR and the created GH issue. On the same line as `@ts-expect-error`, put down the GH issue number prefixed with `TODO:`. - The `@ts-expect-error` annotation tells the TS compiler to ignore any errors in the line that follows it. However, if there's no error in the line, TypeScript will also raise an error. + > The `@ts-expect-error` annotation tells the TS compiler to ignore any errors in the line that follows it. However, if there's no error in the line, TypeScript will also raise an error. ```ts - // @ts-expect-error - // TODO: #21647 + // @ts-expect-error TODO: #21647 const x: number = "123"; // No TS error raised // @ts-expect-error From 0a0bf3eaf8840821779eafdae39f694c49eb271a Mon Sep 17 00:00:00 2001 From: Hayata Suenaga Date: Mon, 3 Jul 2023 00:08:02 -0700 Subject: [PATCH 034/325] specific path --- contributingGuides/TS_STYLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index c113cf44213b..7523f00e8e0f 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -416,7 +416,7 @@ export default Report; - I think types definitions in a third party library is incomplete or incorrect -When the library indeed contains incorrect type definitions and it cannot be updated, use module augmentation to correct them. All module augmentation code should be contained in `global.d.ts`. +When the library indeed contains incorrect type definitions and it cannot be updated, use module augmentation to correct them. All module augmentation code should be contained in `/src/global.d.ts`. ```ts declare module "external-library-name" { From bf2005ff30da57dc830fa06b2329a15f64a759ba Mon Sep 17 00:00:00 2001 From: Hayata Suenaga Date: Mon, 3 Jul 2023 00:16:57 -0700 Subject: [PATCH 035/325] add tsx section --- contributingGuides/TS_STYLE.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index 7523f00e8e0f..9299542c81fe 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -23,6 +23,7 @@ - [1.14 Export Prop Types](#export-prop-types) - [1.15 File Organization](#file-organization) - [1.16 Reusable Types](#reusable-types) + - [1.17 `.tsx`](#tsx) - [Communication Items](#items) - [Migration Guidelines](#migration-guidelines) @@ -410,6 +411,12 @@ type Report = {...}; export default Report; ``` + + +- [1.17](#tsx) **tsx** Use `.tsx` extension for files that contain React syntax. + +> Why? It is a widely adopted convention to mark any files that contain React specific syntax with `.jsx` or `.tsx`. + ## Communication Items > Comment in the `#expensify-open-source` Slack channel if any of the following situations are encountered. Each comment should be prefixed with `TS ATTENTION:`. Internal engineers will access each situation and prescribe solutions to each case. Internal engineers should refer to general solutions to each situation that follows each list item. From 6d9bf6d7b5ee64af67961503b781518774f127f9 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga Date: Mon, 3 Jul 2023 21:51:01 -0700 Subject: [PATCH 036/325] add more guidelines on naming convention and prop types --- contributingGuides/TS_STYLE.md | 101 +++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 23 deletions(-) diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index 9299542c81fe..0c43525cb477 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -24,6 +24,7 @@ - [1.15 File Organization](#file-organization) - [1.16 Reusable Types](#reusable-types) - [1.17 `.tsx`](#tsx) + - [1.18 No inline prop types](#no-inline-prop-types) - [Communication Items](#items) - [Migration Guidelines](#migration-guidelines) @@ -69,21 +70,56 @@ type Foo = { -- [1.1](#naming-conventions) **Naming Conventions**: Use PascalCase for type names. Do not postfix type aliases with `Type`. Use singular name for union types. eslint: [`@typescript-eslint/naming-convention`](https://typescript-eslint.io/rules/naming-convention/) +- [1.1](#naming-conventions) **Naming Conventions**: Follow naming conventions specified below - ```ts - // bad - type foo = ...; - type BAR = ...; - type PersonType = ...; - type Colors = 'red' | 'blue' | 'green'; + - Use PascalCase for type names. eslint: [`@typescript-eslint/naming-convention`](https://typescript-eslint.io/rules/naming-convention/) - // good - type Foo = ...; - type Bar = ...; - type Person = ...; - type Color = 'red' | 'blue' | 'green'; - ``` + ```ts + // bad + type foo = ...; + type BAR = ...; + + // good + type Foo = ...; + type Bar = ...; + ``` + + - Do not postfix type aliases with `Type`. + + ```ts + // bad + type PersonType = ...; + + // good + type Person = ...; + ``` + + - Use singular name for union types. + + ```ts + // bad + type Colors = "red" | "blue" | "green"; + + // good + type Color = "red" | "blue" | "green"; + ``` + + - For generic type parameters, use `T` if you have only one type parameter. Don't use the `T`, `U`, `V`... sequence. Make type parameter names descriptive, each prefixed with `T`. + + > Prefix each type parameter name to distinguish them from other types. + + ```ts + // bad + type KeyValuePair = { key: K; value: U }; + + type Keys = Array; + + // good + type KeyValuePair = { key: TKey; value: TValue }; + + type Keys = Array; + type Keys = Array; + ``` @@ -311,9 +347,9 @@ type Foo = { -- [1.14](#export-prop-types) **Prop Types**: Define and export prop types for components. Use exported prop types instead of grabbing the prop type from a component. +- [1.14](#export-prop-types) **Prop Types**: Don't use `ComponentProps` to grab a component's prop types. Go to the source file for the component and export prop types from there. Import and use the exported prop types. - > Why? Exporting prop types aids reusability. + > Don't export prop types from component files by default. Only export it when there is a code that needs to access the prop type directly. ```tsx // MyComponent.tsx @@ -403,19 +439,38 @@ type Foo = { - [1.16](#reusable-types) **Reusable Types**: Reusable type definitions, such as models (e.g. Report), must have their own file and be placed under `src/types/`. The type should be exported as a default export. -```ts -// src/types/Report.ts + ```ts + // src/types/Report.ts -type Report = {...}; + type Report = {...}; -export default Report; -``` + export default Report; + ``` + + + +- [1.17](#tsx) **tsx**: Use `.tsx` extension for files that contain React syntax. + + > Why? It is a widely adopted convention to mark any files that contain React specific syntax with `.jsx` or `.tsx`. - + -- [1.17](#tsx) **tsx** Use `.tsx` extension for files that contain React syntax. +- [1.18](#no-inline-prop-types) **No inline prop tpe**: Do not define prop types inline for components that are exported. -> Why? It is a widely adopted convention to mark any files that contain React specific syntax with `.jsx` or `.tsx`. + > Why? Prop types might need to be exported from component files. //TODO: link to the export component types section. If the component is only used inside a file or module and not exported, then inline prop types can be used. + + ```ts + // bad + export default function MyComponent({ foo, bar }: { foo: string, bar: number }){ + // component implementation + }; + + // good + type MyComponentProp = { foo: string, bar: number }; + export default MyComponent({ foo, bar }: MyComponentProp){ + // component implementation + } + ``` ## Communication Items From ff482abe15f53edddd13889545bee0aa98c76077 Mon Sep 17 00:00:00 2001 From: Nathalie Kuoch Date: Tue, 4 Jul 2023 13:24:44 +0200 Subject: [PATCH 037/325] Use getMoneyRequestTotal for displayed amount --- src/languages/en.js | 4 ++-- src/languages/es.js | 2 +- src/pages/home/report/ReportActionItem.js | 4 +++- src/pages/home/report/ReportActionItemReimbursementQueued.js | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index 2020320916cd..e30765f7497f 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -362,9 +362,9 @@ export default { amountEach: ({amount}) => `${amount} each`, payerOwesAmount: ({payer, amount}) => `${payer} owes ${amount}`, payerPaidAmount: ({payer, amount}) => `${payer} paid ${amount}`, - waitingOnBankAccount: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`, - paymentWaitingOnSubmitter: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a `, payerSettled: ({amount}) => `paid ${amount}`, + waitingOnBankAccount: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`, + waitingOnBankAccountPrefix: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a `, settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`, settledElsewhereWithAmount: ({amount}) => `paid ${amount} elsewhere`, settledPaypalMeWithAmount: ({amount}) => `paid ${amount} using Paypal.me`, diff --git a/src/languages/es.js b/src/languages/es.js index bcb634afd76c..d513d86a74f9 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -363,7 +363,7 @@ export default { payerPaidAmount: ({payer, amount}) => `${payer} pagó ${amount}`, payerSettled: ({amount}) => `pagó ${amount}`, waitingOnBankAccount: ({submitterDisplayName}) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} agregue una Cuenta bancaria`, - paymentWaitingOnSubmitter: ({submitterDisplayName}) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} agregue una `, + waitingOnBankAccountPrefix: ({submitterDisplayName}) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} agregue una `, settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} agregó una cuenta bancaria. El pago de ${amount} se ha realizado.`, settledElsewhereWithAmount: ({amount}) => `pagó ${amount} de otra forma`, settledPaypalMeWithAmount: ({amount}) => `pagó ${amount} con PayPal.me`, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index abdf9ada532c..fcfb92a1bc61 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -60,6 +60,7 @@ import {hideContextMenu} from './ContextMenu/ReportActionContextMenu'; import * as PersonalDetailsUtils from '../../../libs/PersonalDetailsUtils'; import ReportActionItemReimbursed from './ReportActionItemReimbursed'; import * as CurrencyUtils from '../../../libs/CurrencyUtils'; +import {getMoneyRequestTotal} from "../../../libs/ReportUtils"; const propTypes = { ...windowDimensionsPropTypes, @@ -280,10 +281,11 @@ function ReportActionItem(props) { /> ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSED) { + const totalAmount = getMoneyRequestTotal(props.report); const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetailsList, [props.report.ownerAccountID, 'displayName'], props.report.ownerEmail); children = ( diff --git a/src/pages/home/report/ReportActionItemReimbursementQueued.js b/src/pages/home/report/ReportActionItemReimbursementQueued.js index 7cb23777e030..1c515ea82297 100644 --- a/src/pages/home/report/ReportActionItemReimbursementQueued.js +++ b/src/pages/home/report/ReportActionItemReimbursementQueued.js @@ -19,7 +19,7 @@ function ReportActionItemReimbursementQueued(props) { if (shouldSubmitterAddBankAccount) { return ( - {props.translate('iou.paymentWaitingOnSubmitter', {submitterDisplayName: props.submitterDisplayName})} + {props.translate('iou.waitingOnBankAccountPrefix', {submitterDisplayName: props.submitterDisplayName})} {props.translate('common.bankAccount')} From ffdbf1ae4815dac5930b3fb4bc88b85f8d4001e0 Mon Sep 17 00:00:00 2001 From: Nathalie Kuoch Date: Tue, 4 Jul 2023 13:37:31 +0200 Subject: [PATCH 038/325] Fix console error by adding report billing state --- src/CONST.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CONST.js b/src/CONST.js index 11208087065e..b73231ae4944 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -592,6 +592,7 @@ const CONST = { OPEN: 0, PROCESSING: 1, SUBMITTED: 2, + BILLING: 3, }, STATUS: { OPEN: 0, From 2f1a1b291d76499cca05da4ebcc540191335eae6 Mon Sep 17 00:00:00 2001 From: Nathalie Kuoch Date: Tue, 4 Jul 2023 14:20:20 +0200 Subject: [PATCH 039/325] Fix eslint --- src/pages/home/report/ReportActionItem.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index fcfb92a1bc61..3bd636826cd3 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -60,7 +60,6 @@ import {hideContextMenu} from './ContextMenu/ReportActionContextMenu'; import * as PersonalDetailsUtils from '../../../libs/PersonalDetailsUtils'; import ReportActionItemReimbursed from './ReportActionItemReimbursed'; import * as CurrencyUtils from '../../../libs/CurrencyUtils'; -import {getMoneyRequestTotal} from "../../../libs/ReportUtils"; const propTypes = { ...windowDimensionsPropTypes, @@ -281,7 +280,7 @@ function ReportActionItem(props) { /> ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSED) { - const totalAmount = getMoneyRequestTotal(props.report); + const totalAmount = ReportUtils.getMoneyRequestTotal(props.report); const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetailsList, [props.report.ownerAccountID, 'displayName'], props.report.ownerEmail); children = ( Date: Tue, 4 Jul 2023 18:20:41 +0200 Subject: [PATCH 040/325] Some refactoring --- src/components/LHNOptionsList/OptionRowLHN.js | 5 +- .../ReportActionItem/MoneyRequestAction.js | 2 +- src/libs/OptionsListUtils.js | 27 ++------- src/libs/ReportUtils.js | 55 ++++++++++++------ src/libs/SidebarUtils.js | 3 +- src/pages/home/ReportScreen.js | 1 + src/pages/home/sidebar/SidebarLinks.js | 1 + src/pages/reportPropTypes.js | 3 + tests/unit/ReportUtilsTest.js | 57 ++++++++++++------- 9 files changed, 91 insertions(+), 63 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 753f5e01c585..1f581d60a388 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -28,6 +28,7 @@ import * as OptionsListUtils from '../../libs/OptionsListUtils'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import * as Report from '../../libs/actions/Report'; +import * as ReportUtils from '../../libs/ReportUtils'; const propTypes = { /** Style for hovered state */ @@ -101,12 +102,12 @@ function OptionRowLHN(props) { const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + const shouldShowGreenDotIndicator = !hasBrickError && (optionItem.isUnreadWithMention || - (optionItem.hasOutstandingIOU && !optionItem.isIOUReportOwner) || (optionItem.isTaskReport && optionItem.isTaskAssignee && !optionItem.isTaskCompleted) || - (optionItem.isWaitingOnBankAccount && optionItem.isIOUReportOwner)); + (ReportUtils.isWaitingForIOUActionFromCurrentUser(optionItem))); /** * Show the ReportActionContextMenu modal popover. diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index 2a78013fdabd..8bef184bab08 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -135,7 +135,7 @@ function MoneyRequestAction(props) { if ( !_.isEmpty(props.iouReport) && !_.isEmpty(props.reportActions) && - (props.chatReport.hasOutstandingIOU || props.chatReport.isWaitingOnBankAccount) && + props.chatReport.hasOutstandingIOU && props.isMostRecentIOUReportAction && props.action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && props.network.isOffline diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index e9376b246f1a..c1701a9bcb01 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -57,26 +57,6 @@ Onyx.connect({ }, }); -const expenseReports = {}; -const iouReports = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - callback: (report, key) => { - if (!report || !key || !report.ownerEmail) { - return; - } - - if (ReportUtils.isExpenseReport(report)) { - expenseReports[key] = report; - return; - } - - if (ReportUtils.isIOUReport(report)) { - iouReports[key] = report; - } - }, -}); - const lastReportActions = {}; const allSortedReportActions = {}; Onyx.connect({ @@ -437,6 +417,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { isDefaultRoom: false, isPinned: false, hasOutstandingIOU: false, + isWaitingOnBankAccount: false, iouReportID: null, isIOUReportOwner: null, iouReportAmount: 0, @@ -510,8 +491,8 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.alternateText = LocalePhoneNumber.formatPhoneNumber(lodashGet(personalDetails, [accountIDs[0], 'login'], '')); } - result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result, iouReports); - result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result, iouReports); + result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); + result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result); if (!hasMultipleParticipants) { result.login = personalDetail.login; @@ -625,7 +606,7 @@ function getOptions( const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchInputValue; // Filter out all the reports that shouldn't be displayed - const filteredReports = _.filter(reports, (report) => ReportUtils.shouldReportBeInOptionList(report, Navigation.getReportIDFromRoute(), false, iouReports, betas, policies)); + const filteredReports = _.filter(reports, (report) => ReportUtils.shouldReportBeInOptionList(report, Navigation.getReportIDFromRoute(), false, betas, policies)); // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index d7dbb68727f8..6d6ec9329db3 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -998,8 +998,17 @@ function getPolicyExpenseChatName(report) { function getMoneyRequestReportName(report) { const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(report), report.currency); const payerName = isExpenseReport(report) ? getPolicyName(report) : getDisplayNameForParticipant(report.managerID); + const payerPaidAmountMsg = Localize.translateLocal('iou.payerPaidAmount', {payer: payerName, amount: formattedAmount}); - return Localize.translateLocal(report.hasOutstandingIOU ? 'iou.payerOwesAmount' : 'iou.payerPaidAmount', {payer: payerName, amount: formattedAmount}); + if (report.isWaitingOnBankAccount) { + return `${payerPaidAmountMsg} • ${Localize.translateLocal('iou.pending')}`; + } + + if (report.hasOutstandingIOU) { + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); + } + + return payerPaidAmountMsg; } /** @@ -1888,41 +1897,54 @@ function isUnreadWithMention(report) { } /** - * Determines if a report has an outstanding IOU that doesn't belong to the currently logged in user + * Determines if a report has an outstanding IOU that is waiting for an action from the current user * * @param {Object} report * @param {String} report.iouReportID - * @param {Object} iouReports * @returns {boolean} */ -function hasOutstandingIOU(report, iouReports) { +function isWaitingForIOUActionFromCurrentUser(report) { + let reportToLook = report; + if (report.iouReportID) { - const iouReport = iouReports && iouReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; - if (!iouReport || !iouReport.ownerAccountID) { + const iouReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; + if (!iouReport) { return false; } - return iouReport.ownerAccountID === currentUserAccountID ? iouReport.isWaitingOnBankAccount : iouReport.hasOutstandingIOU; + reportToLook = iouReport; + } + + if (!reportToLook.ownerAccountID) { + return false; + } + + if (reportToLook.ownerAccountID === currentUserAccountID && reportToLook.isWaitingOnBankAccount) { + return true; + } + + if (reportToLook.ownerAccountID !== currentUserAccountID && reportToLook.hasOutstandingIOU) { + return true; } - return report.ownerAccountID === currentUserAccountID ? report.isWaitingOnBankAccount : report.hasOutstandingIOU; + return false; } /** * @param {Object} report * @param {String} report.iouReportID - * @param {Object} iouReports * @returns {Boolean} */ -function isIOUOwnedByCurrentUser(report, iouReports = {}) { +function isIOUOwnedByCurrentUser(report) { + let reportToLook = report; if (report.iouReportID) { - const iouReport = iouReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; + const iouReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; if (iouReport) { - return iouReport.ownerAccountID === currentUserAccountID; + reportToLook = iouReport; } } - return report.ownerAccountID === currentUserAccountID; + return reportToLook.ownerAccountID === currentUserAccountID; } /** @@ -1993,12 +2015,11 @@ function canAccessReport(report, policies, betas) { * @param {Object} report * @param {String} currentReportId * @param {Boolean} isInGSDMode - * @param {Object} iouReports * @param {String[]} betas * @param {Object} policies * @returns {boolean} */ -function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, iouReports, betas, policies) { +function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies) { const isInDefaultMode = !isInGSDMode; // Exclude reports that have no data because there wouldn't be anything to show in the option item. @@ -2025,7 +2046,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, iouRep // Include reports if they have a draft, are pinned, or have an outstanding IOU // These are always relevant to the user no matter what view mode the user prefers - if (report.hasDraft || report.isPinned || hasOutstandingIOU(report, iouReports)) { + if (report.hasDraft || report.isPinned || isWaitingForIOUActionFromCurrentUser(report)) { return true; } @@ -2429,7 +2450,7 @@ export { isCurrentUserTheOnlyParticipant, hasAutomatedExpensifyAccountIDs, hasExpensifyGuidesEmails, - hasOutstandingIOU, + isWaitingForIOUActionFromCurrentUser, isIOUOwnedByCurrentUser, getMoneyRequestTotal, canShowReportRecipientLocalTime, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index b9961f5fd870..497bc1e5258d 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -125,7 +125,7 @@ function getOrderedReportIDs(currentReportId) { const isInDefaultMode = !isInGSDMode; // Filter out all the reports that shouldn't be displayed - const reportsToDisplay = _.filter(allReports, (report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, allReports, betas, policies)); + const reportsToDisplay = _.filter(allReports, (report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies)); if (_.isEmpty(reportsToDisplay)) { // Display Concierge chat report when there is no report to be displayed const conciergeChatReport = _.find(allReports, ReportUtils.isConciergeChatReport); @@ -302,6 +302,7 @@ function getOptionData(reportID) { result.hasOutstandingIOU = report.hasOutstandingIOU; result.parentReportID = report.parentReportID || null; result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; + result.shouldShowGreenDotIndicator = report.shouldShowGreenDotIndicator; const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; const subtitle = ReportUtils.getChatRoomSubtitle(report); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index efe86e94b2bf..765da723697e 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -97,6 +97,7 @@ const defaultProps = { report: { hasOutstandingIOU: false, isLoadingReportActions: false, + isWaitingOnBankAccount: false, }, isComposerFullSize: false, betas: [], diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 6f122733607b..7a64a42389f8 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -294,6 +294,7 @@ const chatReportSelector = (report) => lastVisibleActionCreated: report.lastVisibleActionCreated, iouReportID: report.iouReportID, hasOutstandingIOU: report.hasOutstandingIOU, + isWaitingOnBankAccount: report.isWaitingOnBankAccount, statusNum: report.statusNum, stateNum: report.stateNum, chatType: report.chatType, diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index 4b50909032a1..5cc9fdbe55ee 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -25,6 +25,9 @@ export default PropTypes.shape({ /** Indicates if the report is pinned to the LHN or not */ isPinned: PropTypes.bool, + /** Whether we're waiting on submitter to add a bank account */ + isWaitingOnBankAccount: PropTypes.bool, + /** The email of the last message's actor */ lastActorEmail: PropTypes.string, diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index f33fdc86ef77..6771332ff4c6 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -291,67 +291,70 @@ describe('ReportUtils', () => { }); }); - describe('hasOutstandingIOU', () => { + describe('isWaitingForIOUActionFromCurrentUser', () => { it('returns false when there is no report', () => { - expect(ReportUtils.hasOutstandingIOU()).toBe(false); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser()).toBe(false); }); it('returns false when the report has no iouReportID', () => { const report = LHNTestUtils.getFakeReport(); - expect(ReportUtils.hasOutstandingIOU(report)).toBe(false); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); }); it('returns false when there is no iouReports collection', () => { const report = { ...LHNTestUtils.getFakeReport(), iouReportID: '1', }; - expect(ReportUtils.hasOutstandingIOU(report)).toBe(false); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); }); it('returns false when there is no matching IOU report', () => { const report = { ...LHNTestUtils.getFakeReport(), iouReportID: '1', }; - const iouReports = {}; - expect(ReportUtils.hasOutstandingIOU(report, iouReports)).toBe(false); + Onyx.merge(ONYXKEYS.COLLECTION.REPORT, { + report_2: { + reportID: '2', + }, + }); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); }); it('returns false when the matched IOU report does not have an owner email', () => { const report = { ...LHNTestUtils.getFakeReport(), iouReportID: '1', }; - const iouReports = { + Onyx.merge(ONYXKEYS.COLLECTION.REPORT, { report_1: { reportID: '1', }, - }; - expect(ReportUtils.hasOutstandingIOU(report, iouReports)).toBe(false); + }); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); }); it('returns false when the matched IOU report does not have an owner email', () => { const report = { ...LHNTestUtils.getFakeReport(), iouReportID: '1', }; - const iouReports = { + Onyx.merge(ONYXKEYS.COLLECTION.REPORT, { report_1: { reportID: '1', ownerAccountID: 99, }, - }; - expect(ReportUtils.hasOutstandingIOU(report, iouReports)).toBe(false); + }); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); }); it('returns true when the report has an oustanding IOU', () => { const report = { ...LHNTestUtils.getFakeReport(), - iouReportID: '1', - hasOutstandingIOU: true, + iouReportID: '1',q }; - const iouReports = { + Onyx.merge(ONYXKEYS.COLLECTION.REPORT, { report_1: { reportID: '1', ownerAccountID: 99, }, - }; - expect(ReportUtils.hasOutstandingIOU(report, iouReports)).toBe(true); + }); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(true); }); it('returns false when the report has no oustanding IOU', () => { const report = { @@ -359,13 +362,29 @@ describe('ReportUtils', () => { iouReportID: '1', hasOutstandingIOU: false, }; - const iouReports = { + Onyx.merge(ONYXKEYS.COLLECTION.REPORT, { report_1: { reportID: '1', ownerAccountID: 99, }, + }); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); + }); + + it('returns true when the report has no oustanding IOU but is waiting for a bank account', () => { + const report = { + ...LHNTestUtils.getFakeReport(), + iouReportID: '1', + hasOutstandingIOU: false, }; - expect(ReportUtils.hasOutstandingIOU(report, iouReports)).toBe(false); + Onyx.merge(ONYXKEYS.COLLECTION.REPORT, { + report_1: { + reportID: '1', + ownerAccountID: currentUserEmail, + isWaitingOnBankAccount: true, + }, + }); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); }); }); From 0dd70cda8d5519b0601a9087e27e0c335cfce1f8 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga Date: Tue, 4 Jul 2023 10:12:25 -0700 Subject: [PATCH 041/325] fix typos --- contributingGuides/TS_STYLE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index 0c43525cb477..2755e6120712 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -41,7 +41,7 @@ - [TypeScript playground](https://www.typescriptlang.org/play?q=231#example) - Go though all examples on the playground. Click on "Example" tab on the top - Handy Reference - - [TypeScript CheatSheet](https://www.typescriptlang.org/static/TypeScript%20Types-ae199d69aeecf7d4a2704a528d0fd3f9.png) + - [TypeScript CheatSheet](https://www.typescriptlang.org/cheatsheets) - [Type](https://www.typescriptlang.org/static/TypeScript%20Types-ae199d69aeecf7d4a2704a528d0fd3f9.png) - [Control Flow Analysis](https://www.typescriptlang.org/static/TypeScript%20Control%20Flow%20Analysis-8a549253ad8470850b77c4c5c351d457.png) - TypeScript with React @@ -88,7 +88,7 @@ type Foo = { ```ts // bad - type PersonType = ...; + type PersonType = ...; // good type Person = ...; @@ -455,7 +455,7 @@ type Foo = { -- [1.18](#no-inline-prop-types) **No inline prop tpe**: Do not define prop types inline for components that are exported. +- [1.18](#no-inline-prop-types) **No inline prop types**: Do not define prop types inline for components that are exported. > Why? Prop types might need to be exported from component files. //TODO: link to the export component types section. If the component is only used inside a file or module and not exported, then inline prop types can be used. From 53717db4f52558dad170c7ed8d55b519f4fa60ab Mon Sep 17 00:00:00 2001 From: Nathalie Kuoch Date: Tue, 4 Jul 2023 21:45:47 +0200 Subject: [PATCH 042/325] Use button instead of link to add a bank account --- src/languages/en.js | 1 - src/languages/es.js | 1 - .../ReportActionItemReimbursementQueued.js | 29 ++++++++++--------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index e30765f7497f..e8e9fdca7dcd 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -364,7 +364,6 @@ export default { payerPaidAmount: ({payer, amount}) => `${payer} paid ${amount}`, payerSettled: ({amount}) => `paid ${amount}`, waitingOnBankAccount: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`, - waitingOnBankAccountPrefix: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a `, settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`, settledElsewhereWithAmount: ({amount}) => `paid ${amount} elsewhere`, settledPaypalMeWithAmount: ({amount}) => `paid ${amount} using Paypal.me`, diff --git a/src/languages/es.js b/src/languages/es.js index d513d86a74f9..59666ef7f6b8 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -363,7 +363,6 @@ export default { payerPaidAmount: ({payer, amount}) => `${payer} pagó ${amount}`, payerSettled: ({amount}) => `pagó ${amount}`, waitingOnBankAccount: ({submitterDisplayName}) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} agregue una Cuenta bancaria`, - waitingOnBankAccountPrefix: ({submitterDisplayName}) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} agregue una `, settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} agregó una cuenta bancaria. El pago de ${amount} se ha realizado.`, settledElsewhereWithAmount: ({amount}) => `pagó ${amount} de otra forma`, settledPaypalMeWithAmount: ({amount}) => `pagó ${amount} con PayPal.me`, diff --git a/src/pages/home/report/ReportActionItemReimbursementQueued.js b/src/pages/home/report/ReportActionItemReimbursementQueued.js index 1c515ea82297..dbca5d6de0bb 100644 --- a/src/pages/home/report/ReportActionItemReimbursementQueued.js +++ b/src/pages/home/report/ReportActionItemReimbursementQueued.js @@ -1,11 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {View} from "react-native"; import Text from '../../../components/Text'; import styles from '../../../styles/styles'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; -import TextLink from '../../../components/TextLink'; import * as BankAccounts from '../../../libs/actions/BankAccounts'; import * as store from '../../../libs/actions/ReimbursementAccount/store'; +import Button from "../../../components/Button"; const propTypes = { submitterDisplayName: PropTypes.string.isRequired, @@ -16,18 +17,20 @@ const propTypes = { function ReportActionItemReimbursementQueued(props) { const shouldSubmitterAddBankAccount = props.isCurrentUserSubmitter && !store.hasCreditBankAccount(); - if (shouldSubmitterAddBankAccount) { - return ( - - {props.translate('iou.waitingOnBankAccountPrefix', {submitterDisplayName: props.submitterDisplayName})} - - {props.translate('common.bankAccount')} - - - ); - } - - return {props.translate('iou.waitingOnBankAccount', {submitterDisplayName: props.submitterDisplayName})}; + return ( + + {props.translate('iou.waitingOnBankAccount', {submitterDisplayName: props.submitterDisplayName})} + {shouldSubmitterAddBankAccount && ( +