diff --git a/android/app/build.gradle b/android/app/build.gradle index 1cdaa76a4443..85d05699e620 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 1001021000 - versionName "1.2.10-0" + versionCode 1001021001 + versionName "1.2.10-1" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index d442f5fc21cd..297fd9b761fd 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.10.0 + 1.2.10.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 319b0779092c..f9cd7a7185ca 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.10.0 + 1.2.10.1 diff --git a/package-lock.json b/package-lock.json index c3d452344d05..aceddadbd9ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.10-0", + "version": "1.2.10-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.10-0", + "version": "1.2.10-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -67,7 +67,7 @@ "react-native-image-picker": "^4.8.5", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#6b5ab5110dc3ed554f8eafbc38d7d87c17147972", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.15", + "react-native-onyx": "1.0.17", "react-native-pdf": "^6.6.2", "react-native-performance": "^2.0.0", "react-native-permissions": "^3.0.1", @@ -77,7 +77,7 @@ "react-native-render-html": "6.3.1", "react-native-safe-area-context": "^3.1.4", "react-native-screens": "^3.10.1", - "react-native-svg": "^12.1.0", + "react-native-svg": "^12.4.4", "react-native-webview": "^11.17.2", "react-pdf": "5.7.2", "react-plaid-link": "3.3.2", @@ -35515,9 +35515,9 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.15.tgz", - "integrity": "sha512-uIJped+agmOppnCoDcs/w3qFertkLhLHyhmEEBXp0OhzNKuCs01wg7ccYFZxOVv+CtzFbtkLVB1VW3Ty/zYogA==", + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.17.tgz", + "integrity": "sha512-ls2GjURfpBcGnIkwVrg2uuLnTBwd0vrEiUvbMo+GF3k81GAp2flCkVTM7ciAbo155Izk50dm0uXHYq1PIjwTxw==", "dependencies": { "ascii-table": "0.0.9", "lodash": "^4.17.21", @@ -70130,9 +70130,9 @@ } }, "react-native-onyx": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.15.tgz", - "integrity": "sha512-uIJped+agmOppnCoDcs/w3qFertkLhLHyhmEEBXp0OhzNKuCs01wg7ccYFZxOVv+CtzFbtkLVB1VW3Ty/zYogA==", + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.17.tgz", + "integrity": "sha512-ls2GjURfpBcGnIkwVrg2uuLnTBwd0vrEiUvbMo+GF3k81GAp2flCkVTM7ciAbo155Izk50dm0uXHYq1PIjwTxw==", "requires": { "ascii-table": "0.0.9", "lodash": "^4.17.21", diff --git a/package.json b/package.json index 9d25af44e31c..9538790b9437 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.10-0", + "version": "1.2.10-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -94,7 +94,7 @@ "react-native-image-picker": "^4.8.5", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#6b5ab5110dc3ed554f8eafbc38d7d87c17147972", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.15", + "react-native-onyx": "1.0.17", "react-native-pdf": "^6.6.2", "react-native-performance": "^2.0.0", "react-native-permissions": "^3.0.1", @@ -104,7 +104,7 @@ "react-native-render-html": "6.3.1", "react-native-safe-area-context": "^3.1.4", "react-native-screens": "^3.10.1", - "react-native-svg": "^12.1.0", + "react-native-svg": "^12.4.4", "react-native-webview": "^11.17.2", "react-pdf": "5.7.2", "react-plaid-link": "3.3.2", diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 162b459e7384..70327599ca22 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -180,6 +180,7 @@ export default { FORMS: { ADD_DEBIT_CARD_FORM: 'addDebitCardForm', REQUEST_CALL_FORM: 'requestCallForm', + REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount', }, // Whether we should show the compose input or not diff --git a/src/components/BlockingViews/FullPageNotFoundView.js b/src/components/BlockingViews/FullPageNotFoundView.js index 8a4447bb0cdc..6502852afd2a 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.js +++ b/src/components/BlockingViews/FullPageNotFoundView.js @@ -18,10 +18,30 @@ const propTypes = { /** If true, child components are replaced with a blocking "not found" view */ shouldShow: PropTypes.bool, + + /** The key in the translations file to use for the title */ + titleKey: PropTypes.string, + + /** The key in the translations file to use for the subtitle */ + subtitleKey: PropTypes.string, + + /** Whether we should show a back icon */ + shouldShowBackButton: PropTypes.bool, + + /** Whether we should show a close button */ + shouldShowCloseButton: PropTypes.bool, + + /** Method to trigger when pressing the back button of the header */ + onBackButtonPress: PropTypes.func, }; const defaultProps = { shouldShow: false, + titleKey: 'notFound.notHere', + subtitleKey: 'notFound.pageNotFound', + shouldShowBackButton: true, + shouldShowCloseButton: true, + onBackButtonPress: () => Navigation.dismissModal(), }; // eslint-disable-next-line rulesdir/no-negated-variables @@ -30,15 +50,16 @@ const FullPageNotFoundView = (props) => { return ( <> Navigation.dismissModal()} + shouldShowBackButton={props.shouldShowBackButton} + shouldShowCloseButton={props.shouldShowCloseButton} + onBackButtonPress={props.onBackButtonPress} onCloseButtonPress={() => Navigation.dismissModal()} /> diff --git a/src/components/Form.js b/src/components/Form.js index f42101700862..4e084cfb6209 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -6,6 +6,7 @@ import {withOnyx} from 'react-native-onyx'; import compose from '../libs/compose'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import * as FormActions from '../libs/actions/FormActions'; +import * as ErrorUtils from '../libs/ErrorUtils'; import styles from '../styles/styles'; import FormAlertWithSubmitButton from './FormAlertWithSubmitButton'; @@ -16,6 +17,9 @@ const propTypes = { /** Text to be displayed in the submit button */ submitButtonText: PropTypes.string.isRequired, + /** Controls the submit button's visibility */ + isSubmitButtonVisible: PropTypes.bool, + /** Callback to validate the form */ validate: PropTypes.func.isRequired, @@ -32,8 +36,8 @@ const propTypes = { /** Controls the loading state of the form */ isLoading: PropTypes.bool, - /** Server side error message */ - error: PropTypes.string, + /** Server side errors keyed by microtime */ + errors: PropTypes.objectOf(PropTypes.string), }), /** Contains draft values for each input in the form */ @@ -44,9 +48,10 @@ const propTypes = { }; const defaultProps = { + isSubmitButtonVisible: true, formState: { isLoading: false, - error: '', + errors: null, }, draftValues: {}, }; @@ -75,6 +80,11 @@ class Form extends React.Component { this.touchedInputs[inputID] = true; } + getErrorMessage() { + const latestErrorMessage = ErrorUtils.getLatestErrorMessage(this.props.formState); + return this.props.formState.error || (typeof latestErrorMessage === 'string' ? latestErrorMessage : ''); + } + submit() { // Return early if the form is already submitting to avoid duplicate submission if (this.props.formState.isLoading) { @@ -100,7 +110,7 @@ class Form extends React.Component { * @returns {Object} - An object containing the errors for each inputID, e.g. {inputID1: error1, inputID2: error2} */ validate(values) { - FormActions.setErrorMessage(this.props.formID, ''); + FormActions.setErrors(this.props.formID, null); const validationErrors = this.props.validate(values); if (!_.isObject(validationErrors)) { @@ -184,17 +194,19 @@ class Form extends React.Component { > {this.childrenWrapperWithProps(this.props.children)} + {this.props.isSubmitButtonVisible && ( 0 || Boolean(this.props.formState.error)} + isAlertVisible={_.size(this.state.errors) > 0 || Boolean(this.getErrorMessage())} isLoading={this.props.formState.isLoading} - message={this.props.formState.error} + message={this.getErrorMessage()} onSubmit={this.submit} onFixTheErrorsLinkPressed={() => { this.inputRefs[_.first(_.keys(this.state.errors))].focus(); }} containerStyles={[styles.mh0, styles.mt5]} /> + )} diff --git a/src/languages/en.js b/src/languages/en.js index 1b7e384a92dd..9ea22843b1f6 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -525,6 +525,7 @@ export default { iouReportNotFound: 'The payment details you are looking for cannot be found.', notHere: "Hmm... it's not here", pageNotFound: 'That page is nowhere to be found.', + noAccess: 'You don\'t have access to this chat', }, setPasswordPage: { enterPassword: 'Enter a password', @@ -822,6 +823,7 @@ export default { error: { genericAdd: 'There was a problem adding this workspace member.', cannotRemove: 'You cannot remove yourself or the workspace owner.', + genericRemove: 'There was a problem removing that workspace member.', }, }, card: { diff --git a/src/languages/es.js b/src/languages/es.js index 9fee9c421a5a..9676d7b748a2 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -525,6 +525,7 @@ export default { iouReportNotFound: 'Los detalles del pago que estás buscando no se pudieron encontrar.', notHere: 'Hmm… no está aquí', pageNotFound: 'La página que buscas no existe.', + noAccess: 'No tienes acceso a este chat', }, setPasswordPage: { enterPassword: 'Escribe una contraseña', @@ -824,6 +825,7 @@ export default { error: { genericAdd: 'Ha ocurrido un problema al agregar el miembro al espacio de trabajo', cannotRemove: 'No puedes eliminarte ni a ti mismo ni al dueño del espacio de trabajo.', + genericRemove: 'Ha ocurrido un problema al eliminar al miembro del espacio de trabajo.', }, }, card: { diff --git a/src/libs/ActiveClientManager/index.js b/src/libs/ActiveClientManager/index.js index 7f0d4bf0cd91..908a500d6b72 100644 --- a/src/libs/ActiveClientManager/index.js +++ b/src/libs/ActiveClientManager/index.js @@ -1,3 +1,9 @@ +/** + * When you have many tabs in one browser, the data of Onyx is shared between all of them. Since we persist write requests in Onyx, we need to ensure that + * only one tab is processing those saved requests or we would be duplicating data (or creating errors). + * This file ensures exactly that by tracking all the clientIDs connected, storing the most recent one last and it considers that last clientID the "leader". + */ + import _ from 'underscore'; import Onyx from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; @@ -6,38 +12,47 @@ import * as ActiveClients from '../actions/ActiveClients'; const clientID = Str.guid(); const maxClients = 20; - -let activeClients; - -let resolveIsReadyPromise; -const isReadyPromise = new Promise((resolve) => { - resolveIsReadyPromise = resolve; +let activeClients = []; +let resolveSavedSelfPromise; +const savedSelfPromise = new Promise((resolve) => { + resolveSavedSelfPromise = resolve; }); /** + * Determines when the client is ready. We need to wait both till we saved our ID in onyx AND the init method was called * @returns {Promise} */ function isReady() { - return isReadyPromise; + return savedSelfPromise; } Onyx.connect({ key: ONYXKEYS.ACTIVE_CLIENTS, callback: (val) => { - activeClients = !val ? [] : val; - if (activeClients.length >= maxClients) { + activeClients = val; + + // Remove from the beginning of the list any clients that are past the limit, to avoid having thousands of them + let removed = false; + while (activeClients.length >= maxClients) { activeClients.shift(); + removed = true; + } + + // Save the clients back to onyx, if they changed + if (removed) { ActiveClients.setActiveClients(activeClients); } }, }); /** - * Add our client ID to the list of active IDs + * Add our client ID to the list of active IDs. + * We want to ensure we have no duplicates and that the activeClient gets added at the end of the array (see isClientTheLeader) */ function init() { - ActiveClients.addClient(clientID) - .then(resolveIsReadyPromise); + activeClients = _.without(activeClients, clientID); + activeClients.push(clientID); + ActiveClients.setActiveClients(activeClients).then(resolveSavedSelfPromise); } /** diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index a402d67761b1..0b9b62c25916 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -20,7 +20,6 @@ import KeyboardShortcut from '../../KeyboardShortcut'; import Navigation from '../Navigation'; import * as User from '../../actions/User'; import * as Modal from '../../actions/Modal'; -import * as Policy from '../../actions/Policy'; import modalCardStyleInterpolator from './modalCardStyleInterpolator'; import createCustomModalStackNavigator from './createCustomModalStackNavigator'; @@ -100,7 +99,6 @@ class AuthScreens extends React.Component { authEndpoint: `${CONFIG.EXPENSIFY.URL_API_ROOT}api?command=AuthenticatePusher`, }).then(() => { User.subscribeToUserEvents(); - Policy.subscribeToPolicyEvents(); }); // Listen for report changes and fetch some data we need on initialization diff --git a/src/libs/Navigation/AppNavigator/BaseDrawerNavigator.js b/src/libs/Navigation/AppNavigator/BaseDrawerNavigator.js index 96664751686b..26537c58fd1b 100644 --- a/src/libs/Navigation/AppNavigator/BaseDrawerNavigator.js +++ b/src/libs/Navigation/AppNavigator/BaseDrawerNavigator.js @@ -49,6 +49,12 @@ class BaseDrawerNavigator extends Component { }; } + componentDidMount() { + // We need to resolve the isDrawerReady promise so that any pending drawer actions, like direct navigation from OldDot to + // a NewDot report, can happen. + Navigation.setIsDrawerReady(); + } + componentDidUpdate(prevProps) { if (prevProps.isSmallScreenWidth === this.props.isSmallScreenWidth) { return; diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index 30f6b4ca8399..ab88b8a600c8 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -15,6 +15,11 @@ const navigationIsReadyPromise = new Promise((resolve) => { resolveNavigationIsReadyPromise = resolve; }); +let resolveDrawerIsReadyPromise; +const drawerIsReadyPromise = new Promise((resolve) => { + resolveDrawerIsReadyPromise = resolve; +}); + let isLoggedIn = false; Onyx.connect({ key: ONYXKEYS.SESSION, @@ -202,6 +207,17 @@ function setIsNavigationReady() { resolveNavigationIsReadyPromise(); } +/** + * @returns {Promise} + */ +function isDrawerReady() { + return drawerIsReadyPromise; +} + +function setIsDrawerReady() { + resolveDrawerIsReadyPromise(); +} + export default { canNavigate, navigate, @@ -214,6 +230,9 @@ export default { setDidTapNotification, isNavigationReady, setIsNavigationReady, + isDrawerReady, + setIsDrawerReady, + isDrawerRoute, }; export { diff --git a/src/libs/actions/ActiveClients.js b/src/libs/actions/ActiveClients.js index 2a3689b2c099..744944cfef1c 100644 --- a/src/libs/actions/ActiveClients.js +++ b/src/libs/actions/ActiveClients.js @@ -3,20 +3,13 @@ import ONYXKEYS from '../../ONYXKEYS'; /** * @param {Array} activeClients + * @return {Promise} */ function setActiveClients(activeClients) { - Onyx.set(ONYXKEYS.ACTIVE_CLIENTS, activeClients); -} - -/** - * @param {Number} clientID - * @returns {Promise} - */ -function addClient(clientID) { - return Onyx.merge(ONYXKEYS.ACTIVE_CLIENTS, [clientID]); + return Onyx.set(ONYXKEYS.ACTIVE_CLIENTS, activeClients); } export { + // eslint-disable-next-line import/prefer-default-export setActiveClients, - addClient, }; diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index d667e8ee078c..265de152b3d5 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -196,6 +196,18 @@ function setUpPoliciesAndNavigate(session) { return; } if (!isLoggingInAsNewUser && exitTo) { + if (Navigation.isDrawerRoute(exitTo)) { + // The drawer navigation is only created after we have fetched reports from the server. + // Thus, if we use the standard navigation and try to navigate to a drawer route before + // the reports have been fetched, we will fail to navigate. + Navigation.isDrawerReady() + .then(() => { + // We must call dismissModal() to remove the /transition route from history + Navigation.dismissModal(); + Navigation.navigate(exitTo); + }); + return; + } Navigation.isNavigationReady() .then(() => { // We must call dismissModal() to remove the /transition route from history diff --git a/src/libs/actions/FormActions.js b/src/libs/actions/FormActions.js index eb087e673361..188d10b759b7 100644 --- a/src/libs/actions/FormActions.js +++ b/src/libs/actions/FormActions.js @@ -10,10 +10,10 @@ function setIsLoading(formID, isLoading) { /** * @param {String} formID - * @param {String} error + * @param {String} errors */ -function setErrorMessage(formID, error) { - Onyx.merge(formID, {error}); +function setErrors(formID, errors) { + Onyx.merge(formID, {errors}); } /** @@ -26,6 +26,6 @@ function setDraftValues(formID, draftValues) { export { setIsLoading, - setErrorMessage, + setErrors, setDraftValues, }; diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 12efd891f05d..ffcbfdc7b443 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -6,15 +6,11 @@ import Str from 'expensify-common/lib/str'; import * as DeprecatedAPI from '../deprecatedAPI'; import * as API from '../API'; import ONYXKEYS from '../../ONYXKEYS'; -import Growl from '../Growl'; -import CONFIG from '../../CONFIG'; import CONST from '../../CONST'; import * as Localize from '../Localize'; import Navigation from '../Navigation/Navigation'; import ROUTES from '../../ROUTES'; import * as OptionsListUtils from '../OptionsListUtils'; -import * as Report from './Report'; -import * as Pusher from '../Pusher/pusher'; import DateUtils from '../DateUtils'; import * as ReportUtils from '../ReportUtils'; @@ -209,29 +205,21 @@ function removeMembers(members, policyID) { if (members.length === 0) { return; } - - const employeeListUpdate = {}; - _.each(members, login => employeeListUpdate[login] = null); - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, employeeListUpdate); - - // Make the API call to remove a login from the policy - DeprecatedAPI.Policy_Employees_Remove({ + const membersListKey = `${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`; + const optimisticData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: membersListKey, + value: _.object(members, Array(members.length).fill({pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE})), + }]; + const failureData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: membersListKey, + value: _.object(members, Array(members.length).fill({errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal('workspace.people.error.genericRemove')}})), + }]; + API.write('DeleteMembersFromWorkspace', { emailList: members.join(','), policyID, - }) - .then((data) => { - if (data.jsonCode === 200) { - return; - } - - // Rollback removal on failure - _.each(members, login => employeeListUpdate[login] = {}); - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, employeeListUpdate); - - // Show the user feedback that the removal failed - const errorMessage = data.jsonCode === 666 ? data.message : Localize.translateLocal('workspace.people.genericFailureMessage'); - Growl.show(errorMessage, CONST.GROWL.ERROR, 5000); - }); + }, {optimisticData, failureData}); } /** @@ -653,31 +641,6 @@ function updateLastAccessedWorkspace(policyID) { Onyx.set(ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, policyID); } -/** - * Subscribe to public-policyEditor-[policyID] events. - */ -function subscribeToPolicyEvents() { - _.each(allPolicies, (policy) => { - const pusherChannelName = `public-policyEditor-${policy.id}${CONFIG.PUSHER.SUFFIX}`; - Pusher.subscribe(pusherChannelName, 'policyEmployeeRemoved', ({removedEmails, policyExpenseChatIDs, defaultRoomChatIDs}) => { - // Refetch the policy expense chats to update their state and their actions to get the archive reason - if (!_.isEmpty(policyExpenseChatIDs)) { - Report.fetchChatReportsByIDs(policyExpenseChatIDs); - _.each(policyExpenseChatIDs, (reportID) => { - Report.reconnect(reportID); - }); - } - - // Remove the default chats if we are one of the users getting removed - if (removedEmails.includes(sessionEmail) && !_.isEmpty(defaultRoomChatIDs)) { - _.each(defaultRoomChatIDs, (chatID) => { - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatID}`, null); - }); - } - }); - }); -} - /** * Removes an error after trying to delete a member * @@ -974,7 +937,6 @@ export { updateWorkspaceCustomUnit, updateCustomUnitRate, updateLastAccessedWorkspace, - subscribeToPolicyEvents, clearDeleteMemberError, clearAddMemberError, clearDeleteWorkspaceError, diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 9008c209e53f..d9ce4cba8cc7 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -331,14 +331,6 @@ function fetchChatReportsByIDs(chatList, shouldRedirectIfInaccessible = false) { // Fetch the personal details if there are any PersonalDetails.getFromReportParticipants(_.values(simplifiedReports)); return simplifiedReports; - }) - .catch((err) => { - if (err.message !== CONST.REPORT.ERROR.INACCESSIBLE_REPORT) { - return; - } - - // eslint-disable-next-line no-use-before-define - handleInaccessibleReport(); }); } @@ -1086,6 +1078,7 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { isEdited: true, html: htmlForNewComment, text: textForNewComment, + type: originalReportAction.message[0].type, }], }, }; @@ -1212,14 +1205,6 @@ function navigateToConciergeChat() { Navigation.navigate(ROUTES.getReportRoute(conciergeChatReportID)); } -/** - * Handle the navigation when report is inaccessible - */ -function handleInaccessibleReport() { - Growl.error(Localize.translateLocal('notFound.chatYouLookingForCannotBeFound')); - navigateToConciergeChat(); -} - /** * Creates a policy room, fetches it, and navigates to it. * @param {String} policyID @@ -1544,7 +1529,6 @@ export { getSimplifiedIOUReport, syncChatAndIOUReports, navigateToConciergeChat, - handleInaccessibleReport, setReportWithDraft, createPolicyRoom, addPolicyReport, diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js index 5fc49b9565fb..afaa3b1c7851 100644 --- a/src/pages/ReimbursementAccount/BankAccountManualStep.js +++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js @@ -1,4 +1,3 @@ -import _ from 'underscore'; import React from 'react'; import {Image, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -15,9 +14,8 @@ import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize import * as ValidationUtils from '../../libs/ValidationUtils'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; -import * as ReimbursementAccount from '../../libs/actions/ReimbursementAccount'; import exampleCheckImage from './exampleCheckImage'; -import ReimbursementAccountForm from './ReimbursementAccountForm'; +import Form from '../../components/Form'; import * as ReimbursementAccountUtils from '../../libs/ReimbursementAccountUtils'; const propTypes = { @@ -27,69 +25,40 @@ const propTypes = { class BankAccountManualStep extends React.Component { constructor(props) { super(props); - this.submit = this.submit.bind(this); - this.clearErrorAndSetValue = this.clearErrorAndSetValue.bind(this); - this.getErrorText = inputKey => ReimbursementAccountUtils.getErrorText(this.props, this.errorTranslationKeys, inputKey); - this.state = { - acceptTerms: ReimbursementAccountUtils.getDefaultStateForField(props, 'acceptTerms', true), - routingNumber: ReimbursementAccountUtils.getDefaultStateForField(props, 'routingNumber'), - accountNumber: ReimbursementAccountUtils.getDefaultStateForField(props, 'accountNumber'), - }; - - // Map a field to the key of the error's translation - this.errorTranslationKeys = { - routingNumber: 'bankAccount.error.routingNumber', - accountNumber: 'bankAccount.error.accountNumber', - acceptTerms: 'common.error.acceptedTerms', - }; + this.validate = this.validate.bind(this); } /** - * @returns {Boolean} + * @param {Object} values - form input values passed by the Form component + * @returns {Object} */ - validate() { + validate(values) { const errorFields = {}; - const routingNumber = this.state.routingNumber.trim(); + const routingNumber = values.routingNumber && values.routingNumber.trim(); - if (!CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(this.state.accountNumber.trim())) { - errorFields.accountNumber = true; + if (!values.accountNumber || !CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(values.accountNumber.trim())) { + errorFields.accountNumber = this.props.translate('bankAccount.error.accountNumber'); } - if (!CONST.BANK_ACCOUNT.REGEX.SWIFT_BIC.test(routingNumber) || !ValidationUtils.isValidRoutingNumber(routingNumber)) { - errorFields.routingNumber = true; + if (!routingNumber || !CONST.BANK_ACCOUNT.REGEX.SWIFT_BIC.test(routingNumber) || !ValidationUtils.isValidRoutingNumber(routingNumber)) { + errorFields.routingNumber = this.props.translate('bankAccount.error.routingNumber'); } - if (!this.state.acceptTerms) { - errorFields.acceptTerms = true; + if (!values.acceptedTerms) { + errorFields.acceptedTerms = this.props.translate('common.error.acceptedTerms'); } - ReimbursementAccount.setBankAccountFormValidationErrors(errorFields); - - return _.size(errorFields) === 0; + return errorFields; } - submit() { - if (!this.validate()) { - return; - } + submit(values) { BankAccounts.connectBankAccountManually( ReimbursementAccountUtils.getDefaultStateForField(this.props, 'bankAccountID', 0), - this.state.accountNumber, - this.state.routingNumber, + values.accountNumber, + values.routingNumber, ReimbursementAccountUtils.getDefaultStateForField(this.props, 'plaidMask'), ); } - /** - * @param {String} inputKey - * @param {String} value - */ - clearErrorAndSetValue(inputKey, value) { - const newState = {[inputKey]: value}; - this.setState(newState); - ReimbursementAccount.updateReimbursementAccountDraft(newState); - ReimbursementAccountUtils.clearError(this.props, inputKey); - } - render() { const shouldDisableInputs = Boolean(ReimbursementAccountUtils.getDefaultStateForField(this.props, 'bankAccountID')); @@ -104,7 +73,13 @@ class BankAccountManualStep extends React.Component { onBackButtonPress={() => BankAccounts.setBankAccountSubStep(null)} onCloseButtonPress={Navigation.dismissModal} /> - +
{this.props.translate('bankAccount.checkHelpLine')} @@ -114,26 +89,23 @@ class BankAccountManualStep extends React.Component { source={exampleCheckImage(this.props.preferredLocale)} /> this.clearErrorAndSetValue('routingNumber', value)} disabled={shouldDisableInputs} - errorText={this.getErrorText('routingNumber')} + shouldSaveDraft /> this.clearErrorAndSetValue('accountNumber', value)} disabled={shouldDisableInputs} - errorText={this.getErrorText('accountNumber')} + shouldSaveDraft /> this.clearErrorAndSetValue('acceptTerms', value)} + inputID="acceptedTerms" LabelComponent={() => ( @@ -144,9 +116,9 @@ class BankAccountManualStep extends React.Component { )} - errorText={this.getErrorText('acceptTerms')} + shouldSaveDraft /> - + ); } diff --git a/src/pages/ReimbursementAccount/BankAccountPlaidStep.js b/src/pages/ReimbursementAccount/BankAccountPlaidStep.js index f738aecf7d8d..5519c4ef1769 100644 --- a/src/pages/ReimbursementAccount/BankAccountPlaidStep.js +++ b/src/pages/ReimbursementAccount/BankAccountPlaidStep.js @@ -11,8 +11,9 @@ import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import AddPlaidBankAccount from '../../components/AddPlaidBankAccount'; import * as ReimbursementAccount from '../../libs/actions/ReimbursementAccount'; -import ReimbursementAccountForm from './ReimbursementAccountForm'; import * as ReimbursementAccountUtils from '../../libs/ReimbursementAccountUtils'; +import Form from '../../components/Form'; +import styles from '../../styles/styles'; const propTypes = { /** The OAuth URI + stateID needed to re-initialize the PlaidLink after the user logs into their bank */ @@ -69,7 +70,14 @@ class BankAccountPlaidStep extends React.Component { onBackButtonPress={() => BankAccounts.setBankAccountSubStep(null)} onCloseButtonPress={Navigation.dismissModal} /> - +
({})} + onSubmit={this.submit} + submitButtonText={this.props.translate('common.saveAndContinue')} + style={[styles.mh5, styles.flexGrow1]} + isSubmitButtonVisible={!_.isUndefined(this.props.plaidData.selectedPlaidBankAccount)} + > { @@ -81,7 +89,7 @@ class BankAccountPlaidStep extends React.Component { allowDebit bankAccountID={bankAccountID} /> - + ); } diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 91c2dfb5b3e2..bdf3a608b71f 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -29,6 +29,7 @@ import ReportFooter from './report/ReportFooter'; import Banner from '../../components/Banner'; import withLocalize from '../../components/withLocalize'; import reportPropTypes from '../reportPropTypes'; +import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -153,10 +154,6 @@ class ReportScreen extends React.Component { */ storeCurrentlyViewedReport() { const reportIDFromPath = getReportID(this.props.route); - if (_.isNaN(reportIDFromPath)) { - Report.handleInaccessibleReport(); - return; - } // Always reset the state of the composer view when the current reportID changes toggleReportActionComposeView(true); @@ -216,56 +213,66 @@ class ReportScreen extends React.Component { style={[styles.appContent, styles.flex1, {marginTop: this.state.viewportOffsetTop}]} keyboardAvoidingViewBehavior={Platform.OS === 'android' ? '' : 'padding'} > - - Navigation.navigate(ROUTES.HOME)} - /> - - {this.props.accountManagerReportID && ReportUtils.isConciergeChatReport(this.props.report) && this.state.isBannerVisible && ( - - )} - this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} + { + Navigation.navigate(ROUTES.HOME); + }} > - {this.shouldShowLoader() - ? ( - - ) - : ( - - )} - - + + Navigation.navigate(ROUTES.HOME)} + /> + + {this.props.accountManagerReportID && ReportUtils.isConciergeChatReport(this.props.report) && this.state.isBannerVisible && ( + + )} + this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} + > + {this.shouldShowLoader() + ? ( + + ) + : ( + + )} + + + ); } diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 1de2d1759565..0b3f12570340 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -283,14 +283,20 @@ class WorkspaceMembersPage extends React.Component { } render() { - const policyMemberList = _.keys(lodashGet(this.props, 'policyMemberList', {})); - const removableMembers = _.without(policyMemberList, this.props.session.email, this.props.policy.owner); - const data = _.chain(policyMemberList) - .map(email => this.props.personalDetails[email]) - .filter() - .sortBy(person => person.displayName.toLowerCase()) - .map(person => ({...person})) // TODO: here we will add the pendingAction and errors prop - .value(); + const policyMemberList = lodashGet(this.props, 'policyMemberList', {}); + const removableMembers = []; + let data = []; + _.each(policyMemberList, (policyMember, email) => { + if (email !== this.props.session.email && email !== this.props.policy.owner) { + removableMembers.push(email); + } + const details = this.props.personalDetails[email] || {displayName: email, login: email}; + data.push({ + ...policyMember, + ...details, + }); + }); + data = _.sortBy(data, value => value.displayName.toLowerCase()); const policyID = lodashGet(this.props.route, 'params.policyID'); const policyName = lodashGet(this.props.policy, 'name'); diff --git a/src/pages/workspace/withPolicy.js b/src/pages/workspace/withPolicy.js index d9c85a5387ae..a23ffdcbc97a 100644 --- a/src/pages/workspace/withPolicy.js +++ b/src/pages/workspace/withPolicy.js @@ -8,7 +8,6 @@ import CONST from '../../CONST'; import getComponentDisplayName from '../../libs/getComponentDisplayName'; import * as Policy from '../../libs/actions/Policy'; import ONYXKEYS from '../../ONYXKEYS'; -import policyMemberPropType from '../policyMemberPropType'; /** * @param {Object} route @@ -56,9 +55,6 @@ const policyPropTypes = { */ errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), }), - - /** The policy member list for the current route */ - policyMemberList: PropTypes.objectOf(policyMemberPropType), }; const policyDefaultProps = {