diff --git a/.eslintrc.js b/.eslintrc.js index 56a5236a7899..4df9493b2e8c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -260,13 +260,7 @@ module.exports = { 'no-restricted-imports': [ 'error', { - paths: [ - ...restrictedImportPaths, - { - name: 'underscore', - message: 'Please use the corresponding method from lodash instead', - }, - ], + paths: restrictedImportPaths, patterns: restrictedImportPatterns, }, ], diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 0a88ecd7bda8..1e2bf7f594e8 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -72,10 +72,10 @@ Using arrow functions is the preferred way to write an anonymous function such a ```javascript // Bad -_.map(someArray, function (item) {...}); +someArray.map(function (item) {...}); // Good -_.map(someArray, (item) => {...}); +someArray.map((item) => {...}); ``` Empty functions (noop) should be declare as arrow functions with no whitespace inside. Avoid _.noop() @@ -112,38 +112,9 @@ if (someCondition) { } ``` -## Object / Array Methods - -We have standardized on using [underscore.js](https://underscorejs.org/) methods for objects and collections instead of the native [Array instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#instance_methods). This is mostly to maintain consistency, but there are some type safety features and conveniences that underscore methods provide us e.g. the ability to iterate over an object and the lack of a `TypeError` thrown if a variable is `undefined`. - -```javascript -// Bad -myArray.forEach(item => doSomething(item)); -// Good -_.each(myArray, item => doSomething(item)); - -// Bad -const myArray = Object.keys(someObject).map(key => doSomething(someObject[key])); -// Good -const myArray = _.map(someObject, (value, key) => doSomething(value)); - -// Bad -myCollection.includes('item'); -// Good -_.contains(myCollection, 'item'); - -// Bad -const modifiedArray = someArray.filter(filterFunc).map(mapFunc); -// Good -const modifiedArray = _.chain(someArray) - .filter(filterFunc) - .map(mapFunc) - .value(); -``` - ## Accessing Object Properties and Default Values -Use `lodashGet()` to safely access object properties and `||` to short circuit null or undefined values that are not guaranteed to exist in a consistent way throughout the codebase. In the rare case that you want to consider a falsy value as usable and the `||` operator prevents this then be explicit about this in your code and check for the type using an underscore method e.g. `_.isBoolean(value)` or `_.isEqual(0)`. +Use `lodashGet()` to safely access object properties and `||` to short circuit null or undefined values that are not guaranteed to exist in a consistent way throughout the codebase. In the rare case that you want to consider a falsy value as usable and the `||` operator prevents this then be explicit about this in your code and check for the type. ```javascript // Bad @@ -448,7 +419,7 @@ const propTypes = { ### Important Note: -In React Native, one **must not** attempt to falsey-check a string for an inline ternary. Even if it's in curly braces, React Native will try to render it as a `` node and most likely throw an error about trying to render text outside of a `` component. Use `_.isEmpty()` instead. +In React Native, one **must not** attempt to falsey-check a string for an inline ternary. Even if it's in curly braces, React Native will try to render it as a `` node and most likely throw an error about trying to render text outside of a `` component. Use `!!` instead. ```javascript // Bad! This will cause a breaking an error on native platforms @@ -467,7 +438,7 @@ In React Native, one **must not** attempt to falsey-check a string for an inline { return ( - {!_.isEmpty(props.title) + {!!props.title ? {props.title} : null} This is the body diff --git a/package-lock.json b/package-lock.json index ba65f7292a80..0d5d51ccb691 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,8 +131,7 @@ "react-webcam": "^7.1.1", "react-window": "^1.8.9", "semver": "^7.5.2", - "shim-keyboard-event-key": "^1.0.3", - "underscore": "^1.13.1" + "shim-keyboard-event-key": "^1.0.3" }, "devDependencies": { "@actions/core": "1.10.0", @@ -181,7 +180,6 @@ "@types/react-test-renderer": "^18.0.0", "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", - "@types/underscore": "^1.11.5", "@types/webpack": "^5.28.5", "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^6.13.2", @@ -12666,11 +12664,6 @@ "version": "4.0.2", "license": "MIT" }, - "node_modules/@types/underscore": { - "version": "1.11.5", - "dev": true, - "license": "MIT" - }, "node_modules/@types/unist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", diff --git a/package.json b/package.json index 414082054f65..51adcb1f6e74 100644 --- a/package.json +++ b/package.json @@ -183,8 +183,7 @@ "react-webcam": "^7.1.1", "react-window": "^1.8.9", "semver": "^7.5.2", - "shim-keyboard-event-key": "^1.0.3", - "underscore": "^1.13.1" + "shim-keyboard-event-key": "^1.0.3" }, "devDependencies": { "@actions/core": "1.10.0", @@ -233,7 +232,6 @@ "@types/react-test-renderer": "^18.0.0", "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", - "@types/underscore": "^1.11.5", "@types/webpack": "^5.28.5", "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^6.13.2", diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d372b30d2393..73c3589ed794 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -208,11 +208,6 @@ const ROUTES = { route: 'r/:reportID/avatar', getRoute: (reportID: string) => `r/${reportID}/avatar` as const, }, - EDIT_REQUEST: { - route: 'r/:threadReportID/edit/:field/:tagIndex?', - getRoute: (threadReportID: string, field: ValueOf, tagIndex?: number) => - `r/${threadReportID}/edit/${field as string}${typeof tagIndex === 'number' ? `/${tagIndex}` : ''}` as const, - }, EDIT_CURRENCY_REQUEST: { route: 'r/:threadReportID/edit/currency', getRoute: (threadReportID: string, currency: string, backTo: string) => `r/${threadReportID}/edit/currency?currency=${currency}&backTo=${backTo}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index aed70dc1e949..9f00377b618d 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -271,7 +271,6 @@ const SCREENS = { }, EDIT_REQUEST: { - ROOT: 'EditRequest_Root', CURRENCY: 'EditRequest_Currency', REPORT_FIELD: 'EditRequest_ReportField', }, diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index ee61beda74ae..93ffa52bc80b 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -27,12 +27,10 @@ type AdapterProps = { const adapter = createAnimatedPropAdapter( (props: AdapterProps) => { - // eslint-disable-next-line rulesdir/prefer-underscore-method if (Object.keys(props).includes('fill')) { // eslint-disable-next-line no-param-reassign props.fill = {type: 0, payload: processColor(props.fill)}; } - // eslint-disable-next-line rulesdir/prefer-underscore-method if (Object.keys(props).includes('stroke')) { // eslint-disable-next-line no-param-reassign props.stroke = {type: 0, payload: processColor(props.stroke)}; diff --git a/src/components/Modal/modalPropTypes.js b/src/components/Modal/modalPropTypes.js deleted file mode 100644 index 84e610b694e4..000000000000 --- a/src/components/Modal/modalPropTypes.js +++ /dev/null @@ -1,89 +0,0 @@ -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import stylePropTypes from '@styles/stylePropTypes'; -import CONST from '@src/CONST'; - -const propTypes = { - /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */ - fullscreen: PropTypes.bool, - - /** Should we close modal on outside click */ - shouldCloseOnOutsideClick: PropTypes.bool, - - /** Should we announce the Modal visibility changes? */ - shouldSetModalVisibility: PropTypes.bool, - - /** Callback method fired when the user requests to close the modal */ - onClose: PropTypes.func.isRequired, - - /** State that determines whether to display the modal or not */ - isVisible: PropTypes.bool.isRequired, - - /** Modal contents */ - children: PropTypes.node.isRequired, - - /** Callback method fired when the user requests to submit the modal content. */ - onSubmit: PropTypes.func, - - /** Callback method fired when the modal is hidden */ - onModalHide: PropTypes.func, - - /** Callback method fired when the modal is shown */ - onModalShow: PropTypes.func, - - /** Style of modal to display */ - type: PropTypes.oneOf(_.values(CONST.MODAL.MODAL_TYPE)), - - /** A react-native-animatable animation definition for the modal display animation. */ - animationIn: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - - /** A react-native-animatable animation definition for the modal hide animation. */ - animationOut: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - - /** The anchor position of a popover modal. Has no effect on other modal types. */ - popoverAnchorPosition: PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }), - - /** Modal container styles */ - innerContainerStyle: stylePropTypes, - - /** Whether the modal should go under the system statusbar */ - statusBarTranslucent: PropTypes.bool, - - /** Whether the modal should avoid the keyboard */ - avoidKeyboard: PropTypes.bool, - - /** - * Whether the modal should hide its content while animating. On iOS, set to true - * if `useNativeDriver` is also true, to avoid flashes in the UI. - * - * See: https://github.com/react-native-modal/react-native-modal/pull/116 - * */ - hideModalContentWhileAnimating: PropTypes.bool, - - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - fullscreen: true, - shouldCloseOnOutsideClick: false, - shouldSetModalVisibility: true, - onSubmit: null, - type: '', - onModalHide: () => {}, - onModalShow: () => {}, - animationIn: null, - animationOut: null, - popoverAnchorPosition: {}, - innerContainerStyle: {}, - statusBarTranslucent: true, - avoidKeyboard: false, - hideModalContentWhileAnimating: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index cc73a6fc8fd7..6515333e4015 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -1,8 +1,13 @@ +import lodashDebounce from 'lodash/debounce'; +import lodashFind from 'lodash/find'; +import lodashFindIndex from 'lodash/findIndex'; import lodashGet from 'lodash/get'; +import lodashIsEqual from 'lodash/isEqual'; +import lodashMap from 'lodash/map'; +import lodashValues from 'lodash/values'; import PropTypes from 'prop-types'; import React, {Component} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; import FixedFooter from '@components/FixedFooter'; @@ -77,9 +82,9 @@ class BaseOptionsSelector extends Component { this.calculateAllVisibleOptionsCount = this.calculateAllVisibleOptionsCount.bind(this); this.handleFocusIn = this.handleFocusIn.bind(this); this.handleFocusOut = this.handleFocusOut.bind(this); - this.debouncedUpdateSearchValue = _.debounce(this.updateSearchValue, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME); + this.debouncedUpdateSearchValue = lodashDebounce(this.updateSearchValue, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME); this.relatedTarget = null; - this.accessibilityRoles = _.values(CONST.ROLE); + this.accessibilityRoles = lodashValues(CONST.ROLE); this.isWebOrDesktop = [CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform()); const allOptions = this.flattenSections(); @@ -155,7 +160,7 @@ class BaseOptionsSelector extends Component { this.focusedOption = this.state.allOptions[this.state.focusedIndex]; } - if (_.isEqual(this.props.sections, prevProps.sections)) { + if (lodashIsEqual(this.props.sections, prevProps.sections)) { return; } @@ -171,14 +176,14 @@ class BaseOptionsSelector extends Component { } const newFocusedIndex = this.props.selectedOptions.length; const isNewFocusedIndex = newFocusedIndex !== this.state.focusedIndex; - const prevFocusedOption = _.find(newOptions, (option) => this.focusedOption && option.keyForList === this.focusedOption.keyForList); - const prevFocusedOptionIndex = prevFocusedOption ? _.findIndex(newOptions, (option) => this.focusedOption && option.keyForList === this.focusedOption.keyForList) : undefined; + const prevFocusedOption = lodashFind(newOptions, (option) => this.focusedOption && option.keyForList === this.focusedOption.keyForList); + const prevFocusedOptionIndex = prevFocusedOption ? lodashFindIndex(newOptions, (option) => this.focusedOption && option.keyForList === this.focusedOption.keyForList) : undefined; // eslint-disable-next-line react/no-did-update-set-state this.setState( { sections: newSections, allOptions: newOptions, - focusedIndex: prevFocusedOptionIndex || (_.isNumber(this.props.focusedIndex) ? this.props.focusedIndex : newFocusedIndex), + focusedIndex: prevFocusedOptionIndex || (typeof this.props.focusedIndex === 'number' ? this.props.focusedIndex : newFocusedIndex), }, () => { // If we just toggled an option on a multi-selection page or cleared the search input, scroll to top @@ -230,11 +235,11 @@ class BaseOptionsSelector extends Component { } else { defaultIndex = this.props.selectedOptions.length; } - if (_.isUndefined(this.props.initiallyFocusedOptionKey)) { + if (this.props.initiallyFocusedOptionKey === undefined) { return defaultIndex; } - const indexOfInitiallyFocusedOption = _.findIndex(allOptions, (option) => option.keyForList === this.props.initiallyFocusedOptionKey); + const indexOfInitiallyFocusedOption = lodashFindIndex(allOptions, (option) => option.keyForList === this.props.initiallyFocusedOptionKey); return indexOfInitiallyFocusedOption; } @@ -245,8 +250,8 @@ class BaseOptionsSelector extends Component { * @returns {Objects[]} */ sliceSections() { - return _.map(this.props.sections, (section) => { - if (_.isEmpty(section.data)) { + return lodashMap(this.props.sections, (section) => { + if (section.data.length === 0) { return section; } @@ -266,7 +271,7 @@ class BaseOptionsSelector extends Component { calculateAllVisibleOptionsCount() { let count = 0; - _.forEach(this.state.sections, (section) => { + this.state.sections.forEach((section) => { count += lodashGet(section, 'data.length', 0); }); @@ -347,7 +352,7 @@ class BaseOptionsSelector extends Component { selectFocusedOption(e) { const focusedItemKey = lodashGet(e, ['target', 'attributes', 'id', 'value']); - const focusedOption = focusedItemKey ? _.find(this.state.allOptions, (option) => option.keyForList === focusedItemKey) : this.state.allOptions[this.state.focusedIndex]; + const focusedOption = focusedItemKey ? lodashFind(this.state.allOptions, (option) => option.keyForList === focusedItemKey) : this.state.allOptions[this.state.focusedIndex]; if (!focusedOption || !this.props.isFocused) { return; @@ -393,8 +398,8 @@ class BaseOptionsSelector extends Component { const allOptions = []; this.disabledOptionsIndexes = []; let index = 0; - _.each(this.props.sections, (section, sectionIndex) => { - _.each(section.data, (option, optionIndex) => { + this.props.sections.forEach((section, sectionIndex) => { + section.data.forEach((option, optionIndex) => { allOptions.push({ ...option, sectionIndex, @@ -496,8 +501,8 @@ class BaseOptionsSelector extends Component { render() { const shouldShowShowMoreButton = this.state.allOptions.length > CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * this.state.paginationPage; const shouldShowFooter = - !this.props.isReadOnly && (this.props.shouldShowConfirmButton || this.props.footerContent) && !(this.props.canSelectMultipleOptions && _.isEmpty(this.props.selectedOptions)); - const defaultConfirmButtonText = _.isUndefined(this.props.confirmButtonText) ? this.props.translate('common.confirm') : this.props.confirmButtonText; + !this.props.isReadOnly && (this.props.shouldShowConfirmButton || this.props.footerContent) && !(this.props.canSelectMultipleOptions && this.props.selectedOptions.length === 0); + const defaultConfirmButtonText = this.props.confirmButtonText === undefined ? this.props.translate('common.confirm') : this.props.confirmButtonText; const shouldShowDefaultConfirmButton = !this.props.footerContent && defaultConfirmButtonText; const safeAreaPaddingBottomStyle = shouldShowFooter ? undefined : this.props.safeAreaPaddingBottomStyle; const listContainerStyles = this.props.listContainerStyles || [this.props.themeStyles.flex1]; diff --git a/src/components/Popover/popoverPropTypes.js b/src/components/Popover/popoverPropTypes.js deleted file mode 100644 index c758c4e6d311..000000000000 --- a/src/components/Popover/popoverPropTypes.js +++ /dev/null @@ -1,48 +0,0 @@ -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import {defaultProps as defaultModalProps, propTypes as modalPropTypes} from '@components/Modal/modalPropTypes'; -import refPropTypes from '@components/refPropTypes'; -import CONST from '@src/CONST'; - -const propTypes = { - ..._.omit(modalPropTypes, ['type', 'popoverAnchorPosition']), - - /** The anchor position of the popover */ - anchorPosition: PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }), - - /** The anchor ref of the popover */ - anchorRef: refPropTypes, - - /** A react-native-animatable animation timing for the modal display animation. */ - animationInTiming: PropTypes.number, - - /** Whether disable the animations */ - disableAnimation: PropTypes.bool, - - /** The ref of the popover */ - withoutOverlayRef: refPropTypes, - - /** Whether we want to show the popover on the right side of the screen */ - fromSidebarMediumScreen: PropTypes.bool, -}; - -const defaultProps = { - ..._.omit(defaultModalProps, ['type', 'popoverAnchorPosition']), - - animationIn: 'fadeIn', - animationOut: 'fadeOut', - animationInTiming: CONST.ANIMATED_TRANSITION, - - // Anchor position is optional only because it is not relevant on mobile - anchorPosition: {}, - anchorRef: () => {}, - disableAnimation: true, - withoutOverlayRef: () => {}, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js deleted file mode 100644 index 80ae1edd5176..000000000000 --- a/src/components/menuItemPropTypes.js +++ /dev/null @@ -1,179 +0,0 @@ -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import stylePropTypes from '@styles/stylePropTypes'; -import CONST from '@src/CONST'; -import avatarPropTypes from './avatarPropTypes'; -import sourcePropTypes from './Image/sourcePropTypes'; -import refPropTypes from './refPropTypes'; - -const propTypes = { - /** Text to be shown as badge near the right end. */ - badgeText: PropTypes.string, - - /** Any additional styles to apply */ - // eslint-disable-next-line react/forbid-prop-types - wrapperStyle: stylePropTypes, - - /** Used to apply offline styles to child text components */ - style: stylePropTypes, - - /** Used to apply styles specifically to the title */ - titleStyle: stylePropTypes, - - /** Function to fire when component is pressed */ - onPress: PropTypes.func, - - /** Icon to display on the left side of component */ - icon: PropTypes.oneOfType([PropTypes.string, sourcePropTypes, PropTypes.arrayOf(avatarPropTypes)]), - - /** Secondary icon to display on the left side of component, right of the icon */ - secondaryIcon: sourcePropTypes, - - /** Icon Width */ - iconWidth: PropTypes.number, - - /** Icon Height */ - iconHeight: PropTypes.number, - - /** Text to display for the item */ - title: PropTypes.string, - - /** Text that appears above the title */ - label: PropTypes.string, - - /** Boolean whether to display the title right icon */ - shouldShowTitleIcon: PropTypes.bool, - - /** Icon to display at right side of title */ - titleIcon: sourcePropTypes, - - /** Boolean whether to display the right icon */ - shouldShowRightIcon: PropTypes.bool, - - /** Should we make this selectable with a checkbox */ - shouldShowSelectedState: PropTypes.bool, - - /** Should the title show with normal font weight (not bold) */ - shouldShowBasicTitle: PropTypes.bool, - - /** Should the description be shown above the title (instead of the other way around) */ - shouldShowDescriptionOnTop: PropTypes.bool, - - /** Whether this item is selected */ - isSelected: PropTypes.bool, - - /** A boolean flag that gives the icon a green fill if true */ - success: PropTypes.bool, - - /** Overrides the icon for shouldShowRightIcon */ - iconRight: sourcePropTypes, - - /** A description text to show under the title */ - description: PropTypes.string, - - /** Any additional styles to pass to the icon container. */ - iconStyles: PropTypes.arrayOf(PropTypes.object), - - /** The fill color to pass into the icon. */ - iconFill: PropTypes.string, - - /** The fill color to pass into the secondary icon. */ - secondaryIconFill: PropTypes.string, - - /** Whether item is focused or active */ - focused: PropTypes.bool, - - /** Should we disable this menu item? */ - disabled: PropTypes.bool, - - /** A right-aligned subtitle for this menu option */ - subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - - /** Flag to choose between avatar image or an icon */ - iconType: PropTypes.oneOf([CONST.ICON_TYPE_AVATAR, CONST.ICON_TYPE_ICON, CONST.ICON_TYPE_WORKSPACE]), - - /** Whether the menu item should be interactive at all */ - interactive: PropTypes.bool, - - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), - - /** Avatars to show on the right of the menu item */ - floatRightAvatars: PropTypes.arrayOf(avatarPropTypes), - - /** The type of brick road indicator to show. */ - brickRoadIndicator: PropTypes.oneOf([CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR, CONST.BRICK_ROAD_INDICATOR_STATUS.INFO, '']), - - /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: PropTypes.bool, - - /** Prop to represent the size of the float right avatar images to be shown */ - floatRightAvatarSize: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)), - - /** Prop to represent the size of the avatar images to be shown */ - avatarSize: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)), - - /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction: PropTypes.func, - - /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ - shouldBlockSelection: PropTypes.bool, - - /** The ref to the menu item */ - forwardedRef: refPropTypes, - - /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle: PropTypes.arrayOf(PropTypes.object), - - /** Text to display under the main item */ - furtherDetails: PropTypes.string, - - /** An icon to display under the main item */ - furtherDetailsIcon: PropTypes.oneOfType([PropTypes.elementType, PropTypes.string]), - - /** The action accept for anonymous user or not */ - isAnonymousAction: PropTypes.bool, - - /** Whether we should use small avatar subscript sizing the for menu item */ - isSmallAvatarSubscriptMenu: PropTypes.bool, - - /** The max number of lines the title text should occupy before ellipses are added */ - numberOfLines: PropTypes.number, - - /** Should we grey out the menu item when it is disabled? */ - shouldGreyOutWhenDisabled: PropTypes.bool, - - /** Error to display below the title */ - error: PropTypes.string, - - /** Should render the content in HTML format */ - shouldRenderAsHTML: PropTypes.bool, - - /** Label to be displayed on the right */ - rightLabel: PropTypes.string, - - /** Component to be displayed on the right */ - rightComponent: PropTypes.node, - - /** Should render component on the right */ - shouldShowRightComponent: PropTypes.bool, - - /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: PropTypes.arrayOf(PropTypes.object), - - /** Should check anonymous user in onPress function */ - shouldCheckActionAllowedOnPress: PropTypes.bool, - - shouldPutLeftPaddingWhenNoIcon: PropTypes.bool, - - /** The menu item link or function to get the link */ - link: PropTypes.oneOfType(PropTypes.func, PropTypes.string), - - /** Icon should be displayed in its own color */ - displayInDefaultIconColor: PropTypes.bool, - - /** Is this menu item in the settings pane */ - isPaneMenu: PropTypes.bool, -}; - -export default propTypes; diff --git a/src/components/transactionPropTypes.js b/src/components/transactionPropTypes.js deleted file mode 100644 index f951837503f3..000000000000 --- a/src/components/transactionPropTypes.js +++ /dev/null @@ -1,96 +0,0 @@ -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import {translatableTextPropTypes} from '@libs/Localize'; -import CONST from '@src/CONST'; -import sourcePropTypes from './Image/sourcePropTypes'; - -export default PropTypes.shape({ - /** The transaction id */ - transactionID: PropTypes.string, - - /** The iouReportID associated with the transaction */ - reportID: PropTypes.string, - - /** The original transaction amount */ - amount: PropTypes.number, - - /** The edited transaction amount */ - modifiedAmount: PropTypes.number, - - /** The original created data */ - created: PropTypes.string, - - /** The edited transaction date */ - modifiedCreated: PropTypes.string, - - /** The filename of the associated receipt */ - filename: PropTypes.string, - - /** The original merchant name */ - merchant: PropTypes.string, - - /** The edited merchant name */ - modifiedMerchant: PropTypes.string, - - /** The comment object on the transaction */ - comment: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - /** The text of the comment */ - comment: PropTypes.string, - - /** The waypoints defining the distance expense */ - waypoints: PropTypes.shape({ - /** The latitude of the waypoint */ - lat: PropTypes.number, - - /** The longitude of the waypoint */ - lng: PropTypes.number, - - /** The address of the waypoint */ - address: PropTypes.string, - - /** The name of the waypoint */ - name: PropTypes.string, - }), - }), - ]), - - /** The type of transaction */ - type: PropTypes.oneOf(_.values(CONST.TRANSACTION.TYPE)), - - /** Custom units attached to the transaction */ - customUnits: PropTypes.arrayOf( - PropTypes.shape({ - /** The name of the custom unit */ - name: PropTypes.string, - }), - ), - - /** Selected participants */ - participants: PropTypes.arrayOf( - PropTypes.shape({ - accountID: PropTypes.number, - login: PropTypes.string, - isPolicyExpenseChat: PropTypes.bool, - isOwnPolicyExpenseChat: PropTypes.bool, - selected: PropTypes.bool, - }), - ), - - /** The original currency of the transaction */ - currency: PropTypes.string, - - /** The edited currency of the transaction */ - modifiedCurrency: PropTypes.string, - - /** The receipt object associated with the transaction */ - receipt: PropTypes.shape({ - receiptID: PropTypes.number, - source: PropTypes.oneOfType([PropTypes.number, PropTypes.string, sourcePropTypes]), - state: PropTypes.string, - }), - - /** Server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), -}); diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 5ced8b1a06e3..a5949006501d 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -321,7 +321,6 @@ function getMonthNames(preferredLocale: Locale): string[] { end: new Date(fullYear, 11, 31), // December 31st of the current year }); - // eslint-disable-next-line rulesdir/prefer-underscore-method return monthsArray.map((monthDate) => format(monthDate, CONST.DATE.MONTH_FORMAT)); } @@ -337,7 +336,6 @@ function getDaysOfWeek(preferredLocale: Locale): string[] { const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn}); const daysOfWeek = eachDayOfInterval({start: startOfCurrentWeek, end: endOfCurrentWeek}); - // eslint-disable-next-line rulesdir/prefer-underscore-method return daysOfWeek.map((date) => format(date, 'eeee')); } diff --git a/src/libs/E2E/tests/appStartTimeTest.e2e.ts b/src/libs/E2E/tests/appStartTimeTest.e2e.ts index 5720af8b3641..321fc3773d51 100644 --- a/src/libs/E2E/tests/appStartTimeTest.e2e.ts +++ b/src/libs/E2E/tests/appStartTimeTest.e2e.ts @@ -20,7 +20,7 @@ const test = () => { // collect performance metrics and submit const metrics: PerformanceEntry[] = Performance.getPerformanceMetrics(); - // underscore promises in sequence without for-loop + // promises in sequence without for-loop Promise.all( metrics.map((metric) => E2EClient.submitTestResults({ diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 6ec283f709c0..8da01424418a 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -300,7 +300,6 @@ const FlagCommentStackNavigator = createModalStackNavigator({ - [SCREENS.EDIT_REQUEST.ROOT]: () => require('../../../../pages/EditRequestPage').default as React.ComponentType, [SCREENS.EDIT_REQUEST.REPORT_FIELD]: () => require('../../../../pages/EditReportFieldPage').default as React.ComponentType, }); diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 3964b7dcd074..797e4ed65f7b 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -623,7 +623,6 @@ const config: LinkingOptions['config'] = { }, [SCREENS.RIGHT_MODAL.EDIT_REQUEST]: { screens: { - [SCREENS.EDIT_REQUEST.ROOT]: ROUTES.EDIT_REQUEST.route, [SCREENS.EDIT_REQUEST.REPORT_FIELD]: ROUTES.EDIT_REPORT_FIELD_REQUEST.route, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 60c3aedbc906..a7d5bdb042c2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -590,10 +590,7 @@ type FlagCommentNavigatorParamList = { }; type EditRequestNavigatorParamList = { - [SCREENS.EDIT_REQUEST.ROOT]: { - field: string; - threadReportID: string; - }; + [SCREENS.EDIT_REQUEST.REPORT_FIELD]: undefined; }; type SignInNavigatorParamList = { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 7b0c28b76653..a218534e6b16 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,4 +1,3 @@ -/* eslint-disable rulesdir/prefer-underscore-method */ import Str from 'expensify-common/lib/str'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; diff --git a/src/libs/markAllPolicyReportsAsRead.ts b/src/libs/markAllPolicyReportsAsRead.ts index c3c719b1132e..49001a851cf5 100644 --- a/src/libs/markAllPolicyReportsAsRead.ts +++ b/src/libs/markAllPolicyReportsAsRead.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line you-dont-need-lodash-underscore/each import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report} from '@src/types/onyx'; diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js deleted file mode 100644 index d3941dca044e..000000000000 --- a/src/pages/EditRequestPage.js +++ /dev/null @@ -1,189 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import categoryPropTypes from '@components/categoryPropTypes'; -import ScreenWrapper from '@components/ScreenWrapper'; -import tagPropTypes from '@components/tagPropTypes'; -import transactionPropTypes from '@components/transactionPropTypes'; -import compose from '@libs/compose'; -import * as IOUUtils from '@libs/IOUUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import EditRequestReceiptPage from './EditRequestReceiptPage'; -import EditRequestTagPage from './EditRequestTagPage'; -import reportActionPropTypes from './home/report/reportActionPropTypes'; -import reportPropTypes from './reportPropTypes'; -import {policyPropTypes} from './workspace/withPolicy'; - -const propTypes = { - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** Which field we are editing */ - field: PropTypes.string, - - /** reportID for the "transaction thread" */ - threadReportID: PropTypes.string, - - /** Indicates which tag list index was selected */ - tagIndex: PropTypes.string, - }), - }).isRequired, - - /** Onyx props */ - /** The report object for the thread report */ - report: reportPropTypes, - - /** The policy of the report */ - policy: policyPropTypes.policy, - - /** Collection of categories attached to a policy */ - policyCategories: PropTypes.objectOf(categoryPropTypes), - - /** Collection of tags attached to a policy */ - policyTags: tagPropTypes, - - /** The actions from the parent report */ - parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), - - /** Transaction that stores the request data */ - transaction: transactionPropTypes, -}; - -const defaultProps = { - report: {}, - policy: {}, - policyCategories: {}, - policyTags: {}, - parentReportActions: {}, - transaction: {}, -}; - -function EditRequestPage({report, route, policy, policyCategories, policyTags, parentReportActions, transaction}) { - const parentReportActionID = lodashGet(report, 'parentReportActionID', '0'); - const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {}); - const {tag: transactionTag} = ReportUtils.getTransactionDetails(transaction); - - const fieldToEdit = lodashGet(route, ['params', 'field'], ''); - const tagListIndex = Number(lodashGet(route, ['params', 'tagIndex'], undefined)); - - const tag = TransactionUtils.getTag(transaction, tagListIndex); - const policyTagListName = PolicyUtils.getTagListName(policyTags, tagListIndex); - const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); - - // A flag for verifying that the current report is a sub-report of a workspace chat - const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report); - - // A flag for showing the tags page - const shouldShowTags = useMemo(() => isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)), [isPolicyExpenseChat, policyTagLists, transactionTag]); - - // Decides whether to allow or disallow editing a money request - useEffect(() => { - // Do not dismiss the modal, when a current user can edit this property of the money request. - if (ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, fieldToEdit)) { - return; - } - - // Dismiss the modal when a current user cannot edit a money request. - Navigation.isNavigationReady().then(() => { - Navigation.dismissModal(); - }); - }, [parentReportAction, fieldToEdit]); - - const saveTag = useCallback( - ({tag: newTag}) => { - let updatedTag = newTag; - if (newTag === tag) { - // In case the same tag has been selected, reset the tag. - updatedTag = ''; - } - IOU.updateMoneyRequestTag( - transaction.transactionID, - report.reportID, - IOUUtils.insertTagIntoTransactionTagsString(transactionTag, updatedTag, tagListIndex), - policy, - policyTags, - policyCategories, - ); - Navigation.dismissModal(); - }, - [tag, transaction.transactionID, report.reportID, transactionTag, tagListIndex, policy, policyTags, policyCategories], - ); - - if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAG && shouldShowTags) { - return ( - - ); - } - - if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { - return ( - - ); - } - - return ( - - - - ); -} - -EditRequestPage.displayName = 'EditRequestPage'; -EditRequestPage.propTypes = propTypes; -EditRequestPage.defaultProps = defaultProps; -export default compose( - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.threadReportID}`, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, - }, - policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, - }, - policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, - canEvict: false, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({report, parentReportActions}) => { - const parentReportActionID = lodashGet(report, 'parentReportActionID', '0'); - const parentReportAction = lodashGet(parentReportActions, parentReportActionID); - return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - }), -)(EditRequestPage); diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index a89a0a3014b9..53f547790768 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -1,8 +1,14 @@ +import lodashEvery from 'lodash/every'; import lodashGet from 'lodash/get'; +import lodashIsEqual from 'lodash/isEqual'; +import lodashMap from 'lodash/map'; +import lodashPick from 'lodash/pick'; +import lodashReject from 'lodash/reject'; +import lodashSome from 'lodash/some'; +import lodashValues from 'lodash/values'; import PropTypes from 'prop-types'; import React, {memo, useCallback, useEffect, useMemo} from 'react'; import {useOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import BlockingView from '@components/BlockingViews/BlockingView'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -49,13 +55,13 @@ const propTypes = { ), /** The type of IOU report, i.e. split, request, send, track */ - iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)).isRequired, + iouType: PropTypes.oneOf(lodashValues(CONST.IOU.TYPE)).isRequired, /** The expense type, ie. manual, scan, distance */ - iouRequestType: PropTypes.oneOf(_.values(CONST.IOU.REQUEST_TYPE)).isRequired, + iouRequestType: PropTypes.oneOf(lodashValues(CONST.IOU.REQUEST_TYPE)).isRequired, /** The action of the IOU, i.e. create, split, move */ - action: PropTypes.oneOf(_.values(CONST.IOU.ACTION)), + action: PropTypes.oneOf(lodashValues(CONST.IOU.ACTION)), }; const defaultProps = { @@ -141,21 +147,21 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF newSections.push({ title: translate('common.recents'), data: chatOptions.recentReports, - shouldShow: !_.isEmpty(chatOptions.recentReports), + shouldShow: chatOptions.recentReports.length > 0, }); if (![CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].includes(action)) { newSections.push({ title: translate('common.contacts'), data: chatOptions.personalDetails, - shouldShow: !_.isEmpty(chatOptions.personalDetails), + shouldShow: chatOptions.personalDetails.length > 0, }); } if (chatOptions.userToInvite && !OptionsListUtils.isCurrentUser(chatOptions.userToInvite)) { newSections.push({ title: undefined, - data: _.map([chatOptions.userToInvite], (participant) => { + data: lodashMap([chatOptions.userToInvite], (participant) => { const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }), @@ -190,7 +196,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF (option) => { onParticipantsAdded([ { - ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID'), + ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID'), selected: true, iouType, }, @@ -218,11 +224,11 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF return false; }; - const isOptionInList = _.some(participants, isOptionSelected); + const isOptionInList = lodashSome(participants, isOptionSelected); let newSelectedOptions; if (isOptionInList) { - newSelectedOptions = _.reject(participants, isOptionSelected); + newSelectedOptions = lodashReject(participants, isOptionSelected); } else { newSelectedOptions = [ ...participants, @@ -247,11 +253,11 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF const headerMessage = useMemo( () => OptionsListUtils.getHeaderMessage( - _.get(newChatOptions, 'personalDetails', []).length + _.get(newChatOptions, 'recentReports', []).length !== 0, + lodashGet(newChatOptions, 'personalDetails', []).length + lodashGet(newChatOptions, 'recentReports', []).length !== 0, Boolean(newChatOptions.userToInvite), debouncedSearchTerm.trim(), maxParticipantsReached, - _.some(participants, (participant) => participant.searchText.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), + lodashSome(participants, (participant) => participant.searchText.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), ), [maxParticipantsReached, newChatOptions, participants, debouncedSearchTerm], ); @@ -259,7 +265,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF // Right now you can't split an expense with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants - const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); + const hasPolicyExpenseChatParticipant = lodashSome(participants, (participant) => participant.isPolicyExpenseChat); const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; // canUseP2PDistanceRequests is true if the iouType is track expense, but we don't want to allow splitting distance with track expense yet @@ -342,7 +348,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF ); - const isAllSectionsEmpty = _.every(sections, (section) => section.data.length === 0); + const isAllSectionsEmpty = lodashEvery(sections, (section) => section.data.length === 0); if ( [CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].includes(action) && isAllSectionsEmpty && @@ -379,8 +385,8 @@ MoneyTemporaryForRefactorRequestParticipantsSelector.displayName = 'MoneyTempora export default memo( MoneyTemporaryForRefactorRequestParticipantsSelector, (prevProps, nextProps) => - _.isEqual(prevProps.participants, nextProps.participants) && + lodashIsEqual(prevProps.participants, nextProps.participants) && prevProps.iouRequestType === nextProps.iouRequestType && prevProps.iouType === nextProps.iouType && - _.isEqual(prevProps.betas, nextProps.betas), + lodashIsEqual(prevProps.betas, nextProps.betas), ); diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js deleted file mode 100755 index 58935cf04330..000000000000 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ /dev/null @@ -1,359 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useMemo} from 'react'; -import {useOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import Button from '@components/Button'; -import FormHelpMessage from '@components/FormHelpMessage'; -import {usePersonalDetails} from '@components/OnyxProvider'; -import {useOptionsList} from '@components/OptionListContextProvider'; -import {PressableWithFeedback} from '@components/Pressable'; -import ReferralProgramCTA from '@components/ReferralProgramCTA'; -import SelectCircle from '@components/SelectCircle'; -import SelectionList from '@components/SelectionList'; -import UserListItem from '@components/SelectionList/UserListItem'; -import useDebouncedState from '@hooks/useDebouncedState'; -import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import usePermissions from '@hooks/usePermissions'; -import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -const propTypes = { - /** Callback to request parent modal to go to next step, which should be request */ - navigateToRequest: PropTypes.func.isRequired, - - /** Callback to request parent modal to go to next step, which should be split */ - navigateToSplit: PropTypes.func.isRequired, - - /** Callback to add participants in MoneyRequestModal */ - onAddParticipants: PropTypes.func.isRequired, - - /** Selected participants from MoneyRequestModal with login */ - participants: PropTypes.arrayOf( - PropTypes.shape({ - accountID: PropTypes.number, - login: PropTypes.string, - isPolicyExpenseChat: PropTypes.bool, - isOwnPolicyExpenseChat: PropTypes.bool, - selected: PropTypes.bool, - }), - ), - - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string.isRequired, - - /** Whether the money request is a distance request or not */ - isDistanceRequest: PropTypes.bool, - - /** Whether the screen transition has ended */ - didScreenTransitionEnd: PropTypes.bool, -}; - -const defaultProps = { - participants: [], - isDistanceRequest: false, - didScreenTransitionEnd: false, -}; - -function MoneyRequestParticipantsSelector({participants, navigateToRequest, navigateToSplit, onAddParticipants, iouType, isDistanceRequest, didScreenTransitionEnd}) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const [betas] = useOnyx(ONYXKEYS.BETAS); - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.PAY_SOMEONE : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SUBMIT_EXPENSE; - const {isOffline} = useNetwork(); - const personalDetails = usePersonalDetails(); - const {options, areOptionsInitialized} = useOptionsList({shouldInitialize: didScreenTransitionEnd}); - const {canUseP2PDistanceRequests} = usePermissions(iouType); - - const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; - const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); - - const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; - - const newChatOptions = useMemo(() => { - const chatOptions = OptionsListUtils.getFilteredOptions( - options.reports, - options.personalDetails, - betas, - debouncedSearchTerm, - participants, - CONST.EXPENSIFY_EMAILS, - - // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user - // sees the option to request money from their admin on their own Workspace Chat. - iouType === CONST.IOU.TYPE.REQUEST, - - canUseP2PDistanceRequests || !isDistanceRequest, - false, - {}, - [], - false, - {}, - [], - // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. - // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - canUseP2PDistanceRequests || !isDistanceRequest, - true, - ); - return { - recentReports: chatOptions.recentReports, - personalDetails: chatOptions.personalDetails, - userToInvite: chatOptions.userToInvite, - }; - }, [options.reports, options.personalDetails, betas, debouncedSearchTerm, participants, iouType, canUseP2PDistanceRequests, isDistanceRequest]); - - /** - * Returns the sections needed for the OptionsSelector - * - * @returns {Array} - */ - const sections = useMemo(() => { - const newSections = []; - - const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( - debouncedSearchTerm, - participants, - newChatOptions.recentReports, - newChatOptions.personalDetails, - maxParticipantsReached, - personalDetails, - true, - ); - newSections.push(formatResults.section); - - if (maxParticipantsReached) { - return newSections; - } - - newSections.push({ - title: translate('common.recents'), - data: newChatOptions.recentReports, - shouldShow: !_.isEmpty(newChatOptions.recentReports), - }); - - newSections.push({ - title: translate('common.contacts'), - data: newChatOptions.personalDetails, - shouldShow: !_.isEmpty(newChatOptions.personalDetails), - }); - - if (newChatOptions.userToInvite && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) { - newSections.push({ - title: undefined, - data: _.map([newChatOptions.userToInvite], (participant) => { - const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); - return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); - }), - shouldShow: true, - }); - } - - return newSections; - }, [maxParticipantsReached, newChatOptions.personalDetails, newChatOptions.recentReports, newChatOptions.userToInvite, participants, personalDetails, debouncedSearchTerm, translate]); - - /** - * Adds a single participant to the request - * - * @param {Object} option - */ - const addSingleParticipant = useCallback( - (option) => { - if (participants.length) { - return; - } - onAddParticipants( - [ - { - accountID: option.accountID, - login: option.login, - isPolicyExpenseChat: option.isPolicyExpenseChat, - reportID: option.reportID, - selected: true, - searchText: option.searchText, - }, - ], - false, - ); - navigateToRequest(); - }, - [navigateToRequest, onAddParticipants, participants.length], - ); - - /** - * Removes a selected option from list if already selected. If not already selected add this option to the list. - * @param {Object} option - */ - const addParticipantToSelection = useCallback( - (option) => { - const isOptionSelected = (selectedOption) => { - if (selectedOption.accountID && selectedOption.accountID === option.accountID) { - return true; - } - - if (selectedOption.reportID && selectedOption.reportID === option.reportID) { - return true; - } - - return false; - }; - const isOptionInList = _.some(participants, isOptionSelected); - let newSelectedOptions; - - if (isOptionInList) { - newSelectedOptions = _.reject(participants, isOptionSelected); - } else { - newSelectedOptions = [ - ...participants, - { - accountID: option.accountID, - login: option.login, - isPolicyExpenseChat: option.isPolicyExpenseChat, - reportID: option.reportID, - selected: true, - searchText: option.searchText, - }, - ]; - } - onAddParticipants(newSelectedOptions, newSelectedOptions.length !== 0); - }, - [participants, onAddParticipants], - ); - - const headerMessage = useMemo( - () => - OptionsListUtils.getHeaderMessage( - _.get(newChatOptions, 'personalDetails', []).length + _.get(newChatOptions, 'recentReports', []).length !== 0, - Boolean(newChatOptions.userToInvite), - debouncedSearchTerm.trim(), - maxParticipantsReached, - _.some(participants, (participant) => participant.searchText.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), - ), - [maxParticipantsReached, newChatOptions, participants, debouncedSearchTerm], - ); - - // Right now you can't split a request with a workspace and other additional participants - // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent - // the app from crashing on native when you try to do this, we'll going to show error message if you have a workspace and other participants - const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); - const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; - - // canUseP2PDistanceRequests is true if the iouType is track expense, but we don't want to allow splitting distance with track expense yet - const isAllowedToSplit = (canUseP2PDistanceRequests || !isDistanceRequest) && (iouType !== CONST.IOU.TYPE.SEND || iouType !== CONST.IOU.TYPE.TRACK_EXPENSE); - - const handleConfirmSelection = useCallback( - (keyEvent, option) => { - const shouldAddSingleParticipant = option && !participants.length; - - if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) { - return; - } - - if (shouldAddSingleParticipant) { - addSingleParticipant(option); - return; - } - - navigateToSplit(); - }, - [shouldShowSplitBillErrorMessage, navigateToSplit, addSingleParticipant, participants.length], - ); - - const {isDismissed} = useDismissedReferralBanners({referralContentType}); - - const footerContent = useMemo(() => { - if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) { - return null; - } - return ( - <> - {!isDismissed && ( - - )} - - {shouldShowSplitBillErrorMessage && ( - - )} - - {!!participants.length && ( -