diff --git a/android/app/build.gradle b/android/app/build.gradle index 4bd11d4e5a88..e48845c9ebbf 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -156,8 +156,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001021103 - versionName "1.2.11-3" + versionCode 1001021202 + versionName "1.2.12-2" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index af004baa6d89..f35803fe8682 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.2.11 + 1.2.12 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.11.3 + 1.2.12.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 2abe543d2735..a766da190a7f 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.11 + 1.2.12 CFBundleSignature ???? CFBundleVersion - 1.2.11.3 + 1.2.12.2 diff --git a/package-lock.json b/package-lock.json index be10d925d4ea..6d1d8edb7076 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.11-3", + "version": "1.2.12-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.11-3", + "version": "1.2.12-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8238f27f47f5..e9a6c18b1231 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.11-3", + "version": "1.2.12-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.js b/src/CONST.js index f95292f39ce4..59403775effa 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -88,6 +88,10 @@ const CONST = { }, REGEX: { US_ACCOUNT_NUMBER: /^[0-9]{4,17}$/, + + // If the account number length is from 4 to 13 digits, we show the last 4 digits and hide the rest with X + // If the length is longer than 13 digits, we show the first 6 and last 4 digits, hiding the rest with X + MASKED_US_ACCOUNT_NUMBER: /^[X]{0,9}[0-9]{4}$|^[0-9]{6}[X]{4,7}[0-9]{4}$/, SWIFT_BIC: /^[A-Za-z0-9]{8,11}$/, }, VERIFICATION_MAX_ATTEMPTS: 7, @@ -801,6 +805,7 @@ const CONST = { }, BRICK_ROAD_INDICATOR_STATUS: { ERROR: 'error', + INFO: 'info', }, REPORT_DETAILS_MENU_ITEM: { MEMBERS: 'member', diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 70327599ca22..0b1f45006fc2 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -24,9 +24,6 @@ export default { // Stores current date CURRENT_DATE: 'currentDate', - // Currently viewed reportID - CURRENTLY_VIEWED_REPORTID: 'currentlyViewedReportID', - // Credentials to authenticate the user CREDENTIALS: 'credentials', @@ -161,9 +158,6 @@ export default { // Is policy data loading? IS_LOADING_POLICY_DATA: 'isLoadingPolicyData', - // Are we loading the create policy room command - IS_LOADING_CREATE_POLICY_ROOM: 'isLoadingCratePolicyRoom', - // Is Keyboard shortcuts modal open? IS_SHORTCUTS_MODAL_OPEN: 'isShortcutsModalOpen', diff --git a/src/components/BigNumberPad.js b/src/components/BigNumberPad.js index 867b946f25bc..56dc37d5a440 100644 --- a/src/components/BigNumberPad.js +++ b/src/components/BigNumberPad.js @@ -78,7 +78,6 @@ class BigNumberPad extends React.Component { ControlSelection.unblock(); this.props.longPressHandlerStateChanged(false); }} - textSelectable={false} /> ); })} diff --git a/src/components/BlockingViews/FullPageNotFoundView.js b/src/components/BlockingViews/FullPageNotFoundView.js index 6502852afd2a..960428027e14 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.js +++ b/src/components/BlockingViews/FullPageNotFoundView.js @@ -55,7 +55,7 @@ const FullPageNotFoundView = (props) => { onBackButtonPress={props.onBackButtonPress} onCloseButtonPress={() => Navigation.dismissModal()} /> - + { const titleTextStyle = StyleUtils.combineStyles([ styles.popoverMenuText, styles.ml3, + (props.shouldShowBasicTitle ? undefined : styles.textStrong), (props.interactive && props.disabled ? styles.disabledText : undefined), ], props.style); - const descriptionTextStyle = StyleUtils.combineStyles([styles.textLabelSupporting, styles.ml3, styles.mt1, styles.breakAll], props.style); + const descriptionTextStyle = StyleUtils.combineStyles([styles.textLabelSupporting, styles.ml3, styles.breakAll], props.style); return ( { /> )} - - - {props.title} - - {Boolean(props.description) && ( + + {Boolean(props.description) && props.shouldShowDescriptionOnTop && ( + + {props.description} + + )} + {Boolean(props.title) && ( + + {props.title} + + )} + {Boolean(props.description) && !props.shouldShowDescriptionOnTop && ( ( + +); + +MenuItemWithTopDescription.propTypes = propTypes; +MenuItemWithTopDescription.displayName = 'MenuItemWithTopDescription'; + +export default MenuItemWithTopDescription; diff --git a/src/components/ReportActionItem/IOUAction.js b/src/components/ReportActionItem/IOUAction.js index 1baa2305b378..83730269a909 100644 --- a/src/components/ReportActionItem/IOUAction.js +++ b/src/components/ReportActionItem/IOUAction.js @@ -14,7 +14,7 @@ const propTypes = { action: PropTypes.shape(reportActionPropTypes).isRequired, /** The associated chatReport */ - chatReportID: PropTypes.number.isRequired, + chatReportID: PropTypes.string.isRequired, /** Is this IOUACTION the most recent? */ isMostRecentIOUReportAction: PropTypes.bool.isRequired, @@ -47,7 +47,7 @@ const IOUAction = (props) => { {((props.isMostRecentIOUReportAction && Boolean(props.action.originalMessage.IOUReportID)) || (props.action.originalMessage.type === 'pay')) && ( { // Determines the number of content items based on container height - const possibleVisibleContentItems = Math.floor(props.containerHeight / CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT); + const possibleVisibleContentItems = Math.ceil(props.containerHeight / CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT); const skeletonViewLines = []; for (let index = 0; index < possibleVisibleContentItems; index++) { const iconIndex = (index + 1) % 4; diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js index 65f0dbf48d03..7f38175d564e 100644 --- a/src/components/menuItemPropTypes.js +++ b/src/components/menuItemPropTypes.js @@ -34,6 +34,12 @@ const propTypes = { /** 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, @@ -71,7 +77,7 @@ const propTypes = { fallbackIcon: PropTypes.func, /** The type of brick road indicator to show. */ - brickRoadIndicator: PropTypes.oneOf([CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR, '']), + brickRoadIndicator: PropTypes.oneOf([CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR, CONST.BRICK_ROAD_INDICATOR_STATUS.INFO, '']), }; export default propTypes; diff --git a/src/languages/en.js b/src/languages/en.js index eea9ccc81f65..844f1b090c81 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -992,7 +992,6 @@ export default { renamedRoomAction: ({oldName, newName}) => ` renamed this room from ${oldName} to ${newName}`, social: 'social', selectAWorkspace: 'Select a workspace', - growlMessageOnError: 'Unable to create policy room, please check your connection and try again.', growlMessageOnRenameError: 'Unable to rename policy room, please check your connection and try again.', visibilityOptions: { restricted: 'Restricted', diff --git a/src/languages/es.js b/src/languages/es.js index 394010a83425..66d8ceb364c1 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -994,7 +994,6 @@ export default { renamedRoomAction: ({oldName, newName}) => ` cambió el nombre de la sala de ${oldName} a ${newName}`, social: 'social', selectAWorkspace: 'Seleccionar un espacio de trabajo', - growlMessageOnError: 'No se pudo crear el espacio de trabajo, por favor comprueba tu conexión e inténtalo de nuevo.', growlMessageOnRenameError: 'No se pudo cambiar el nomdre del espacio de trabajo, por favor comprueba tu conexión e inténtalo de nuevo.', visibilityOptions: { restricted: 'Restringida', diff --git a/src/libs/Navigation/AppNavigator/MainDrawerNavigator.js b/src/libs/Navigation/AppNavigator/MainDrawerNavigator.js index 5df2edc2178d..66b7ef2e7372 100644 --- a/src/libs/Navigation/AppNavigator/MainDrawerNavigator.js +++ b/src/libs/Navigation/AppNavigator/MainDrawerNavigator.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {Component} from 'react'; import PropTypes from 'prop-types'; import lodashGet from 'lodash/get'; import {withOnyx} from 'react-native-onyx'; @@ -54,31 +54,55 @@ const getInitialReportScreenParams = (reports, ignoreDefaultRooms, policies) => return {reportID: String(reportID)}; }; -const MainDrawerNavigator = (props) => { - const initialParams = getInitialReportScreenParams(props.reports, !Permissions.canUseDefaultRooms(props.betas), props.policies); +class MainDrawerNavigator extends Component { + constructor(props) { + super(props); + this.initialParams = getInitialReportScreenParams(props.reports, !Permissions.canUseDefaultRooms(props.betas), props.policies); + } + + shouldComponentUpdate(nextProps) { + const initialNextParams = getInitialReportScreenParams(nextProps.reports, !Permissions.canUseDefaultRooms(nextProps.betas), nextProps.policies); + if (this.initialParams.reportID === initialNextParams.reportID) { + return false; + } - // Wait until reports are fetched and there is a reportID in initialParams - if (!initialParams.reportID) { - return ; + this.initialParams = initialNextParams; + return true; } - // After the app initializes and reports are available the home navigation is mounted - // This way routing information is updated (if needed) based on the initial report ID resolved. - // This is usually needed after login/create account and re-launches - return ( - } - screens={[ - { - name: SCREENS.REPORT, - component: ReportScreen, - initialParams, - }, - ]} - isMainScreen - /> - ); -}; + render() { + // Wait until reports are fetched and there is a reportID in initialParams + if (!this.initialParams.reportID) { + return ; + } + + // After the app initializes and reports are available the home navigation is mounted + // This way routing information is updated (if needed) based on the initial report ID resolved. + // This is usually needed after login/create account and re-launches + return ( + { + // This state belongs to the drawer so it should always have the ReportScreen as it's initial (and only) route + const reportIDFromRoute = lodashGet(state, ['routes', 0, 'params', 'reportID']); + return ( + + ); + }} + screens={[ + { + name: SCREENS.REPORT, + component: ReportScreen, + initialParams: this.initialParams, + }, + ]} + isMainScreen + /> + ); + } +} MainDrawerNavigator.propTypes = propTypes; MainDrawerNavigator.defaultProps = defaultProps; diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index ab88b8a600c8..ae54d59f7aca 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import lodashGet from 'lodash/get'; import {Keyboard} from 'react-native'; import {DrawerActions, getPathFromState, StackActions} from '@react-navigation/native'; import Onyx from 'react-native-onyx'; @@ -182,6 +183,19 @@ function getActiveRoute() { : ''; } +/** + * @returns {String} + */ +function getReportIDFromRoute() { + if (!navigationRef.current) { + return ''; + } + + const drawerState = lodashGet(navigationRef.current.getState(), ['routes', 0, 'state']); + const reportRoute = lodashGet(drawerState, ['routes', 0]); + return lodashGet(reportRoute, ['params', 'reportID'], ''); +} + /** * Check whether the passed route is currently Active or not. * @@ -230,6 +244,7 @@ export default { setDidTapNotification, isNavigationReady, setIsNavigationReady, + getReportIDFromRoute, isDrawerReady, setIsDrawerReady, isDrawerRoute, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 4ae6da7fbf56..eeae8d804a8f 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -9,6 +9,7 @@ import * as ReportUtils from './ReportUtils'; import * as Localize from './Localize'; import Permissions from './Permissions'; import * as CollectionUtils from './CollectionUtils'; +import Navigation from './Navigation/Navigation'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -22,18 +23,6 @@ Onyx.connect({ callback: val => currentUserLogin = val && val.email, }); -let currentlyViewedReportID; -Onyx.connect({ - key: ONYXKEYS.CURRENTLY_VIEWED_REPORTID, - callback: val => currentlyViewedReportID = val, -}); - -let priorityMode; -Onyx.connect({ - key: ONYXKEYS.NVP_PRIORITY_MODE, - callback: val => priorityMode = val, -}); - let loginList; Onyx.connect({ key: ONYXKEYS.LOGIN_LIST, @@ -111,6 +100,9 @@ function addSMSDomainIfPhoneNumber(login) { */ function getPersonalDetailsForLogins(logins, personalDetails) { const personalDetailsForLogins = {}; + if (!personalDetails) { + return personalDetailsForLogins; + } _.each(logins, (login) => { let personalDetail = personalDetails[login]; if (!personalDetail) { @@ -428,12 +420,11 @@ function isCurrentUser(userDetails) { * * @param {Object} reports * @param {Object} personalDetails - * @param {String} activeReportID * @param {Object} options * @returns {Object} * @private */ -function getOptions(reports, personalDetails, activeReportID, { +function getOptions(reports, personalDetails, { reportActions = {}, betas = [], selectedOptions = [], @@ -451,13 +442,20 @@ function getOptions(reports, personalDetails, activeReportID, { sortPersonalDetailsByAlphaAsc = true, forcePolicyNamePreview = false, }) { - const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; let recentReportOptions = []; let personalDetailsOptions = []; const reportMapForLogins = {}; // Filter out all the reports that shouldn't be displayed - const filteredReports = _.filter(reports, report => ReportUtils.shouldReportBeInOptionList(report, currentlyViewedReportID, isInGSDMode, currentUserLogin, iouReports, betas, policies)); + const filteredReports = _.filter(reports, report => ReportUtils.shouldReportBeInOptionList( + report, + Navigation.getReportIDFromRoute(), + false, + currentUserLogin, + iouReports, + betas, + policies, + )); // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) @@ -639,7 +637,7 @@ function getSearchOptions( searchValue = '', betas, ) { - return getOptions(reports, personalDetails, 0, { + return getOptions(reports, personalDetails, { betas, searchValue: searchValue.trim(), includeRecentReports: true, @@ -706,7 +704,7 @@ function getNewChatOptions( selectedOptions = [], excludeLogins = [], ) { - return getOptions(reports, personalDetails, 0, { + return getOptions(reports, personalDetails, { betas, searchValue: searchValue.trim(), selectedOptions, @@ -733,7 +731,7 @@ function getMemberInviteOptions( searchValue = '', excludeLogins = [], ) { - return getOptions([], personalDetails, 0, { + return getOptions([], personalDetails, { betas, searchValue: searchValue.trim(), excludeDefaultRooms: true, diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index d78ca432f6fe..8f237cdc4efc 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -865,12 +865,12 @@ function isUnread(report) { * @param {Object} report * @param {String} report.iouReportID * @param {String} currentUserLogin - * @param {Object[]} iouReports - * @param {String} iouReports[].ownerEmail + * @param {Object} iouReports * @returns {boolean} */ + function hasOutstandingIOU(report, currentUserLogin, iouReports) { - if (!report || !report.iouReportID || report.hasOutstandingIOU) { + if (!report || !report.iouReportID || _.isUndefined(report.hasOutstandingIOU)) { return false; } @@ -879,7 +879,11 @@ function hasOutstandingIOU(report, currentUserLogin, iouReports) { return false; } - return iouReport.ownerEmail !== currentUserLogin; + if (iouReport.ownerEmail === currentUserEmail) { + return false; + } + + return report.hasOutstandingIOU; } /** @@ -890,7 +894,7 @@ function hasOutstandingIOU(report, currentUserLogin, iouReports) { * filter out the majority of reports before filtering out very specific minority of reports. * * @param {Object} report - * @param {String} currentlyViewedReportID + * @param {String} reportIDFromRoute * @param {Boolean} isInGSDMode * @param {String} currentUserLogin * @param {Object} iouReports @@ -898,7 +902,7 @@ function hasOutstandingIOU(report, currentUserLogin, iouReports) { * @param {Object} policies * @returns {boolean} */ -function shouldReportBeInOptionList(report, currentlyViewedReportID, isInGSDMode, currentUserLogin, iouReports, betas, policies) { +function shouldReportBeInOptionList(report, reportIDFromRoute, isInGSDMode, currentUserLogin, iouReports, betas, policies) { // Exclude reports that have no data because there wouldn't be anything to show in the option item. // This can happen if data is currently loading from the server or a report is in various stages of being created. if (!report || !report.reportID || !report.participants || _.isEmpty(report.participants)) { @@ -908,16 +912,10 @@ function shouldReportBeInOptionList(report, currentlyViewedReportID, isInGSDMode // Include the currently viewed report. If we excluded the currently viewed report, then there // would be no way to highlight it in the options list and it would be confusing to users because they lose // a sense of context. - if (report.reportID === currentlyViewedReportID) { + if (report.reportID === reportIDFromRoute) { return true; } - // Include unread reports when in GSD mode - // GSD mode is specifically for focusing the user on the most relevant chats, primarily, the unread ones - if (isInGSDMode) { - return isUnread(report); - } - // 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, currentUserLogin, iouReports)) { @@ -931,12 +929,18 @@ function shouldReportBeInOptionList(report, currentlyViewedReportID, isInGSDMode } // Exclude reports that don't have any comments - // Archived rooms or user created policy rooms are OK to show when the don't have any comments + // User created policy rooms are OK to show when the don't have any comments, only if they aren't archived. const hasNoComments = report.lastMessageTimestamp === 0; - if (hasNoComments && (isArchivedRoom(report) || isUserCreatedPolicyRoom(report))) { + if (hasNoComments && (isArchivedRoom(report) || !isUserCreatedPolicyRoom(report))) { return false; } + // Include unread reports when in GSD mode + // GSD mode is specifically for focusing the user on the most relevant chats, primarily, the unread ones + if (isInGSDMode) { + return isUnread(report); + } + // Include default rooms for free plan policies if (isDefaultRoom(report) && getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE) { return true; @@ -979,6 +983,7 @@ export { isConciergeChatReport, hasExpensifyEmails, hasExpensifyGuidesEmails, + hasOutstandingIOU, canShowReportRecipientLocalTime, formatReportLastMessageText, chatIncludesConcierge, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index a73755e0fb61..e9365750daf1 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -12,7 +12,7 @@ import * as CollectionUtils from './CollectionUtils'; // keys that are connected to SidebarLinks withOnyx(). If there was a key missing from SidebarLinks and it's data was updated // for that key, then there would be no re-render and the options wouldn't reflect the new data because SidebarUtils.getOrderedReportIDs() wouldn't be triggered. // There are a couple of keys here which are OK to have stale data. iouReports for example, doesn't need to exist in withOnyx() because -// when IOUs change, it also triggers a change on the reports collection. Having redudant subscriptions causes more re-renders which should be avoided. +// when IOUs change, it also triggers a change on the reports collection. Having redundant subscriptions causes more re-renders which should be avoided. // Session also can remain stale because the only way for the current user to change is to sign out and sign in, which would clear out all the Onyx // data anyway and cause SidebarLinks to rerender. @@ -29,12 +29,6 @@ Onyx.connect({ callback: val => personalDetails = val, }); -let currentlyViewedReportID; -Onyx.connect({ - key: ONYXKEYS.CURRENTLY_VIEWED_REPORTID, - callback: val => currentlyViewedReportID = val, -}); - let priorityMode; Onyx.connect({ key: ONYXKEYS.NVP_PRIORITY_MODE, @@ -88,9 +82,10 @@ Onyx.connect({ }); /** + * @param {String} reportIDFromRoute * @returns {String[]} An array of reportIDs sorted in the proper order */ -function getOrderedReportIDs() { +function getOrderedReportIDs(reportIDFromRoute) { let recentReportOptions = []; const pinnedReportOptions = []; const iouDebtReportOptions = []; @@ -100,7 +95,7 @@ function getOrderedReportIDs() { const isInDefaultMode = !isInGSDMode; // Filter out all the reports that shouldn't be displayed - const filteredReports = _.filter(reports, report => ReportUtils.shouldReportBeInOptionList(report, currentlyViewedReportID, isInGSDMode, currentUserLogin, iouReports, betas, policies)); + const filteredReports = _.filter(reports, report => ReportUtils.shouldReportBeInOptionList(report, reportIDFromRoute, isInGSDMode, currentUserLogin, iouReports, betas, policies)); // Get all the display names for our reports in an easy to access property so we don't have to keep // re-running the logic @@ -142,7 +137,7 @@ function getOrderedReportIDs() { // If the active report has a draft, we do not put it in the group of draft reports because we want it to maintain it's current position. Otherwise the report's position // jumps around in the LHN and it's kind of confusing to the user to see the LHN reorder when they start typing a comment on a report. - } else if (report.hasDraft && report.reportID !== currentlyViewedReportID) { + } else if (report.hasDraft && report.reportID !== reportIDFromRoute) { draftReportOptions.push(report); } else { recentReportOptions.push(report); diff --git a/src/libs/UnreadIndicatorUpdater/index.js b/src/libs/UnreadIndicatorUpdater/index.js index e4b21ddea4be..4cf82b19d376 100644 --- a/src/libs/UnreadIndicatorUpdater/index.js +++ b/src/libs/UnreadIndicatorUpdater/index.js @@ -7,13 +7,14 @@ import * as ReportUtils from '../ReportUtils'; const reports = {}; /** - * Updates the title and favicon of the current browser tab - * and Mac OS or iOS dock icon with an unread indicator. + * Updates the title and favicon of the current browser tab and Mac OS or iOS dock icon with an unread indicator. + * Note: We are throttling this since listening for report changes can trigger many updates depending on how many reports + * a user has and how often they are updated. */ const throttledUpdatePageTitleAndUnreadCount = _.throttle(() => { const totalCount = _.filter(reports, ReportUtils.isUnread).length; updateUnread(totalCount); -}, 1000, {leading: false}); +}, 100, {leading: false}); let connectionID; diff --git a/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js b/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js index 75f6d3f31d08..c39afe29551b 100644 --- a/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js +++ b/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js @@ -10,6 +10,10 @@ import CONFIG from '../../../CONFIG'; */ function updateUnread(totalCount) { const hasUnread = totalCount !== 0; + + // There is a Chrome browser bug that causes the title to revert back to the previous when we are navigating back. Setting the title to an empty string + // seems to improve this issue. + document.title = ''; document.title = hasUnread ? `(${totalCount}) ${CONFIG.SITE_TITLE}` : CONFIG.SITE_TITLE; document.getElementById('favicon').href = hasUnread ? CONFIG.FAVICON.UNREAD : CONFIG.FAVICON.DEFAULT; } diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index ffcbfdc7b443..6af4d574bcbb 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -25,6 +25,13 @@ Onyx.connect({ allPolicies[key] = val; }, }); + +let lastAccessedWorkspacePolicyID = null; +Onyx.connect({ + key: ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, + callback: value => lastAccessedWorkspacePolicyID = value, +}); + let sessionEmail = ''; Onyx.connect({ key: ONYXKEYS.SESSION, @@ -83,6 +90,14 @@ function getSimplifiedPolicyObject(fullPolicyOrPolicySummary, isFromFullPolicy) }; } +/** + * Stores in Onyx the policy ID of the last workspace that was accessed by the user + * @param {String|null} policyID + */ +function updateLastAccessedWorkspace(policyID) { + Onyx.set(ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, policyID); +} + /** * Used to update ALL of the policies at once. If a policy is present locally, but not in the policies object passed here it will be removed. * @param {Object} policyCollection - object of policy key and partial policy object @@ -125,6 +140,11 @@ function deleteWorkspace(policyID) { const failureData = []; const successData = []; API.write('DeleteWorkspace', {policyID}, {optimisticData, successData, failureData}); + + // Reset the lastAccessedWorkspacePolicyID + if (policyID === lastAccessedWorkspacePolicyID) { + updateLastAccessedWorkspace(null); + } } /** @@ -633,14 +653,6 @@ function updateCustomUnitRate(policyID, currentCustomUnitRate, customUnitID, new }, {optimisticData, successData, failureData}); } -/** - * Stores in Onyx the policy ID of the last workspace that was accessed by the user - * @param {String} policyID - */ -function updateLastAccessedWorkspace(policyID) { - Onyx.set(ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, policyID); -} - /** * Removes an error after trying to delete a member * diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 901fad841e7d..fc9b2e5a8518 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -41,12 +41,6 @@ Onyx.connect({ }, }); -let lastViewedReportID; -Onyx.connect({ - key: ONYXKEYS.CURRENTLY_VIEWED_REPORTID, - callback: val => lastViewedReportID = val ? Number(val) : null, -}); - const allReports = {}; let conciergeChatReportID; const typingWatchTimers = {}; @@ -946,13 +940,6 @@ function handleReportChanged(report) { } } -/** - * @param {String} reportID - */ -function updateCurrentlyViewedReportID(reportID) { - Onyx.merge(ONYXKEYS.CURRENTLY_VIEWED_REPORTID, String(reportID)); -} - Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: handleReportChanged, @@ -1205,40 +1192,6 @@ function navigateToConciergeChat() { Navigation.navigate(ROUTES.getReportRoute(conciergeChatReportID)); } -/** - * Creates a policy room, fetches it, and navigates to it. - * @param {String} policyID - * @param {String} reportName - * @param {String} visibility - * @return {Promise} - */ -function createPolicyRoom(policyID, reportName, visibility) { - Onyx.set(ONYXKEYS.IS_LOADING_CREATE_POLICY_ROOM, true); - return DeprecatedAPI.CreatePolicyRoom({policyID, reportName, visibility}) - .then((response) => { - if (response.jsonCode === CONST.JSON_CODE.UNABLE_TO_RETRY) { - Growl.error(Localize.translateLocal('newRoomPage.growlMessageOnError')); - return; - } - - if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { - Growl.error(response.message); - return; - } - - return fetchChatReportsByIDs([response.reportID]); - }) - .then((chatReports) => { - const reportID = lodashGet(_.first(_.values(chatReports)), 'reportID', ''); - if (!reportID) { - Log.error('Unable to grab policy room after creation', reportID); - return; - } - Navigation.navigate(ROUTES.getReportRoute(reportID)); - }) - .finally(() => Onyx.set(ONYXKEYS.IS_LOADING_CREATE_POLICY_ROOM, false)); -} - /** * Add a policy report (workspace room) optimistically and navigate to it. * @@ -1261,7 +1214,7 @@ function addPolicyReport(policy, reportName, visibility) { ); // Onyx.set is used on the optimistic data so that it is present before navigating to the workspace room. With Onyx.merge the workspace room reportID is not present when - // storeCurrentlyViewedReport is called on the ReportScreen, so fetchChatReportsByIDs is called which is unnecessary since the optimistic data will be stored in Onyx. + // fetchReportIfNeeded is called on the ReportScreen, so fetchChatReportsByIDs is called which is unnecessary since the optimistic data will be stored in Onyx. // If there was an error creating the room, then fetchChatReportsByIDs throws an error and the user is navigated away from the report instead of showing the RBR error message. // Therefore, Onyx.set is used instead of Onyx.merge. const optimisticData = [ @@ -1433,7 +1386,7 @@ function viewNewReportAction(reportID, action) { } // If we are currently viewing this report do not show a notification. - if (reportID === lastViewedReportID && Visibility.isVisible()) { + if (reportID === Navigation.getReportIDFromRoute() && Visibility.isVisible()) { Log.info('[LOCAL_NOTIFICATION] No notification because it was a comment for the current report'); return; } @@ -1469,7 +1422,7 @@ Onyx.connect({ initWithStoredValues: false, callback: (actions, key) => { // reportID can be derived from the Onyx key - const reportID = parseInt(key.split('_')[1], 10); + const reportID = key.split('_')[1]; if (!reportID) { return; } @@ -1522,7 +1475,6 @@ export { saveReportComment, broadcastUserIsTyping, togglePinnedState, - updateCurrentlyViewedReportID, editReportComment, saveReportActionDraft, deleteReportComment, @@ -1530,7 +1482,6 @@ export { syncChatAndIOUReports, navigateToConciergeChat, setReportWithDraft, - createPolicyRoom, addPolicyReport, navigateToConciergeChatAndDeletePolicyReport, setIsComposerFullSize, diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 7554ce05a040..4e9ccc88478c 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -30,12 +30,6 @@ Onyx.connect({ }, }); -let currentlyViewedReportID = ''; -Onyx.connect({ - key: ONYXKEYS.CURRENTLY_VIEWED_REPORTID, - callback: val => currentlyViewedReportID = val || '', -}); - /** * Changes a password for a given account * @@ -215,7 +209,6 @@ function setSecondaryLoginAndNavigate(login, password) { */ function validateLogin(accountID, validateCode) { const isLoggedIn = !_.isEmpty(sessionAuthToken); - const redirectRoute = isLoggedIn ? ROUTES.getReportRoute(currentlyViewedReportID) : ROUTES.HOME; Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, isLoading: true}); DeprecatedAPI.ValidateEmail({ @@ -237,7 +230,7 @@ function validateLogin(accountID, validateCode) { } }).finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); - Navigation.navigate(redirectRoute); + Navigation.navigate(ROUTES.HOME); }); } diff --git a/src/libs/deprecatedAPI.js b/src/libs/deprecatedAPI.js index 68caeea5443d..4fc2937560f6 100644 --- a/src/libs/deprecatedAPI.js +++ b/src/libs/deprecatedAPI.js @@ -526,19 +526,6 @@ function GetReportSummaryList(parameters) { return Network.post(commandName, {...parameters, returnValueList: 'reportSummaryList'}); } -/** - * @param {Object} parameters - * @param {String} parameters.policyID - * @param {String} parameters.reportName - * @param {String} parameters.visibility - * @return {Promise} - */ -function CreatePolicyRoom(parameters) { - const commandName = 'CreatePolicyRoom'; - requireParameters(['policyID', 'reportName', 'visibility'], parameters, commandName); - return Network.post(commandName, parameters); -} - /** * Transfer Wallet balance and takes either the bankAccoundID or fundID * @param {Object} parameters @@ -570,7 +557,6 @@ export { ChangePassword, CreateChatReport, CreateLogin, - CreatePolicyRoom, DeleteLogin, Get, GetStatementPDF, diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js index afaa3b1c7851..65f214a8b2c6 100644 --- a/src/pages/ReimbursementAccount/BankAccountManualStep.js +++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js @@ -37,7 +37,10 @@ class BankAccountManualStep extends React.Component { const errorFields = {}; const routingNumber = values.routingNumber && values.routingNumber.trim(); - if (!values.accountNumber || !CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(values.accountNumber.trim())) { + if ( + !values.accountNumber + || (!CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(values.accountNumber.trim()) && !CONST.BANK_ACCOUNT.REGEX.MASKED_US_ACCOUNT_NUMBER.test(values.accountNumber.trim())) + ) { errorFields.accountNumber = this.props.translate('bankAccount.error.accountNumber'); } if (!routingNumber || !CONST.BANK_ACCOUNT.REGEX.SWIFT_BIC.test(routingNumber) || !ValidationUtils.isValidRoutingNumber(routingNumber)) { @@ -91,6 +94,7 @@ class BankAccountManualStep extends React.Component { )} + defaultValue={ReimbursementAccountUtils.getDefaultStateForField(this.props, 'acceptTerms', true)} shouldSaveDraft /> diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index bdf3a608b71f..4cb8dc947ec9 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -115,7 +115,8 @@ class ReportScreen extends React.Component { } componentDidMount() { - this.storeCurrentlyViewedReport(); + this.fetchReportIfNeeded(); + toggleReportActionComposeView(true); this.removeViewportResizeListener = addViewportResizeListener(this.updateViewportOffsetTop); } @@ -123,7 +124,9 @@ class ReportScreen extends React.Component { if (this.props.route.params.reportID === prevProps.route.params.reportID) { return; } - this.storeCurrentlyViewedReport(); + + this.fetchReportIfNeeded(); + toggleReportActionComposeView(true); } componentWillUnmount() { @@ -149,16 +152,9 @@ class ReportScreen extends React.Component { return !getReportID(this.props.route) || isLoadingInitialReportActions || !this.props.report.reportID; } - /** - * Persists the currently viewed report id - */ - storeCurrentlyViewedReport() { + fetchReportIfNeeded() { const reportIDFromPath = getReportID(this.props.route); - // Always reset the state of the composer view when the current reportID changes - toggleReportActionComposeView(true); - Report.updateCurrentlyViewedReportID(reportIDFromPath); - // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If props.report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.js index 109985bdc2bf..96558d6facb8 100644 --- a/src/pages/home/report/ReportActionItemCreated.js +++ b/src/pages/home/report/ReportActionItemCreated.js @@ -15,7 +15,7 @@ import reportPropTypes from '../../reportPropTypes'; const propTypes = { /** The id of the report */ - reportID: PropTypes.number.isRequired, + reportID: PropTypes.string.isRequired, /** The report currently being looked at */ report: reportPropTypes, diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index cfa72719de47..22a95997fab7 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -58,8 +58,8 @@ const propTypes = { avatar: PropTypes.string, }), - /** Currently viewed reportID */ - currentlyViewedReportID: PropTypes.string, + /** Current reportID from the route in react navigation state object */ + reportIDFromRoute: PropTypes.string, /** Whether we are viewing below the responsive breakpoint */ isSmallScreenWidth: PropTypes.bool.isRequired, @@ -77,7 +77,7 @@ const defaultProps = { currentUserPersonalDetails: { avatar: ReportUtils.getDefaultAvatar(), }, - currentlyViewedReportID: '', + reportIDFromRoute: '', priorityMode: CONST.PRIORITY_MODE.DEFAULT, }; @@ -91,7 +91,7 @@ class SidebarLinks extends React.Component { if (_.isEmpty(this.props.personalDetails)) { return null; } - const optionListItems = SidebarUtils.getOrderedReportIDs(); + const optionListItems = SidebarUtils.getOrderedReportIDs(this.props.reportIDFromRoute); return ( option.toString() === this.props.currentlyViewedReportID + option => option.toString() === this.props.reportIDFromRoute ))} onSelectRow={(option) => { Navigation.navigate(ROUTES.getReportRoute(option.reportID)); @@ -177,9 +177,6 @@ export default compose( personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS, }, - currentlyViewedReportID: { - key: ONYXKEYS.CURRENTLY_VIEWED_REPORTID, - }, priorityMode: { key: ONYXKEYS.NVP_PRIORITY_MODE, }, diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js index ff3b97c3f778..e2b92deb3c60 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js @@ -28,6 +28,9 @@ const propTypes = { /* Callback function before the menu is hidden */ onHideCreateMenu: PropTypes.func, + /** reportID in the current navigation state */ + reportIDFromRoute: PropTypes.string, + ...sidebarPropTypes, }; const defaultProps = { @@ -112,6 +115,7 @@ class BaseSidebarScreen extends Component { onAvatarClick={this.navigateToSettings} isSmallScreenWidth={this.props.isSmallScreenWidth} isDrawerOpen={this.props.isDrawerOpen} + reportIDFromRoute={this.props.reportIDFromRoute} /> { let baseSidebarScreen = null; @@ -40,7 +39,6 @@ SidebarScreen.defaultProps = sidebarDefaultProps; SidebarScreen.displayName = 'SidebarScreen'; export default compose( - withNavigation, withLocalize, withWindowDimensions, withOnyx({ diff --git a/src/pages/home/sidebar/SidebarScreen/index.native.js b/src/pages/home/sidebar/SidebarScreen/index.native.js index 2c865c83900d..e2cb2838efe8 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.native.js +++ b/src/pages/home/sidebar/SidebarScreen/index.native.js @@ -6,7 +6,6 @@ import withLocalize from '../../../../components/withLocalize'; import ONYXKEYS from '../../../../ONYXKEYS'; import {sidebarPropTypes, sidebarDefaultProps} from './sidebarPropTypes'; import BaseSidebarScreen from './BaseSidebarScreen'; -import withNavigation from '../../../../components/withNavigation'; // eslint-disable-next-line react/jsx-props-no-spreading const SidebarScreen = props => ; @@ -16,7 +15,6 @@ SidebarScreen.defaultProps = sidebarDefaultProps; SidebarScreen.displayName = 'SidebarScreen'; export default compose( - withNavigation, withLocalize, withWindowDimensions, withOnyx({ diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 898840e0ed7f..654e2ce8cc0f 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -56,6 +56,7 @@ class WorkspaceNewRoomPage extends React.Component { }; this.validateAndAddPolicyReport = this.validateAndAddPolicyReport.bind(this); + this.focusRoomNameInput = this.focusRoomNameInput.bind(this); } componentDidMount() { @@ -127,6 +128,14 @@ class WorkspaceNewRoomPage extends React.Component { })); } + focusRoomNameInput() { + if (!this.roomNameInputRef) { + return; + } + + this.roomNameInputRef.focus(); + } + render() { if (!Permissions.canUsePolicyRooms(this.props.betas)) { Log.info('Not showing create Policy Room page since user is not on policy rooms beta'); @@ -141,7 +150,7 @@ class WorkspaceNewRoomPage extends React.Component { })); return ( - + Navigation.dismissModal()} @@ -149,6 +158,7 @@ class WorkspaceNewRoomPage extends React.Component { this.roomNameInputRef = el} policyID={this.state.policyID} errorText={this.state.errors.roomName} onChangeText={roomName => this.clearErrorAndSetValue('roomName', roomName)} diff --git a/src/setup/index.js b/src/setup/index.js index 61d55dba207d..b0d2d7687cc8 100644 --- a/src/setup/index.js +++ b/src/setup/index.js @@ -22,7 +22,6 @@ export default function () { Onyx.init({ keys: ONYXKEYS, safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], - keysToDisableSyncEvents: [ONYXKEYS.CURRENTLY_VIEWED_REPORTID], captureMetrics: Metrics.canCaptureOnyxMetrics(), initialKeyStates: { diff --git a/src/styles/styles.js b/src/styles/styles.js index bba8f24ed62d..330df6e84946 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1122,9 +1122,7 @@ const styles = { }, popoverMenuText: { - fontFamily: fontFamily.GTA_BOLD, fontSize: variables.fontSizeNormal, - fontWeight: fontWeightBold, color: themeColors.heading, maxWidth: 240, }, @@ -1858,6 +1856,10 @@ const styles = { fontSize: 15, }, + blockingViewContainer: { + paddingBottom: variables.contentHeaderHeight, + }, + defaultModalContainer: { backgroundColor: themeColors.componentBG, borderColor: colors.transparent, diff --git a/src/styles/utilities/spacing.js b/src/styles/utilities/spacing.js index 53c220f41220..b638006f59e6 100644 --- a/src/styles/utilities/spacing.js +++ b/src/styles/utilities/spacing.js @@ -353,4 +353,8 @@ export default { pb10Percentage: { paddingBottom: '10%', }, + + gap1: { + gap: 4, + }, }; diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 9531266efe2b..7f160add48a4 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -26,7 +26,8 @@ beforeAll(() => { // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. global.fetch = TestHelper.getGlobalFetchMock(); - jest.setTimeout(15000); + // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App + jest.setTimeout(30000); Linking.setInitialURL('https://new.expensify.com/r/1'); appSetup(); }); diff --git a/tests/unit/LHNFilterTest.js b/tests/unit/LHNFilterTest.js index cd9e4d90add2..4a0e0b91a02c 100644 --- a/tests/unit/LHNFilterTest.js +++ b/tests/unit/LHNFilterTest.js @@ -10,7 +10,6 @@ jest.mock('../../src/libs/Permissions'); const ONYXKEYS = { PERSONAL_DETAILS: 'personalDetails', - CURRENTLY_VIEWED_REPORTID: 'currentlyViewedReportID', NVP_PRIORITY_MODE: 'nvp_priorityMode', SESSION: 'session', BETAS: 'betas', @@ -248,17 +247,16 @@ describe('Sidebar', () => { // Given these combinations of booleans which result in the report being filtered out (not shown). const booleansWhichRemovesInactiveReport = [ - // isUserCreatedPolicyRoom - JSON.stringify([false, false, true, false, false, false, false]), + JSON.stringify([false, false, false, false, false, false, false]), - // isUserCreatedPolicyRoom, isUnread - JSON.stringify([false, false, true, false, true, false, false]), + // isUnread + JSON.stringify([false, false, false, false, true, false, false]), - // isUserCreatedPolicyRoom, hasAddWorkspaceError - JSON.stringify([false, false, true, true, false, false, false]), + // hasAddWorkspaceError, isUnread + JSON.stringify([false, false, false, true, true, false, false]), - // isUserCreatedPolicyRoom, hasAddWorkspaceError, isUnread - JSON.stringify([false, false, true, true, true, false, false]), + // hasAddWorkspaceError + JSON.stringify([false, false, false, true, false, false, false]), // isArchived JSON.stringify([false, true, false, false, false, false, false]), @@ -306,7 +304,7 @@ describe('Sidebar', () => { ...LHNTestUtils.getAdvancedFakeReport(...boolArr), policyID: policy.policyID, }; - const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(report1.reportID); return waitForPromisesToResolve() @@ -314,7 +312,6 @@ describe('Sidebar', () => { .then(() => Onyx.multiSet({ [ONYXKEYS.BETAS]: betas, [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.CURRENTLY_VIEWED_REPORTID]: report1.reportID.toString(), [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, @@ -340,8 +337,6 @@ describe('Sidebar', () => { describe('in #focus mode', () => { it('hides unread chats', () => { - const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); - // Given the sidebar is rendered in #focus mode (hides read chats) // with report 1 and 2 having unread actions const report1 = { @@ -353,6 +348,7 @@ describe('Sidebar', () => { lastReadSequenceNumber: LHNTestUtils.TEST_MAX_SEQUENCE_NUMBER - 1, }; const report3 = LHNTestUtils.getFakeReport(['email5@test.com', 'email6@test.com']); + let sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(report1.reportID); return waitForPromisesToResolve() @@ -360,7 +356,6 @@ describe('Sidebar', () => { .then(() => Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.CURRENTLY_VIEWED_REPORTID]: report1.reportID.toString(), [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -391,7 +386,10 @@ describe('Sidebar', () => { }) // When report 2 becomes the active report - .then(() => Onyx.merge(ONYXKEYS.CURRENTLY_VIEWED_REPORTID, report2.reportID.toString())) + .then(() => { + sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(report2.reportID); + return waitForPromisesToResolve(); + }) // Then report 1 should now disappear .then(() => { @@ -399,5 +397,242 @@ describe('Sidebar', () => { expect(sidebarLinks.queryAllByText(/One, Two/)).toHaveLength(0); }); }); + + it('always shows pinned and draft chats', () => { + // Given a draft report and a pinned report + const draftReport = { + ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com']), + hasDraft: true, + }; + const pinnedReport = { + ...LHNTestUtils.getFakeReport(['email3@test.com', 'email4@test.com']), + isPinned: true, + }; + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(draftReport.reportID); + + return waitForPromisesToResolve() + + // When Onyx is updated to contain that data and the sidebar re-renders + .then(() => Onyx.multiSet({ + [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, + [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, + [`${ONYXKEYS.COLLECTION.REPORT}${draftReport.reportID}`]: draftReport, + [`${ONYXKEYS.COLLECTION.REPORT}${pinnedReport.reportID}`]: pinnedReport, + })) + + // Then both reports are visible + .then(() => { + const displayNames = sidebarLinks.queryAllByA11yLabel('Chat user display names'); + expect(displayNames).toHaveLength(2); + expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('Three, Four'); + expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('One, Two'); + }); + }); + + it('archived rooms are displayed only when they have unread messages', () => { + // Given an archived chat report, an archived default policy room, and an archived user created policy room + const archivedReport = { + ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com']), + statusNum: CONST.REPORT.STATUS.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + }; + const archivedPolicyRoomReport = { + ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com']), + chatType: CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, + statusNum: CONST.REPORT.STATUS.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + }; + const archivedUserCreatedPolicyRoomReport = { + ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com']), + chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, + statusNum: CONST.REPORT.STATUS.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + }; + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); + + return waitForPromisesToResolve() + + // When Onyx is updated to contain that data and the sidebar re-renders + .then(() => Onyx.multiSet({ + [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, + [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, + [`${ONYXKEYS.COLLECTION.REPORT}${archivedReport.reportID}`]: archivedReport, + [`${ONYXKEYS.COLLECTION.REPORT}${archivedPolicyRoomReport.reportID}`]: archivedPolicyRoomReport, + [`${ONYXKEYS.COLLECTION.REPORT}${archivedUserCreatedPolicyRoomReport.reportID}`]: archivedUserCreatedPolicyRoomReport, + })) + + // Then neither reports are visible + .then(() => { + const displayNames = sidebarLinks.queryAllByA11yLabel('Chat user display names'); + expect(displayNames).toHaveLength(0); + }) + + // When they have unread messages + .then(() => Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${archivedReport.reportID}`, { + lastReadSequenceNumber: LHNTestUtils.TEST_MAX_SEQUENCE_NUMBER - 1, + })) + .then(() => Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${archivedPolicyRoomReport.reportID}`, { + lastReadSequenceNumber: LHNTestUtils.TEST_MAX_SEQUENCE_NUMBER - 1, + })) + .then(() => Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${archivedUserCreatedPolicyRoomReport.reportID}`, { + lastReadSequenceNumber: LHNTestUtils.TEST_MAX_SEQUENCE_NUMBER - 1, + })) + + // Then they are all visible + .then(() => { + const displayNames = sidebarLinks.queryAllByA11yLabel('Chat user display names'); + expect(displayNames).toHaveLength(3); + }); + }); + + it('policy rooms are displayed only when they have unread messages', () => { + // Given a default policy room and user created policy room + const policyRoomReport = { + ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com']), + chatType: CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, + }; + const userCreatedPolicyRoomReport = { + ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com']), + chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, + }; + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); + + return waitForPromisesToResolve() + + // When Onyx is updated to contain that data and the sidebar re-renders + .then(() => Onyx.multiSet({ + [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, + [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, + [`${ONYXKEYS.COLLECTION.REPORT}${policyRoomReport.reportID}`]: policyRoomReport, + [`${ONYXKEYS.COLLECTION.REPORT}${userCreatedPolicyRoomReport.reportID}`]: userCreatedPolicyRoomReport, + })) + + // Then neither reports are visible + .then(() => { + const displayNames = sidebarLinks.queryAllByA11yLabel('Chat user display names'); + expect(displayNames).toHaveLength(0); + }) + + // When they both have unread messages + .then(() => Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${policyRoomReport.reportID}`, { + lastReadSequenceNumber: LHNTestUtils.TEST_MAX_SEQUENCE_NUMBER - 1, + })) + .then(() => Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${userCreatedPolicyRoomReport.reportID}`, { + lastReadSequenceNumber: LHNTestUtils.TEST_MAX_SEQUENCE_NUMBER - 1, + })) + + // Then both rooms are visible + .then(() => { + const displayNames = sidebarLinks.queryAllByA11yLabel('Chat user display names'); + expect(displayNames).toHaveLength(2); + }); + }); + }); + + describe('all combinations of hasComments, isArchived, isUserCreatedPolicyRoom, hasAddWorkspaceError, isUnread, isPinned, hasDraft', () => { + // Given a report that is the active report and doesn't change + const report1 = LHNTestUtils.getFakeReport(['email3@test.com', 'email4@test.com']); + + // Given a free policy that doesn't change + const policy = { + name: 'Policy One', + policyID: '1', + type: CONST.POLICY.TYPE.FREE, + }; + + // Given the user is in all betas + const betas = [ + CONST.BETAS.DEFAULT_ROOMS, + CONST.BETAS.POLICY_ROOMS, + CONST.BETAS.POLICY_EXPENSE_CHAT, + ]; + + // Given there are 7 boolean variables tested in the filtering logic: + // 1. hasComments + // 2. isArchived + // 3. isUserCreatedPolicyRoom + // 4. hasAddWorkspaceError + // 5. isUnread + // 6. isPinned + // 7. hasDraft + // There is one setting not represented here, which is hasOutstandingIOU. In order to test that setting, there must be + // additional reports in Onyx, so it's being left out for now. It's identical to the logic for hasDraft and isPinned though. + + // Given these combinations of booleans which result in the report being filtered out (not shown). + const booleansWhichRemovesInactiveReport = [ + JSON.stringify([false, false, false, false, false, false, false]), + JSON.stringify([false, false, false, true, false, false, false]), + JSON.stringify([false, false, false, false, true, false, false]), + JSON.stringify([false, false, false, true, true, false, false]), + JSON.stringify([false, false, true, false, false, false, false]), + JSON.stringify([false, false, true, true, false, false, false]), + JSON.stringify([false, true, false, false, false, false, false]), + JSON.stringify([false, true, false, false, true, false, false]), + JSON.stringify([false, true, false, true, false, false, false]), + JSON.stringify([false, true, false, true, true, false, false]), + JSON.stringify([false, true, true, false, false, false, false]), + JSON.stringify([false, true, true, false, true, false, false]), + JSON.stringify([false, true, true, true, false, false, false]), + JSON.stringify([false, true, true, true, true, false, false]), + JSON.stringify([true, false, false, false, false, false, false]), + JSON.stringify([true, false, false, true, false, false, false]), + JSON.stringify([true, false, true, false, false, false, false]), + JSON.stringify([true, false, true, true, false, false, false]), + JSON.stringify([true, true, false, false, false, false, false]), + JSON.stringify([true, true, false, true, false, false, false]), + JSON.stringify([true, true, true, false, false, false, false]), + JSON.stringify([true, true, true, true, false, false, false]), + ]; + + // When every single combination of those booleans is tested + + // Taken from https://stackoverflow.com/a/39734979/9114791 to generate all possible boolean combinations + const AMOUNT_OF_VARIABLES = 7; + // eslint-disable-next-line no-bitwise + for (let i = 0; i < (1 << AMOUNT_OF_VARIABLES); i++) { + const boolArr = []; + for (let j = AMOUNT_OF_VARIABLES - 1; j >= 0; j--) { + // eslint-disable-next-line no-bitwise + boolArr.push(Boolean(i & (1 << j))); + } + + // To test a failing set of conditions, comment out the for loop above and then use a hardcoded array + // for the specific case that's failing. You can then debug the code to see why the test is not passing. + // const boolArr = [false, false, false, true, false, false, false]; + + it(`the booleans ${JSON.stringify(boolArr)}`, () => { + const report2 = { + ...LHNTestUtils.getAdvancedFakeReport(...boolArr), + policyID: policy.policyID, + }; + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(report1.reportID); + + return waitForPromisesToResolve() + + // When Onyx is updated to contain that data and the sidebar re-renders + .then(() => Onyx.multiSet({ + [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, + [ONYXKEYS.BETAS]: betas, + [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, + [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, + [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, + [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, + })) + + // Then depending on the outcome, either one or two reports are visible + .then(() => { + if (booleansWhichRemovesInactiveReport.indexOf(JSON.stringify(boolArr)) > -1) { + // Only one report visible + expect(sidebarLinks.queryAllByA11yHint('Navigates to a chat')).toHaveLength(1); + expect(sidebarLinks.queryAllByA11yLabel('Chat user display names')).toHaveLength(1); + const displayNames = sidebarLinks.queryAllByA11yLabel('Chat user display names'); + expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('Three, Four'); + } else { + // Both reports visible + expect(sidebarLinks.queryAllByA11yHint('Navigates to a chat')).toHaveLength(2); + } + }); + }); + } }); }); diff --git a/tests/unit/LHNOrderTest.js b/tests/unit/LHNOrderTest.js index 7f8c0a974ff6..4a7bcf4f6db6 100644 --- a/tests/unit/LHNOrderTest.js +++ b/tests/unit/LHNOrderTest.js @@ -10,7 +10,6 @@ jest.mock('../../src/libs/Permissions'); const ONYXKEYS = { PERSONAL_DETAILS: 'personalDetails', - CURRENTLY_VIEWED_REPORTID: 'currentlyViewedReportID', NVP_PRIORITY_MODE: 'nvp_priorityMode', SESSION: 'session', BETAS: 'betas', @@ -63,10 +62,9 @@ describe('Sidebar', () => { }); it('contains one report when a report is in Onyx', () => { - const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); - // Given a single report const report = LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com']); + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(report.reportID); return waitForPromisesToResolve() @@ -74,7 +72,6 @@ describe('Sidebar', () => { .then(() => Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.CURRENTLY_VIEWED_REPORTID]: report.reportID.toString(), [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, })) @@ -123,8 +120,6 @@ describe('Sidebar', () => { }); it('doesn\'t change the order when adding a draft to the active report', () => { - const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); - // Given three reports in the recently updated order of 3, 2, 1 // And the first report has a draft // And the currently viewed report is the first report @@ -134,15 +129,14 @@ describe('Sidebar', () => { }; const report2 = LHNTestUtils.getFakeReport(['email3@test.com', 'email4@test.com'], 2); const report3 = LHNTestUtils.getFakeReport(['email5@test.com', 'email6@test.com'], 1); - const currentlyViewedReportID = report1.reportID; - + const reportIDFromRoute = report1.reportID; + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(reportIDFromRoute); return waitForPromisesToResolve() // When Onyx is updated with the data and the sidebar re-renders .then(() => Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.CURRENTLY_VIEWED_REPORTID]: currentlyViewedReportID.toString(), [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -196,8 +190,6 @@ describe('Sidebar', () => { }); it('reorders the reports to keep draft reports on top', () => { - const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); - // Given three reports in the recently updated order of 3, 2, 1 // And the second report has a draft // And the currently viewed report is the second report @@ -207,7 +199,8 @@ describe('Sidebar', () => { hasDraft: true, }; const report3 = LHNTestUtils.getFakeReport(['email5@test.com', 'email6@test.com'], 1); - const currentlyViewedReportID = report2.reportID; + const reportIDFromRoute = report2.reportID; + let sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(reportIDFromRoute); return waitForPromisesToResolve() @@ -215,14 +208,18 @@ describe('Sidebar', () => { .then(() => Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.CURRENTLY_VIEWED_REPORTID]: currentlyViewedReportID.toString(), [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, })) // When the currently active chat is switched to report 1 (the one on the bottom) - .then(() => Onyx.merge(ONYXKEYS.CURRENTLY_VIEWED_REPORTID, '1')) + .then(() => { + // The changing of a route itself will re-render the component in the App, but since we are not performing this test + // inside the navigator and it has no access to the routes we need to trigger an update to the SidebarLinks manually. + sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks('1'); + return waitForPromisesToResolve(); + }) // Then the order of the reports should be 2 > 3 > 1 // ^--- (2 goes to the front and pushes 3 down) @@ -302,8 +299,6 @@ describe('Sidebar', () => { }); it('sorts chats by pinned > IOU > draft', () => { - const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); - // Given three reports in the recently updated order of 3, 2, 1 // with the current user set to email9@ (someone not participating in any of the chats) // with a report that has a draft, a report that is pinned, and @@ -331,9 +326,10 @@ describe('Sidebar', () => { currency: 'USD', chatReportID: report3.reportID, }; - report3.iouReportID = iouReport.reportID.toString(); - const currentlyViewedReportID = report2.reportID; + report3.iouReportID = iouReport.reportID; + const reportIDFromRoute = report2.reportID; const currentlyLoggedInUserEmail = 'email9@test.com'; + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(reportIDFromRoute); return waitForPromisesToResolve() @@ -341,7 +337,6 @@ describe('Sidebar', () => { .then(() => Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.CURRENTLY_VIEWED_REPORTID]: currentlyViewedReportID.toString(), [ONYXKEYS.SESSION]: {email: currentlyLoggedInUserEmail}, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, @@ -364,8 +359,6 @@ describe('Sidebar', () => { }); it('alphabetizes all the chats that are pinned', () => { - const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); - // Given three reports in the recently updated order of 3, 2, 1 // and they are all pinned const report1 = { @@ -384,14 +377,13 @@ describe('Sidebar', () => { ...LHNTestUtils.getFakeReport(['email7@test.com', 'email8@test.com'], 0), isPinned: true, }; - + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks('0'); return waitForPromisesToResolve() // When Onyx is updated with the data and the sidebar re-renders .then(() => Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.CURRENTLY_VIEWED_REPORTID]: '0', [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -421,8 +413,6 @@ describe('Sidebar', () => { }); it('alphabetizes all the chats that have drafts', () => { - const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); - // Given three reports in the recently updated order of 3, 2, 1 // and they all have drafts const report1 = { @@ -441,14 +431,13 @@ describe('Sidebar', () => { ...LHNTestUtils.getFakeReport(['email7@test.com', 'email8@test.com'], 0), hasDraft: true, }; - + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks('0'); return waitForPromisesToResolve() // When Onyx is updated with the data and the sidebar re-renders .then(() => Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.CURRENTLY_VIEWED_REPORTID]: '0', [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -478,8 +467,6 @@ describe('Sidebar', () => { }); it('puts archived chats last', () => { - const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); - // Given three reports, with the first report being archived const report1 = { ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com']), @@ -496,7 +483,7 @@ describe('Sidebar', () => { CONST.BETAS.POLICY_ROOMS, CONST.BETAS.POLICY_EXPENSE_CHAT, ]; - + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks('0'); return waitForPromisesToResolve() // When Onyx is updated with the data and the sidebar re-renders @@ -504,7 +491,6 @@ describe('Sidebar', () => { [ONYXKEYS.BETAS]: betas, [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.CURRENTLY_VIEWED_REPORTID]: '0', [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -578,8 +564,6 @@ describe('Sidebar', () => { }); it('puts archived chats last', () => { - const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks(); - // Given three unread reports, with the first report being archived const report1 = { ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com'], 3), @@ -603,7 +587,7 @@ describe('Sidebar', () => { CONST.BETAS.POLICY_ROOMS, CONST.BETAS.POLICY_EXPENSE_CHAT, ]; - + const sidebarLinks = LHNTestUtils.getDefaultRenderedSidebarLinks('0'); return waitForPromisesToResolve() // When Onyx is updated with the data and the sidebar re-renders @@ -611,7 +595,6 @@ describe('Sidebar', () => { [ONYXKEYS.BETAS]: betas, [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.CURRENTLY_VIEWED_REPORTID]: '0', [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 89bfff2b5231..dce82f822c9b 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -283,8 +283,8 @@ describe('OptionsListUtils', () => { // Then the 2 personalDetails that don't have reports should be returned expect(results.personalDetails.length).toBe(2); - // Then all of the reports should be shown, including the one that has no message on them. - expect(results.recentReports.length).toBe(_.size(REPORTS)); + // Then all of the reports should be shown EXCEPT the archived room, including the one that has no message on them. + expect(results.recentReports.length).toBe(_.size(REPORTS) - 1); // When we filter again but provide a searchValue results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, 'spider'); diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index 9fd92655e7fd..53a1553aa951 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -4,6 +4,7 @@ import CONST from '../../src/CONST'; import ONYXKEYS from '../../src/ONYXKEYS'; import * as ReportUtils from '../../src/libs/ReportUtils'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; +import * as LHNTestUtils from '../utils/LHNTestUtils'; const currentUserEmail = 'bjorn@vikings.net'; const participantsPersonalDetails = { @@ -223,4 +224,82 @@ describe('ReportUtils', () => { }); }); }); + + describe('hasOutstandingIOU', () => { + it('returns false when there is no report', () => { + expect(ReportUtils.hasOutstandingIOU()).toBe(false); + }); + it('returns false when the report has no iouReportID', () => { + const report = LHNTestUtils.getFakeReport(); + expect(ReportUtils.hasOutstandingIOU(report)).toBe(false); + }); + it('returns false when there is no iouReports collection', () => { + const report = { + ...LHNTestUtils.getFakeReport(), + iouReportID: '1', + }; + expect(ReportUtils.hasOutstandingIOU(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, undefined, iouReports)).toBe(false); + }); + it('returns false when the matched IOU report does not have an owner email', () => { + const report = { + ...LHNTestUtils.getFakeReport(), + iouReportID: '1', + }; + const iouReports = { + reportIOUs_1: { + reportID: '1', + }, + }; + expect(ReportUtils.hasOutstandingIOU(report, undefined, iouReports)).toBe(false); + }); + it('returns false when the matched IOU report does not have an owner email', () => { + const report = { + ...LHNTestUtils.getFakeReport(), + iouReportID: '1', + }; + const iouReports = { + reportIOUs_1: { + reportID: '1', + ownerEmail: 'a@a.com', + }, + }; + expect(ReportUtils.hasOutstandingIOU(report, 'b@b.com', iouReports)).toBe(false); + }); + it('returns true when the report has an oustanding IOU', () => { + const report = { + ...LHNTestUtils.getFakeReport(), + iouReportID: '1', + hasOutstandingIOU: true, + }; + const iouReports = { + reportIOUs_1: { + reportID: '1', + ownerEmail: 'a@a.com', + }, + }; + expect(ReportUtils.hasOutstandingIOU(report, 'b@b.com', iouReports)).toBe(true); + }); + it('returns false when the report has no oustanding IOU', () => { + const report = { + ...LHNTestUtils.getFakeReport(), + iouReportID: '1', + hasOutstandingIOU: false, + }; + const iouReports = { + reportIOUs_1: { + reportID: '1', + ownerEmail: 'a@a.com', + }, + }; + expect(ReportUtils.hasOutstandingIOU(report, 'b@b.com', iouReports)).toBe(false); + }); + }); }); diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index 49ee672f582d..540085fe1bf3 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -108,7 +108,11 @@ function getAdvancedFakeReport(hasComments, isArchived, isUserCreatedPolicyRoom, }; } -function getDefaultRenderedSidebarLinks() { +/** + * @param {String} [reportIDFromRoute] + * @returns {RenderAPI} + */ +function getDefaultRenderedSidebarLinks(reportIDFromRoute = '') { // An ErrorBoundary needs to be added to the rendering so that any errors that happen while the component // renders are logged to the console. Without an error boundary, Jest only reports the error like "The above error // occurred in your component", except, there is no "above error". It's just swallowed up by Jest somewhere. @@ -148,6 +152,7 @@ function getDefaultRenderedSidebarLinks() { }} onAvatarClick={() => {}} isSmallScreenWidth={false} + reportIDFromRoute={reportIDFromRoute} />