diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c3245e9dc200..3585d0f52e40 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -103,7 +103,7 @@ The Contributor+ will copy/paste it into a new comment and complete it after the - [ ] Android / Chrome - [ ] MacOS / Chrome - [ ] MacOS / Desktop -- [ ] I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed) +- [ ] If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack - [ ] I verified proper code patterns were followed (see [Reviewing the code](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md#reviewing-the-code)) - [ ] I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. `toggleReport` and not `onIconClick`). - [ ] I verified that comments were added to code that is not self explanatory diff --git a/.github/actions/javascript/contributorChecklist/action.yml b/.github/actions/javascript/contributorChecklist/action.yml index 16a6dbea0b04..787fac8182ce 100644 --- a/.github/actions/javascript/contributorChecklist/action.yml +++ b/.github/actions/javascript/contributorChecklist/action.yml @@ -4,6 +4,9 @@ inputs: GITHUB_TOKEN: description: Auth token for New Expensify Github required: true + CHECKLIST: + description: The checklist to look for, either 'contributor' or 'contributorPlus' + required: true runs: using: 'node16' main: './index.js' diff --git a/.github/actions/javascript/contributorChecklist/contributorChecklist.js b/.github/actions/javascript/contributorChecklist/contributorChecklist.js index b0c1f9a885d6..62036078d24c 100644 --- a/.github/actions/javascript/contributorChecklist/contributorChecklist.js +++ b/.github/actions/javascript/contributorChecklist/contributorChecklist.js @@ -1,5 +1,6 @@ const core = require('@actions/core'); const github = require('@actions/github'); +const _ = require('underscore'); const GitHubUtils = require('../../../libs/GithubUtils'); /* eslint-disable max-len */ @@ -96,43 +97,44 @@ const completedContributorPlusChecklist = `- [x] I have verified the author chec - [x] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected. - [x] I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.`; +// True if we are validating a contributor checklist, otherwise we are validating a contributor+ checklist +const verifyingContributorChecklist = core.getInput('CHECKLIST', {required: true}) === 'contributor'; const issue = github.context.payload.issue ? github.context.payload.issue.number : github.context.payload.pull_request.number; const combinedData = []; -function printUncheckedItems(result) { - const checklist = result.split('\n'); +function getPullRequestBody() { + return GitHubUtils.octokit.pulls.get({ + owner: GitHubUtils.GITHUB_OWNER, + repo: GitHubUtils.APP_REPO, + pull_number: issue, + }).then(({data: pullRequestComment}) => pullRequestComment.body); +} - checklist.forEach((line) => { - // Provide a search string with the first 30 characters to figure out if the checkbox item is in the checklist - const lineSearchString = line.replace('- [ ] ', '').slice(0, 30); - if (line.includes('- [ ]') && (completedContributorChecklist.includes(lineSearchString) || completedContributorPlusChecklist.includes(lineSearchString))) { - console.log(`Unchecked checklist item: ${line}`); - } - }); +function getAllReviewComments() { + return GitHubUtils.paginate(GitHubUtils.octokit.pulls.listReviews, { + owner: GitHubUtils.GITHUB_OWNER, + repo: GitHubUtils.APP_REPO, + pull_number: issue, + per_page: 100, + }, response => _.map(response.data, review => review.body)); } -// Get all user text from the pull request, review comments, and pull request comments -GitHubUtils.octokit.pulls.get({ - owner: GitHubUtils.GITHUB_OWNER, - repo: GitHubUtils.APP_REPO, - pull_number: issue, -}).then(({data: pullRequestComment}) => { - combinedData.push(pullRequestComment.body); -}).then(() => GitHubUtils.octokit.pulls.listReviews({ - owner: GitHubUtils.GITHUB_OWNER, - repo: GitHubUtils.APP_REPO, - pull_number: issue, -})).then(({data: pullRequestReviewComments}) => { - pullRequestReviewComments.forEach(pullRequestReviewComment => combinedData.push(pullRequestReviewComment.body)); -}) - .then(() => GitHubUtils.octokit.issues.listComments({ +function getAllComments() { + return GitHubUtils.paginate(GitHubUtils.octokit.issues.listComments, { owner: GitHubUtils.GITHUB_OWNER, repo: GitHubUtils.APP_REPO, issue_number: issue, per_page: 100, - })) - .then(({data: pullRequestComments}) => { - pullRequestComments.forEach(pullRequestComment => combinedData.push(pullRequestComment.body)); + }, response => _.map(response.data, comment => comment.body)); +} + +getPullRequestBody() + .then(pullRequestBody => combinedData.push(pullRequestBody)) + .then(() => getAllReviewComments()) + .then(reviewComments => combinedData.push(...reviewComments)) + .then(() => getAllComments()) + .then(comments => combinedData.push(...comments)) + .then(() => { let contributorChecklistComplete = false; let contributorPlusChecklistComplete = false; @@ -143,26 +145,24 @@ GitHubUtils.octokit.pulls.get({ if (comment.includes(completedContributorChecklist.replace(whitespace, ''))) { contributorChecklistComplete = true; - } else if (comment.includes('- [')) { - printUncheckedItems(combinedData[i]); } if (comment.includes(completedContributorPlusChecklist.replace(whitespace, ''))) { contributorPlusChecklistComplete = true; - } else if (comment.includes('- [')) { - printUncheckedItems(combinedData[i]); } } - if (!contributorChecklistComplete) { + if (verifyingContributorChecklist && !contributorChecklistComplete) { + console.log('Make sure you are using the most up to date checklist found here: https://raw.githubusercontent.com/Expensify/App/main/.github/PULL_REQUEST_TEMPLATE.md'); core.setFailed('Contributor checklist is not completely filled out. Please check every box to verify you\'ve thought about the item.'); return; } - if (!contributorPlusChecklistComplete) { - core.setFailed('Contributor plus checklist is not completely filled out. Please check every box to verify you\'ve thought about the item.'); + if (!verifyingContributorChecklist && !contributorPlusChecklistComplete) { + console.log('Make sure you are using the most up to date checklist found here: https://raw.githubusercontent.com/Expensify/App/main/.github/PULL_REQUEST_TEMPLATE.md'); + core.setFailed('Contributor+ checklist is not completely filled out. Please check every box to verify you\'ve thought about the item.'); return; } - console.log('All checklists are complete 🎉'); + console.log(`${verifyingContributorChecklist ? 'Contributor' : 'Contributor+'} checklist is complete 🎉`); }); diff --git a/.github/actions/javascript/contributorChecklist/index.js b/.github/actions/javascript/contributorChecklist/index.js index fb5607e4253d..1041b6906cfd 100644 --- a/.github/actions/javascript/contributorChecklist/index.js +++ b/.github/actions/javascript/contributorChecklist/index.js @@ -10,6 +10,7 @@ module.exports = const core = __nccwpck_require__(2186); const github = __nccwpck_require__(5438); +const _ = __nccwpck_require__(3571); const GitHubUtils = __nccwpck_require__(7999); /* eslint-disable max-len */ @@ -106,43 +107,44 @@ const completedContributorPlusChecklist = `- [x] I have verified the author chec - [x] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected. - [x] I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.`; +// True if we are validating a contributor checklist, otherwise we are validating a contributor+ checklist +const verifyingContributorChecklist = core.getInput('CHECKLIST', {required: true}) === 'contributor'; const issue = github.context.payload.issue ? github.context.payload.issue.number : github.context.payload.pull_request.number; const combinedData = []; -function printUncheckedItems(result) { - const checklist = result.split('\n'); +function getPullRequestBody() { + return GitHubUtils.octokit.pulls.get({ + owner: GitHubUtils.GITHUB_OWNER, + repo: GitHubUtils.APP_REPO, + pull_number: issue, + }).then(({data: pullRequestComment}) => pullRequestComment.body); +} - checklist.forEach((line) => { - // Provide a search string with the first 30 characters to figure out if the checkbox item is in the checklist - const lineSearchString = line.replace('- [ ] ', '').slice(0, 30); - if (line.includes('- [ ]') && (completedContributorChecklist.includes(lineSearchString) || completedContributorPlusChecklist.includes(lineSearchString))) { - console.log(`Unchecked checklist item: ${line}`); - } - }); +function getAllReviewComments() { + return GitHubUtils.paginate(GitHubUtils.octokit.pulls.listReviews, { + owner: GitHubUtils.GITHUB_OWNER, + repo: GitHubUtils.APP_REPO, + pull_number: issue, + per_page: 100, + }, response => _.map(response.data, review => review.body)); } -// Get all user text from the pull request, review comments, and pull request comments -GitHubUtils.octokit.pulls.get({ - owner: GitHubUtils.GITHUB_OWNER, - repo: GitHubUtils.APP_REPO, - pull_number: issue, -}).then(({data: pullRequestComment}) => { - combinedData.push(pullRequestComment.body); -}).then(() => GitHubUtils.octokit.pulls.listReviews({ - owner: GitHubUtils.GITHUB_OWNER, - repo: GitHubUtils.APP_REPO, - pull_number: issue, -})).then(({data: pullRequestReviewComments}) => { - pullRequestReviewComments.forEach(pullRequestReviewComment => combinedData.push(pullRequestReviewComment.body)); -}) - .then(() => GitHubUtils.octokit.issues.listComments({ +function getAllComments() { + return GitHubUtils.paginate(GitHubUtils.octokit.issues.listComments, { owner: GitHubUtils.GITHUB_OWNER, repo: GitHubUtils.APP_REPO, issue_number: issue, per_page: 100, - })) - .then(({data: pullRequestComments}) => { - pullRequestComments.forEach(pullRequestComment => combinedData.push(pullRequestComment.body)); + }, response => _.map(response.data, comment => comment.body)); +} + +getPullRequestBody() + .then(pullRequestBody => combinedData.push(pullRequestBody)) + .then(() => getAllReviewComments()) + .then(reviewComments => combinedData.push(...reviewComments)) + .then(() => getAllComments()) + .then(comments => combinedData.push(...comments)) + .then(() => { let contributorChecklistComplete = false; let contributorPlusChecklistComplete = false; @@ -153,28 +155,26 @@ GitHubUtils.octokit.pulls.get({ if (comment.includes(completedContributorChecklist.replace(whitespace, ''))) { contributorChecklistComplete = true; - } else if (comment.includes('- [')) { - printUncheckedItems(combinedData[i]); } if (comment.includes(completedContributorPlusChecklist.replace(whitespace, ''))) { contributorPlusChecklistComplete = true; - } else if (comment.includes('- [')) { - printUncheckedItems(combinedData[i]); } } - if (!contributorChecklistComplete) { + if (verifyingContributorChecklist && !contributorChecklistComplete) { + console.log('Make sure you are using the most up to date checklist found here: https://raw.githubusercontent.com/Expensify/App/main/.github/PULL_REQUEST_TEMPLATE.md'); core.setFailed('Contributor checklist is not completely filled out. Please check every box to verify you\'ve thought about the item.'); return; } - if (!contributorPlusChecklistComplete) { - core.setFailed('Contributor plus checklist is not completely filled out. Please check every box to verify you\'ve thought about the item.'); + if (!verifyingContributorChecklist && !contributorPlusChecklistComplete) { + console.log('Make sure you are using the most up to date checklist found here: https://raw.githubusercontent.com/Expensify/App/main/.github/PULL_REQUEST_TEMPLATE.md'); + core.setFailed('Contributor+ checklist is not completely filled out. Please check every box to verify you\'ve thought about the item.'); return; } - console.log('All checklists are complete 🎉'); + console.log(`${verifyingContributorChecklist ? 'Contributor' : 'Contributor+'} checklist is complete 🎉`); }); diff --git a/.github/actions/javascript/getPullRequestDetails/getPullRequestDetails.js b/.github/actions/javascript/getPullRequestDetails/getPullRequestDetails.js index cf3f27ee662c..15ee2869a14d 100644 --- a/.github/actions/javascript/getPullRequestDetails/getPullRequestDetails.js +++ b/.github/actions/javascript/getPullRequestDetails/getPullRequestDetails.js @@ -87,9 +87,13 @@ if (pullRequestNumber) { ...DEFAULT_PAYLOAD, state: 'all', }) + .then(({data}) => _.find(data, PR => PR.user.login === user && titleRegex.test(PR.title)).number) + .then(matchingPRNum => GithubUtils.octokit.pulls.get({ + ...DEFAULT_PAYLOAD, + pull_number: matchingPRNum, + })) .then(({data}) => { - const matchingPR = _.find(data, PR => PR.user.login === user && titleRegex.test(PR.title)); - outputMergeCommitHash(matchingPR); - outputMergeActor(matchingPR); + outputMergeCommitHash(data); + outputMergeActor(data); }); } diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index 692751956bbf..b06196149058 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -97,10 +97,14 @@ if (pullRequestNumber) { ...DEFAULT_PAYLOAD, state: 'all', }) + .then(({data}) => _.find(data, PR => PR.user.login === user && titleRegex.test(PR.title)).number) + .then(matchingPRNum => GithubUtils.octokit.pulls.get({ + ...DEFAULT_PAYLOAD, + pull_number: matchingPRNum, + })) .then(({data}) => { - const matchingPR = _.find(data, PR => PR.user.login === user && titleRegex.test(PR.title)); - outputMergeCommitHash(matchingPR); - outputMergeActor(matchingPR); + outputMergeCommitHash(data); + outputMergeActor(data); }); } diff --git a/.github/workflows/contributorChecklists.yml b/.github/workflows/contributorChecklists.yml new file mode 100644 index 000000000000..692ba8944956 --- /dev/null +++ b/.github/workflows/contributorChecklists.yml @@ -0,0 +1,14 @@ +name: Contributor Checklist + +on: pull_request + +jobs: + checklist: + runs-on: ubuntu-latest + if: github.actor != 'OSBotify' && (github.event_name == 'pull_request' && contains(github.event.pull_request.body, '- [')) + steps: + - name: contributorChecklist.js + uses: Expensify/App/.github/actions/javascript/contributorChecklist@andrew-checklist-3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHECKLIST: 'contributor' diff --git a/.github/workflows/contributorPlusChecklists.yml b/.github/workflows/contributorPlusChecklists.yml new file mode 100644 index 000000000000..5acffb511386 --- /dev/null +++ b/.github/workflows/contributorPlusChecklists.yml @@ -0,0 +1,14 @@ +name: Contributor+ Checklist + +on: issue_comment + +jobs: + checklist: + runs-on: ubuntu-latest + if: github.actor != 'OSBotify' && (contains(github.event.issue.pull_request.url, 'http') && github.event_name == 'issue_comment' && contains(github.event.comment.body, '- [')) + steps: + - name: contributorChecklist.js + uses: Expensify/App/.github/actions/javascript/contributorChecklist@andrew-checklist-3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHECKLIST: 'contributorPlus' diff --git a/.github/workflows/testChecklists.yml b/.github/workflows/testChecklists.yml deleted file mode 100644 index a242f4e5f943..000000000000 --- a/.github/workflows/testChecklists.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Contributor Checklist - -on: - issue_comment: - pull_request_review: - pull_request: - types: [opened, edited, synchronize] - -jobs: - checklist: - runs-on: ubuntu-latest - # Only run when a comment, PR, or PR review comment contains a checklist item - if: ${{ github.actor != 'OSBotify' || (github.event.issue.pull_request && github.event_name == 'issue_comment' && contains(github.event.comment.body, '- [') || github.event_name == 'pull_request_review' && contains(github.event.review.body, '- [') || github.event_name == 'pull_request' && contains(github.event.pull_request.body, '- [')) }} - steps: - - name: contributorChecklist.js - uses: Expensify/App/.github/actions/javascript/contributorChecklist@main - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 75f7d621bacc..c0f9e32884d9 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Alternatively, you can also set up debugger using [Flipper](https://fbflipper.co Our React Native Android app now uses the `Hermes` JS engine which requires your browser for remote debugging. These instructions are specific to Chrome since that's what the Hermes documentation provided. 1. Navigate to `chrome://inspect` 2. Use the `Configure...` button to add the Metro server address (typically `localhost:8081`, check your `Metro` output) -3. You should now see a "Hermes React Native" target with an "inspect" link which can be used to bring up a debugger. If you don't see the "inspect" link, make sure the Metro server is running. +3. You should now see a "Hermes React Native" target with an "inspect" link which can be used to bring up a debugger. If you don't see the "inspect" link, make sure the Metro server is running 4. You can now use the Chrome debug tools. See [React Native Debugging Hermes](https://reactnative.dev/docs/hermes#debugging-hermes-using-google-chromes-devtools) ## Web diff --git a/android/app/build.gradle b/android/app/build.gradle index 4dcc674c352d..7e22a8623b0e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001019702 - versionName "1.1.97-2" + versionCode 1001019707 + versionName "1.1.97-7" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 401372f8197f..dbb9df872315 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -172,6 +172,11 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ 'process/browser': require.resolve('process/browser'), }, }, + devServer: { + client: { + overlay: false, + }, + }, }); module.exports = webpackConfig; diff --git a/ios/Certificates.p12.gpg b/ios/Certificates.p12.gpg index 7a7ec35e4dcd..c4a68891f6e4 100644 Binary files a/ios/Certificates.p12.gpg and b/ios/Certificates.p12.gpg differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f2f5c64fe627..289ba96a2413 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.1.97.2 + 1.1.97.7 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index ba7ee1904a98..11549d1d5412 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.1.97.2 + 1.1.97.7 diff --git a/ios/chat_expensify_appstore.mobileprovision.gpg b/ios/chat_expensify_appstore.mobileprovision.gpg index 7dca07c8c0e4..d083c5449b22 100644 Binary files a/ios/chat_expensify_appstore.mobileprovision.gpg and b/ios/chat_expensify_appstore.mobileprovision.gpg differ diff --git a/package-lock.json b/package-lock.json index 9e6999e5eef2..50eaad994c61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.1.97-2", + "version": "1.1.97-7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.1.97-2", + "version": "1.1.97-7", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -130,7 +130,7 @@ "electron-notarize": "^1.2.1", "electron-reloader": "^1.2.1", "eslint": "^7.6.0", - "eslint-config-expensify": "2.0.29", + "eslint-config-expensify": "2.0.30", "eslint-loader": "^4.0.2", "eslint-plugin-jest": "^24.1.0", "eslint-plugin-jsx-a11y": "^6.6.1", @@ -20247,9 +20247,9 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.29", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.29.tgz", - "integrity": "sha512-oUIWZFF+lilWFgPrdAs4V5PjOnQrCAG3rg9t5KrusR8jvYRx6xo/8jn2bXrJp7FMdnx+4gpNAhKNtsuZTsCjag==", + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.30.tgz", + "integrity": "sha512-SmZTvgUXTRmE4PRlSVDP1LgSZzrxZCgRaKJYiGso+oWl7GcB0HADmrS4nARQswfd4SWqRDy8zsY+j7+GuG8f1A==", "dev": true, "dependencies": { "@lwc/eslint-plugin-lwc": "^0.11.0", @@ -57197,9 +57197,9 @@ } }, "eslint-config-expensify": { - "version": "2.0.29", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.29.tgz", - "integrity": "sha512-oUIWZFF+lilWFgPrdAs4V5PjOnQrCAG3rg9t5KrusR8jvYRx6xo/8jn2bXrJp7FMdnx+4gpNAhKNtsuZTsCjag==", + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.30.tgz", + "integrity": "sha512-SmZTvgUXTRmE4PRlSVDP1LgSZzrxZCgRaKJYiGso+oWl7GcB0HADmrS4nARQswfd4SWqRDy8zsY+j7+GuG8f1A==", "dev": true, "requires": { "@lwc/eslint-plugin-lwc": "^0.11.0", diff --git a/package.json b/package.json index 3255f3863795..85672217086e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.1.97-2", + "version": "1.1.97-7", "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.", @@ -157,7 +157,7 @@ "electron-notarize": "^1.2.1", "electron-reloader": "^1.2.1", "eslint": "^7.6.0", - "eslint-config-expensify": "2.0.29", + "eslint-config-expensify": "2.0.30", "eslint-loader": "^4.0.2", "eslint-plugin-jest": "^24.1.0", "eslint-plugin-jsx-a11y": "^6.6.1", diff --git a/src/CONST.js b/src/CONST.js index afc53840880b..949988f248d0 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -268,6 +268,7 @@ const CONST = { MESSAGE: { TYPE: { COMMENT: 'COMMENT', + TEXT: 'TEXT', }, }, TYPE: { @@ -281,6 +282,10 @@ const CONST = { POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', }, + WORKSPACE_CHAT_ROOMS: { + ANNOUNCE: '#announce', + ADMINS: '#admins', + }, STATE_NUM: { OPEN: 0, PROCESSING: 1, @@ -667,6 +672,7 @@ const CONST = { ADMIN: 'admin', }, ROOM_PREFIX: '#', + CUSTOM_UNIT_RATE_BASE_OFFSET: 100, }, CUSTOM_UNITS: { @@ -768,6 +774,7 @@ const CONST = { // There's a limit of 60k characters in Auth - https://github.com/Expensify/Auth/blob/198d59547f71fdee8121325e8bc9241fc9c3236a/auth/lib/Request.h#L28 MAX_COMMENT_LENGTH: 60000, + FORM_CHARACTER_LIMIT: 50, AVATAR_CROP_MODAL: { // The next two constants control what is min and max value of the image crop scale. // Values define in how many times the image can be bigger than its container. @@ -801,6 +808,7 @@ const CONST = { INVITE: 'invite', LEAVE_ROOM: 'leaveRoom', }, + PROFILE_SETTINGS_FORM: 'profileSettingsForm', // These split the maximum decimal value of a signed 64-bit number (9,223,372,036,854,775,807) into parts where none of them are too big to fit into a 32-bit number, so that we can // generate them each with a random number generator with only 32-bits of precision. diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index 1727cc719a19..c450642df1c8 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -67,6 +67,7 @@ const AddPaymentMethodMenu = props => ( AddPaymentMethodMenu.propTypes = propTypes; AddPaymentMethodMenu.defaultProps = defaultProps; +AddPaymentMethodMenu.displayName = 'AddPaymentMethodMenu'; export default compose( withWindowDimensions, diff --git a/src/components/AddressSearch.js b/src/components/AddressSearch.js index 6e0ebb9718ee..1b749cc72be4 100644 --- a/src/components/AddressSearch.js +++ b/src/components/AddressSearch.js @@ -237,6 +237,7 @@ const AddressSearch = (props) => { AddressSearch.propTypes = propTypes; AddressSearch.defaultProps = defaultProps; +AddressSearch.displayName = 'AddressSearch'; export default withLocalize(React.forwardRef((props, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/ArrowKeyFocusManager.js b/src/components/ArrowKeyFocusManager.js index dcb915d46bcd..46cff9371656 100644 --- a/src/components/ArrowKeyFocusManager.js +++ b/src/components/ArrowKeyFocusManager.js @@ -26,7 +26,7 @@ class ArrowKeyFocusManager extends Component { const arrowDownConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_DOWN; this.unsubscribeArrowUpKey = KeyboardShortcut.subscribe(arrowUpConfig.shortcutKey, () => { - if (this.props.maxIndex < 1) { + if (this.props.maxIndex < 0) { return; } @@ -41,7 +41,7 @@ class ArrowKeyFocusManager extends Component { }, arrowUpConfig.descriptionKey, arrowUpConfig.modifiers, true); this.unsubscribeArrowDownKey = KeyboardShortcut.subscribe(arrowDownConfig.shortcutKey, () => { - if (this.props.maxIndex < 1) { + if (this.props.maxIndex < 0) { return; } diff --git a/src/components/CheckboxWithLabel.js b/src/components/CheckboxWithLabel.js index 38074a39342b..6ac3180e174c 100644 --- a/src/components/CheckboxWithLabel.js +++ b/src/components/CheckboxWithLabel.js @@ -127,7 +127,6 @@ class CheckboxWithLabel extends React.Component { CheckboxWithLabel.propTypes = propTypes; CheckboxWithLabel.defaultProps = defaultProps; -CheckboxWithLabel.displayName = 'CheckboxWithLabel'; export default React.forwardRef((props, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index a7c1d0045170..37c951ca84dd 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -3,6 +3,7 @@ import {StyleSheet} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; +import Str from 'expensify-common/lib/str'; import RNTextInput from '../RNTextInput'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; import Growl from '../../libs/Growl'; @@ -130,6 +131,7 @@ class Composer extends React.Component { }, }; this.dragNDropListener = this.dragNDropListener.bind(this); + this.paste = this.paste.bind(this); this.handlePaste = this.handlePaste.bind(this); this.handlePastedHTML = this.handlePastedHTML.bind(this); this.handleWheel = this.handleWheel.bind(this); @@ -233,15 +235,12 @@ class Composer extends React.Component { } /** - * Manually place the pasted HTML into Composer - * - * @param {String} html - pasted HTML + * Set pasted text to clipboard + * @param {String} text */ - handlePastedHTML(html) { - const parser = new ExpensiMark(); - const markdownText = parser.htmlToMarkdown(html); + paste(text) { try { - document.execCommand('insertText', false, markdownText); + document.execCommand('insertText', false, text); this.updateNumberOfLines(); // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view. @@ -251,6 +250,16 @@ class Composer extends React.Component { } catch (e) {} } + /** + * Manually place the pasted HTML into Composer + * + * @param {String} html - pasted HTML + */ + handlePastedHTML(html) { + const parser = new ExpensiMark(); + this.paste(parser.htmlToMarkdown(html)); + } + /** * Check the paste event for an attachment, parse the data and call onPasteFile from props with the selected file, * Otherwise, convert pasted HTML to Markdown and set it on the composer. @@ -258,13 +267,14 @@ class Composer extends React.Component { * @param {ClipboardEvent} event */ handlePaste(event) { + event.preventDefault(); + const {files, types} = event.clipboardData; const TEXT_HTML = 'text/html'; // If paste contains files, then trigger file management if (files.length > 0) { // Prevent the default so we do not post the file name into the text box - event.preventDefault(); this.props.onPasteFile(event.clipboardData.files[0]); return; } @@ -273,7 +283,6 @@ class Composer extends React.Component { if (types.includes(TEXT_HTML)) { const pastedHTML = event.clipboardData.getData(TEXT_HTML); - event.preventDefault(); const domparser = new DOMParser(); const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images; @@ -309,7 +318,11 @@ class Composer extends React.Component { } this.handlePastedHTML(pastedHTML); + return; } + + const plainText = event.clipboardData.getData('text/plain'); + this.paste(Str.htmlDecode(plainText)); } /** diff --git a/src/components/DotIndicatorMessage.js b/src/components/DotIndicatorMessage.js index 2b073627ef51..0ad914db7d78 100644 --- a/src/components/DotIndicatorMessage.js +++ b/src/components/DotIndicatorMessage.js @@ -45,6 +45,10 @@ const DotIndicatorMessage = (props) => { .keys() .sortBy() .map(key => props.messages[key]) + + // Using uniq here since some fields are wrapped by the same OfflineWithFeedback component (e.g. WorkspaceReimburseView) + // and can potentially pass the same error. + .uniq() .value(); return ( @@ -63,6 +67,7 @@ const DotIndicatorMessage = (props) => { DotIndicatorMessage.propTypes = propTypes; DotIndicatorMessage.defaultProps = defaultProps; +DotIndicatorMessage.displayName = 'DotIndicatorMessage'; export default DotIndicatorMessage; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js index 6b911a720aff..59c05e73ed97 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js @@ -1,4 +1,5 @@ import React from 'react'; +import _ from 'underscore'; import withLocalize from '../../../withLocalize'; import htmlRendererPropTypes from '../htmlRendererPropTypes'; import BasePreRenderer from './BasePreRenderer'; @@ -8,6 +9,7 @@ class PreRenderer extends React.Component { super(props); this.scrollNode = this.scrollNode.bind(this); + this.debouncedIsScrollingVertically = _.debounce(this.isScrollingVertically.bind(this), 100, true); } componentDidMount() { @@ -23,6 +25,18 @@ class PreRenderer extends React.Component { .removeEventListener('wheel', this.scrollNode); } + /** + * Check if user is scrolling vertically based on deltaX and deltaY. We debounce this + * method in the constructor to make sure it's called only for the first event. + * @param {WheelEvent} event Wheel event + * @returns {Boolean} true if user is scrolling vertically + */ + isScrollingVertically(event) { + // Mark as vertical scrolling only when absolute value of deltaY is more than the double of absolute + // value of deltaX, so user can use trackpad scroll on the code block horizontally at a wide angle. + return Math.abs(event.deltaY) > (Math.abs(event.deltaX) * 2); + } + /** * Manually scrolls the code block if code block horizontal scrollable, then prevents the event from being passed up to the parent. * @param {Object} event native event @@ -30,10 +44,8 @@ class PreRenderer extends React.Component { scrollNode(event) { const node = this.ref.getScrollableNode(); const horizontalOverflow = node.scrollWidth > node.offsetWidth; - - // Account for vertical scrolling variation when horizontally scrolling via touchpad by checking a large delta. - const isVerticalScrolling = Math.abs(event.deltaY) > 3; // This is for touchpads sensitive - if ((event.currentTarget === node) && horizontalOverflow && !isVerticalScrolling) { + const isScrollingVertically = this.debouncedIsScrollingVertically(event); + if ((event.currentTarget === node) && horizontalOverflow && !isScrollingVertically) { node.scrollLeft += event.deltaX; event.preventDefault(); event.stopPropagation(); @@ -52,6 +64,5 @@ class PreRenderer extends React.Component { } PreRenderer.propTypes = htmlRendererPropTypes; -PreRenderer.displayName = 'PreRenderer'; export default withLocalize(PreRenderer); diff --git a/src/components/HeaderWithCloseButton.js b/src/components/HeaderWithCloseButton.js index 8b83e61ac279..27ab5dbbe890 100755 --- a/src/components/HeaderWithCloseButton.js +++ b/src/components/HeaderWithCloseButton.js @@ -220,7 +220,6 @@ class HeaderWithCloseButton extends Component { HeaderWithCloseButton.propTypes = propTypes; HeaderWithCloseButton.defaultProps = defaultProps; -HeaderWithCloseButton.displayName = 'HeaderWithCloseButton'; export default compose( withLocalize, diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js index 0bd2bb5da688..f29abef842e1 100644 --- a/src/components/MultipleAvatars.js +++ b/src/components/MultipleAvatars.js @@ -101,4 +101,6 @@ const MultipleAvatars = (props) => { MultipleAvatars.defaultProps = defaultProps; MultipleAvatars.propTypes = propTypes; +MultipleAvatars.displayName = 'MultipleAvatars'; + export default memo(MultipleAvatars); diff --git a/src/components/PlaidLink/index.native.js b/src/components/PlaidLink/index.native.js index e30e9072a6f6..2fa8f6260779 100644 --- a/src/components/PlaidLink/index.native.js +++ b/src/components/PlaidLink/index.native.js @@ -32,4 +32,6 @@ const PlaidLink = (props) => { PlaidLink.propTypes = plaidLinkPropTypes; PlaidLink.defaultProps = plaidLinkDefaultProps; +PlaidLink.displayName = 'PlaidLink'; + export default PlaidLink; diff --git a/src/components/PopoverMenu/BasePopoverMenu.js b/src/components/PopoverMenu/BasePopoverMenu.js index 32551ba203d0..52d876c48792 100644 --- a/src/components/PopoverMenu/BasePopoverMenu.js +++ b/src/components/PopoverMenu/BasePopoverMenu.js @@ -10,7 +10,10 @@ import { propTypes as createMenuPropTypes, defaultProps as defaultCreateMenuPropTypes, } from './popoverMenuPropTypes'; +import ArrowKeyFocusManager from '../ArrowKeyFocusManager'; import Text from '../Text'; +import KeyboardShortcut from '../../libs/KeyboardShortcut'; +import CONST from '../../CONST'; const propTypes = { /** Callback fired when the menu is completely closed */ @@ -26,13 +29,71 @@ const defaultProps = { }; class BasePopoverMenu extends PureComponent { + constructor(props) { + super(props); + this.state = { + focusedIndex: -1, + }; + this.updateFocusedIndex = this.updateFocusedIndex.bind(this); + this.resetFocusAndHideModal = this.resetFocusAndHideModal.bind(this); + this.removeKeyboardListener = this.removeKeyboardListener.bind(this); + this.attachKeyboardListener = this.attachKeyboardListener.bind(this); + } + + componentDidUpdate(prevProps) { + if (this.props.isVisible === prevProps.isVisible) { + return; + } + + if (this.props.isVisible) { + this.attachKeyboardListener(); + } else { + this.removeKeyboardListener(); + } + } + + componentWillUnmount() { + this.removeKeyboardListener(); + } + + attachKeyboardListener() { + const shortcutConfig = CONST.KEYBOARD_SHORTCUTS.ENTER; + this.unsubscribeEnterKey = KeyboardShortcut.subscribe(shortcutConfig.shortcutKey, () => { + if (this.state.focusedIndex === -1) { + return; + } + this.props.onItemSelected(this.props.menuItems[this.state.focusedIndex]); + this.updateFocusedIndex(-1); // Reset the focusedIndex on selecting any menu + }, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true); + } + + removeKeyboardListener() { + if (!this.unsubscribeEnterKey) { + return; + } + this.unsubscribeEnterKey(); + } + + /** + * @param {Number} index + */ + updateFocusedIndex(index) { + this.setState({focusedIndex: index}); + } + + resetFocusAndHideModal() { + this.updateFocusedIndex(-1); // Reset the focusedIndex on modal hide + this.removeKeyboardListener(); + this.props.onMenuHide(); + } + render() { return ( )} - {_.map(this.props.menuItems, item => ( - this.props.onItemSelected(item)} - /> - ))} + + {_.map(this.props.menuItems, (item, menuIndex) => ( + this.props.onItemSelected(item)} + focused={this.state.focusedIndex === menuIndex} + /> + ))} + ); diff --git a/src/components/SafeArea/index.ios.js b/src/components/SafeArea/index.ios.js index ead107f90a22..3d84c4620677 100644 --- a/src/components/SafeArea/index.ios.js +++ b/src/components/SafeArea/index.ios.js @@ -13,5 +13,6 @@ SafeArea.propTypes = { /** App content */ children: PropTypes.node.isRequired, }; +SafeArea.displayName = 'SafeArea'; export default SafeArea; diff --git a/src/components/ScreenWrapper/index.android.js b/src/components/ScreenWrapper/index.android.js index aee674422f6b..e4b64a51bb40 100644 --- a/src/components/ScreenWrapper/index.android.js +++ b/src/components/ScreenWrapper/index.android.js @@ -15,5 +15,6 @@ defaultProps.keyboardAvoidingViewBehavior = 'height'; ScreenWrapper.propTypes = propTypes; ScreenWrapper.defaultProps = defaultProps; +ScreenWrapper.displayName = 'ScreenWrapper'; export default ScreenWrapper; diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index 29c537442985..187e75159561 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -12,5 +12,6 @@ const ScreenWrapper = props => ( ); ScreenWrapper.propTypes = propTypes; ScreenWrapper.defaultProps = defaultProps; +ScreenWrapper.displayName = 'ScreenWrapper'; export default ScreenWrapper; diff --git a/src/components/TestToolMenu.js b/src/components/TestToolMenu.js index f04ae6a26b92..1654854799d2 100644 --- a/src/components/TestToolMenu.js +++ b/src/components/TestToolMenu.js @@ -76,6 +76,8 @@ const TestToolMenu = props => ( TestToolMenu.propTypes = propTypes; TestToolMenu.defaultProps = defaultProps; +TestToolMenu.displayName = 'TestToolMenu'; + export default compose( withNetwork(), withOnyx({ diff --git a/src/components/TestToolRow.js b/src/components/TestToolRow.js index 80405a898bb0..505dfefa2172 100644 --- a/src/components/TestToolRow.js +++ b/src/components/TestToolRow.js @@ -26,4 +26,6 @@ const TestToolRow = props => ( ); TestToolRow.propTypes = propTypes; +TestToolRow.displayName = 'TestToolRow'; + export default TestToolRow; diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index df829f7fd89c..eec708d91113 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -14,6 +14,7 @@ import * as Expensicons from '../Icon/Expensicons'; import Text from '../Text'; import * as styleConst from './styleConst'; import * as StyleUtils from '../../styles/StyleUtils'; +import variables from '../../styles/variables'; import Checkbox from '../Checkbox'; import getSecureEntryKeyboardType from '../../libs/getSecureEntryKeyboardType'; import CONST from '../../CONST'; @@ -33,6 +34,7 @@ class BaseTextInput extends Component { textInputWidth: 0, prefixWidth: 0, selection: props.selection, + height: variables.componentSizeLarge, // Value should be kept in state for the autoGrow feature to work - https://github.com/Expensify/App/pull/8232#issuecomment-1077282006 value, @@ -217,6 +219,9 @@ class BaseTextInput extends Component { > !this.props.multiline && this.setState({height: event.nativeEvent.layout.height})} style={[ textInputContainerStyles, @@ -268,6 +273,7 @@ class BaseTextInput extends Component { !hasLabel && styles.pv0, this.props.prefixCharacter && StyleUtils.getPaddingLeft(this.state.prefixWidth + styles.pl1.paddingLeft), this.props.secureTextEntry && styles.secureInput, + !this.props.multiline && {height: this.state.height}, ]} multiline={this.props.multiline} maxLength={this.props.maxLength} diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.js index 294d15fef3ac..b3825784832d 100644 --- a/src/components/ThreeDotsMenu/index.js +++ b/src/components/ThreeDotsMenu/index.js @@ -54,19 +54,20 @@ class ThreeDotsMenu extends Component { constructor(props) { super(props); - this.togglePopupMenu = this.togglePopupMenu.bind(this); + this.hidePopoverMenu = this.hidePopoverMenu.bind(this); + this.showPopoverMenu = this.showPopoverMenu.bind(this); this.state = { isPopupMenuVisible: false, }; + this.button = null; } - /** - * Toggles the popup menu visibility - */ - togglePopupMenu() { - this.setState(prevState => ({ - isPopupMenuVisible: !prevState.isPopupMenuVisible, - })); + showPopoverMenu() { + this.setState({isPopupMenuVisible: true}); + } + + hidePopoverMenu() { + this.setState({isPopupMenuVisible: false}); } render() { @@ -76,11 +77,12 @@ class ThreeDotsMenu extends Component { { - this.togglePopupMenu(); + this.showPopoverMenu(); if (this.props.onIconPress) { this.props.onIconPress(); } }} + ref={el => this.button = el} style={[styles.touchableButtonImage, ...this.props.iconStyles]} > this.togglePopupMenu()} + onItemSelected={this.hidePopoverMenu} menuItems={this.props.menuItems} /> @@ -104,7 +106,7 @@ class ThreeDotsMenu extends Component { ThreeDotsMenu.propTypes = propTypes; ThreeDotsMenu.defaultProps = defaultProps; -ThreeDotsMenu.displayName = 'ThreeDotsMenu'; + export default withLocalize(ThreeDotsMenu); export {ThreeDotsMenuItemPropTypes}; diff --git a/src/components/WalletStatementModal/index.js b/src/components/WalletStatementModal/index.js index 60d57fc5c14b..762bed5b6296 100644 --- a/src/components/WalletStatementModal/index.js +++ b/src/components/WalletStatementModal/index.js @@ -66,7 +66,7 @@ class WalletStatementModal extends React.Component { WalletStatementModal.propTypes = walletStatementPropTypes; WalletStatementModal.defaultProps = walletStatementDefaultProps; -WalletStatementModal.displayName = 'WalletStatementModal'; + export default compose( withLocalize, withOnyx({ diff --git a/src/components/WalletStatementModal/index.native.js b/src/components/WalletStatementModal/index.native.js index 01feae5fa021..9c525a12420c 100644 --- a/src/components/WalletStatementModal/index.native.js +++ b/src/components/WalletStatementModal/index.native.js @@ -59,7 +59,7 @@ class WalletStatementModal extends React.Component { WalletStatementModal.propTypes = walletStatementPropTypes; WalletStatementModal.defaultProps = walletStatementDefaultProps; -WalletStatementModal.displayName = 'WalletStatementModal'; + export default compose( withLocalize, withOnyx({ diff --git a/src/languages/en.js b/src/languages/en.js index cfc3803a2c91..00d6f9e6f9d3 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -792,7 +792,6 @@ export default { issueAndManageCards: 'Issue and manage cards', reconcileCards: 'Reconcile cards', settlementFrequency: 'Settlement frequency', - growlMessageOnCreate: 'Workspace created', growlMessageOnSave: 'Your workspace settings were successfully saved!', deleteConfirmation: 'Are you sure you want to delete this workspace?', growlMessageOnDelete: 'Workspace deleted', @@ -802,7 +801,6 @@ export default { new: { newWorkspace: 'New workspace', getTheExpensifyCardAndMore: 'Get the Expensify Card and more', - genericFailureMessage: 'An error occurred creating the workspace, please try again.', }, people: { genericFailureMessage: 'An error occurred removing a user from the workspace, please try again.', diff --git a/src/languages/es.js b/src/languages/es.js index b30a1995ffcc..75554f7c91e8 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -794,7 +794,6 @@ export default { issueAndManageCards: 'Emitir y gestionar tarjetas', reconcileCards: 'Reconciliar tarjetas', settlementFrequency: 'Frecuencia de liquidación', - growlMessageOnCreate: 'El espacio de trabajo ha sido creado', growlMessageOnSave: '¡La configuración del espacio de trabajo se ha guardado correctamente!', growlMessageOnDelete: 'Espacio de trabajo eliminado', deleteConfirmation: '¿Estás seguro de que quieres eliminar este espacio de trabajo?', @@ -804,7 +803,6 @@ export default { new: { newWorkspace: 'Nuevo espacio de trabajo', getTheExpensifyCardAndMore: 'Consigue la Tarjeta Expensify y más', - genericFailureMessage: 'Se ha producido un error al intentar crear el espacio de trabajo. Por favor, inténtalo de nuevo.', }, people: { genericFailureMessage: 'Se ha producido un error al intentar eliminar a un usuario del espacio de trabajo. Por favor inténtalo más tarde.', diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index a562df509c82..02b64dda758d 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -321,7 +321,19 @@ function isValidRoutingNumber(number) { * @returns {Boolean[]} */ function doesFailCharacterLimit(maxLength, valuesToBeValidated) { - return _.map(valuesToBeValidated, value => value.length > maxLength); + return _.map(valuesToBeValidated, value => value && value.length > maxLength); +} + +/** + * Checks if each string in array is of valid length and then returns true + * for each string which exceeds the limit. The function trims the passed values. + * + * @param {Number} maxLength + * @param {String[]} valuesToBeValidated + * @returns {Boolean[]} + */ +function doesFailCharacterLimitAfterTrim(maxLength, valuesToBeValidated) { + return _.map(valuesToBeValidated, value => value && value.trim().length > maxLength); } /** @@ -384,6 +396,7 @@ export { isValidSSNLastFour, isValidSSNFullNine, doesFailCharacterLimit, + doesFailCharacterLimitAfterTrim, isReservedRoomName, isExistingRoomName, isValidTaxID, diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index c5e7b93c8060..2e2a493ed468 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -156,7 +156,7 @@ function reconnectApp() { * will occur. * When the exitTo route is 'workspace/new', we create a new - * workspace and navigate to it via Policy.createAndGetPolicyList. + * workspace and navigate to it * * We subscribe to the session using withOnyx in the AuthScreens and * pass it in as a parameter. withOnyx guarantees that the value has been read @@ -177,7 +177,7 @@ function setUpPoliciesAndNavigate(session) { && Str.startsWith(url.pathname, Str.normalizeUrl(ROUTES.TRANSITION_FROM_OLD_DOT)) && exitTo === ROUTES.WORKSPACE_NEW; if (shouldCreateFreePolicy) { - Policy.createAndGetPolicyList(); + Policy.createWorkspace(); return; } if (!isLoggingInAsNewUser && exitTo) { diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 55bc10f84cd6..2653398b046e 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -72,7 +72,7 @@ function getDisplayName(login, personalDetail) { * @returns {String} */ function getMaxCharacterError(isError) { - return isError ? Localize.translateLocal('personalDetails.error.characterLimit', {limit: 50}) : ''; + return isError ? Localize.translateLocal('personalDetails.error.characterLimit', {limit: CONST.FORM_CHARACTER_LIMIT}) : ''; } /** @@ -263,7 +263,6 @@ function setPersonalDetails(details, shouldGrowl) { } function updateProfile(firstName, lastName, pronouns, timezone) { - const myPersonalDetails = personalDetails[currentUserEmail]; API.write('UpdateProfile', { firstName, lastName, @@ -286,19 +285,6 @@ function updateProfile(firstName, lastName, pronouns, timezone) { }, }, }], - failureData: [{ - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS, - value: { - [currentUserEmail]: { - firstName: myPersonalDetails.firstName, - lastName: myPersonalDetails.lastName, - pronouns: myPersonalDetails.pronouns, - timezone: myPersonalDetails.timeZone, - displayName: myPersonalDetails.displayName, - }, - }, - }], }); } diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index d38846c9aa8a..64f190168720 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -111,58 +111,6 @@ function updateAllPolicies(policyCollection) { }); } -/** - * Merges the passed in login into the specified policy - * - * @param {String} [name] - * @param {Boolean} [shouldAutomaticallyReroute] - * @returns {Promise} - */ -function create(name = '') { - let res = null; - return DeprecatedAPI.Policy_Create({type: CONST.POLICY.TYPE.FREE, policyName: name}) - .then((response) => { - if (response.jsonCode !== 200) { - // Show the user feedback - const errorMessage = Localize.translateLocal('workspace.new.genericFailureMessage'); - Growl.error(errorMessage, 5000); - return; - } - Growl.show(Localize.translateLocal('workspace.common.growlMessageOnCreate'), CONST.GROWL.SUCCESS, 3000); - res = response; - - // Fetch the default reports and the policyExpenseChat reports on the policy - Report.fetchChatReportsByIDs([response.policy.chatReportIDAdmins, response.policy.chatReportIDAnnounce, response.ownerPolicyExpenseChatID]); - - // We are awaiting this merge so that we can guarantee our policy is available to any React components connected to the policies collection before we navigate to a new route. - return Promise.all([ - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${response.policyID}`, { - id: response.policyID, - type: response.policy.type, - name: response.policy.name, - role: CONST.POLICY.ROLE.ADMIN, - outputCurrency: response.policy.outputCurrency, - }), - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${response.policyID}`, getSimplifiedEmployeeList(response.policy.employeeList)), - ]); - }) - .then(() => Promise.resolve(lodashGet(res, 'policyID'))); -} - -/** - * @param {String} policyID - */ -function navigateToPolicy(policyID) { - Navigation.navigate(policyID ? ROUTES.getWorkspaceInitialRoute(policyID) : ROUTES.HOME); -} - -/** - * @param {String} [name] - */ -function createAndNavigate(name = '') { - create(name).then(navigateToPolicy); -} - /** * Delete the policy * @@ -225,19 +173,6 @@ function getPolicyList() { }); } -function createAndGetPolicyList() { - let newPolicyID; - create() - .then((policyID) => { - newPolicyID = policyID; - return getPolicyList(); - }) - .then(() => { - Navigation.dismissModal(); - navigateToPolicy(newPolicyID); - }); -} - /** * @param {String} policyID */ @@ -574,20 +509,31 @@ function clearWorkspaceGeneralSettingsErrors(policyID) { * @param {Object} errors */ function setWorkspaceErrors(policyID, errors) { + if (!allPolicies[policyID]) { + return; + } + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errors: null}); Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errors}); } /** * @param {String} policyID - * @param {Number} customUnitID + * @param {String} customUnitID + * @param {String} customUnitRateID */ -function removeUnitError(policyID, customUnitID) { +function clearCustomUnitErrors(policyID, customUnitID, customUnitRateID) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { customUnits: { [customUnitID]: { errors: null, pendingAction: null, + onyxRates: { + [customUnitRateID]: { + errors: null, + pendingAction: null, + }, + }, }, }, }); @@ -597,23 +543,27 @@ function removeUnitError(policyID, customUnitID) { * @param {String} policyID */ function hideWorkspaceAlertMessage(policyID) { + if (!allPolicies[policyID]) { + return; + } + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {alertMessage: ''}); } /** * @param {String} policyID * @param {Object} currentCustomUnit - * @param {Object} values The new custom unit values + * @param {Object} newCustomUnit */ -function updateWorkspaceCustomUnit(policyID, currentCustomUnit, values) { +function updateWorkspaceCustomUnit(policyID, currentCustomUnit, newCustomUnit) { const optimisticData = [ { onyxMethod: 'merge', key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { customUnits: { - [values.customUnitID]: { - ...values, + [newCustomUnit.customUnitID]: { + ...newCustomUnit, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, }, @@ -627,7 +577,7 @@ function updateWorkspaceCustomUnit(policyID, currentCustomUnit, values) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { customUnits: { - [values.customUnitID]: { + [newCustomUnit.customUnitID]: { pendingAction: null, errors: null, }, @@ -657,40 +607,81 @@ function updateWorkspaceCustomUnit(policyID, currentCustomUnit, values) { API.write('UpdateWorkspaceCustomUnit', { policyID, - customUnit: JSON.stringify(values), + customUnit: JSON.stringify(newCustomUnit), }, {optimisticData, successData, failureData}); } /** * @param {String} policyID + * @param {Object} currentCustomUnitRate * @param {String} customUnitID - * @param {Object} values + * @param {Object} newCustomUnitRate */ -function setCustomUnitRate(policyID, customUnitID, values) { - DeprecatedAPI.Policy_CustomUnitRate_Update({ - policyID: policyID.toString(), - customUnitID: customUnitID.toString(), - customUnitRate: JSON.stringify(values), - lastModified: null, - }) - .then((response) => { - if (response.jsonCode !== 200) { - throw new Error(); - } +function updateCustomUnitRate(policyID, currentCustomUnitRate, customUnitID, newCustomUnitRate) { + const optimisticData = [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + onyxRates: { + [newCustomUnitRate.customUnitRateID]: { + ...newCustomUnitRate, + errors: null, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + }, + }, + }, + ]; - updateLocalPolicyValues(policyID, { - customUnit: { - rate: { - id: values.customUnitRateID, - name: values.name, - value: Number(values.rate), + const successData = [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + onyxRates: { + [newCustomUnitRate.customUnitRateID]: { + pendingAction: null, + }, + }, }, }, - }); - }).catch(() => { - // Show the user feedback - Growl.error(Localize.translateLocal('workspace.editor.genericFailureMessage'), 5000); - }); + }, + }, + ]; + + const failureData = [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + onyxRates: { + [currentCustomUnitRate.customUnitRateID]: { + ...currentCustomUnitRate, + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('workspace.reimburse.updateCustomUnitError'), + }, + }, + }, + }, + }, + }, + }, + ]; + + API.write('UpdateWorkspaceCustomUnitRate', { + policyID, + customUnitID, + customUnitRate: JSON.stringify(newCustomUnitRate), + }, {optimisticData, successData, failureData}); } /** @@ -802,26 +793,186 @@ function generatePolicyID() { return _.times(16, () => Math.floor(Math.random() * 16).toString(16)).join('').toUpperCase(); } +/** + * Optimistically creates a new workspace and default workspace chats + */ +function createWorkspace() { + const policyID = generatePolicyID(); + const workspaceName = generateDefaultWorkspaceName(); + + const { + announceChatReportID, + announceChatData, + announceReportActionData, + adminsChatReportID, + adminsChatData, + adminsReportActionData, + expenseChatReportID, + expenseChatData, + expenseReportActionData, + } = Report.createOptimisticWorkspaceChats(policyID, workspaceName); + + // We need to use makeRequestWithSideEffects as we try to redirect to the policy right after creation + // The policy hasn't been merged in Onyx data at this point, leading to an intermittent Not Found screen + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects('CreateWorkspace', { + policyID, + announceChatReportID, + adminsChatReportID, + expenseChatReportID, + policyName: workspaceName, + type: CONST.POLICY.TYPE.FREE, + }, + { + optimisticData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + id: policyID, + type: CONST.POLICY.TYPE.FREE, + name: workspaceName, + role: CONST.POLICY.ROLE.ADMIN, + owner: sessionEmail, + outputCurrency: 'USD', + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, + value: { + [sessionEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + errors: {}, + }, + }, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, + value: announceChatData, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`, + value: announceReportActionData, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, + value: adminsChatData, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, + value: adminsReportActionData, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: expenseChatData, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: expenseReportActionData, + }], + successData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: {pendingAction: null}, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, + value: {pendingAction: null}, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, + value: {pendingAction: null}, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: {pendingAction: null}, + }], + failureData: [{ + onyxMethod: CONST.ONYX.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: null, + }, + { + onyxMethod: CONST.ONYX.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, + value: null, + }, + { + onyxMethod: CONST.ONYX.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, + value: null, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`, + value: null, + }, + { + onyxMethod: CONST.ONYX.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, + value: null, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, + value: null, + }, + { + onyxMethod: CONST.ONYX.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: null, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: null, + }], + }).then(() => { + Navigation.navigate(ROUTES.getWorkspaceInitialRoute(policyID)); + }); +} + function openWorkspaceReimburseView(policyID) { API.read('OpenWorkspaceReimburseView', {policyID}); } +function openWorkspaceMembersPage(policyID, clientMemberEmails) { + API.read('OpenWorkspaceMembersPage', { + policyID, + clientMemberEmails: JSON.stringify(clientMemberEmails), + }); +} + +function openWorkspaceInvitePage(policyID, clientMemberEmails) { + API.read('OpenWorkspaceInvitePage', { + policyID, + clientMemberEmails: JSON.stringify(clientMemberEmails), + }); +} + export { getPolicyList, loadFullPolicy, removeMembers, invite, isAdminOfFreePolicy, - create, update, setWorkspaceErrors, - removeUnitError, + clearCustomUnitErrors, hideWorkspaceAlertMessage, deletePolicy, - createAndNavigate, - createAndGetPolicyList, updateWorkspaceCustomUnit, - setCustomUnitRate, + updateCustomUnitRate, updateLastAccessedWorkspace, subscribeToPolicyEvents, clearDeleteMemberError, @@ -834,4 +985,7 @@ export { updateWorkspaceAvatar, clearAvatarErrors, generatePolicyID, + createWorkspace, + openWorkspaceMembersPage, + openWorkspaceInvitePage, }; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 66df72f91915..105ca3964226 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -588,13 +588,27 @@ function fetchAllReports( * Creates an optimistic chat report with a randomly generated reportID and as much information as we currently have * * @param {Array} participantList + * @param {String} reportName + * @param {String} chatType + * @param {String} policyID + * @param {String} ownerEmail + * @param {Boolean} isOwnPolicyExpenseChat + * @param {String} oldPolicyName * @returns {Object} */ -function createOptimisticChatReport(participantList) { +function createOptimisticChatReport( + participantList, + reportName = 'Chat Report', + chatType = '', + policyID = '_FAKE_', + ownerEmail = '__FAKE__', + isOwnPolicyExpenseChat = false, + oldPolicyName = '', +) { return { - chatType: '', + chatType, hasOutstandingIOU: false, - isOwnPolicyExpenseChat: false, + isOwnPolicyExpenseChat, isPinned: false, lastActorEmail: '', lastMessageHtml: '', @@ -604,18 +618,97 @@ function createOptimisticChatReport(participantList) { lastVisitedTimestamp: 0, maxSequenceNumber: 0, notificationPreference: '', - oldPolicyName: '', - ownerEmail: '__FAKE__', + oldPolicyName, + ownerEmail, participants: participantList, - policyID: '_FAKE_', + policyID, reportID: ReportUtils.generateReportID(), - reportName: 'Chat Report', + reportName, stateNum: 0, statusNum: 0, visibility: undefined, }; } +/** + * Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically + * @param {String} ownerEmail + * @returns {Object} + */ +function createOptimisticCreatedReportAction(ownerEmail) { + return { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + actorEmail: currentUserEmail, + actorAccountID: currentUserAccountID, + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'strong', + text: ownerEmail === currentUserEmail ? 'You' : ownerEmail, + }, + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'normal', + text: ' created this report', + }, + ], + person: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'strong', + text: lodashGet(personalDetails, [currentUserEmail, 'displayName'], currentUserEmail), + }, + ], + automatic: false, + sequenceNumber: 0, + avatar: lodashGet(personalDetails, [currentUserEmail, 'avatar'], ReportUtils.getDefaultAvatar(currentUserEmail)), + timestamp: moment().unix(), + shouldShow: true, + }, + }; +} + +/** + * @param {String} policyID + * @param {String} policyName + * @returns {Object} + */ +function createOptimisticWorkspaceChats(policyID, policyName) { + const announceChatData = createOptimisticChatReport( + [currentUserEmail], + CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE, + CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, + policyID, + null, + false, + policyName, + ); + const announceChatReportID = announceChatData.reportID; + const announceReportActionData = createOptimisticCreatedReportAction(announceChatData.ownerEmail); + + const adminsChatData = createOptimisticChatReport([currentUserEmail], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, policyID, null, false, policyName); + const adminsChatReportID = adminsChatData.reportID; + const adminsReportActionData = createOptimisticCreatedReportAction(adminsChatData.ownerEmail); + + const expenseChatData = createOptimisticChatReport([currentUserEmail], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, currentUserEmail, true, policyName); + const expenseChatReportID = expenseChatData.reportID; + const expenseReportActionData = createOptimisticCreatedReportAction(expenseChatData.ownerEmail); + + return { + announceChatReportID, + announceChatData, + announceReportActionData, + adminsChatReportID, + adminsChatData, + adminsReportActionData, + expenseChatReportID, + expenseChatData, + expenseReportActionData, + }; +} + /** * @param {Number} reportID * @param {String} [text] @@ -1538,7 +1631,9 @@ export { readOldestAction, openReport, openPaymentDetailsPage, + createOptimisticWorkspaceChats, createOptimisticChatReport, + createOptimisticCreatedReportAction, updatePolicyRoomName, clearPolicyRoomNameErrors, }; diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index ea9d12375508..838f2e3b06ab 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -186,8 +186,8 @@ function updatePersonalDetails(personalDetails) { const ssn = personalDetails.ssn || ''; const phoneNumber = personalDetails.phoneNumber || ''; API.write('UpdatePersonalDetailsForWallet', { - firstName, - lastName, + legalFirstName: firstName, + legalLastName: lastName, dob, addressStreet, addressCity, diff --git a/src/libs/deprecatedAPI.js b/src/libs/deprecatedAPI.js index 26a25ff2c9e7..dffae4360b76 100644 --- a/src/libs/deprecatedAPI.js +++ b/src/libs/deprecatedAPI.js @@ -478,17 +478,6 @@ function User_IsUsingExpensifyCard() { return Network.post('User_IsUsingExpensifyCard', {}); } -/** - * @param {Object} parameters - * @param {String} [parameters.type] - * @param {String} [parameters.policyName] - * @returns {Promise} - */ -function Policy_Create(parameters) { - const commandName = 'Policy_Create'; - return Network.post(commandName, parameters); -} - /** * @param {Object} parameters * @param {String} parameters.policyID @@ -640,7 +629,6 @@ export { Wallet_GetOnfidoSDKToken, TransferWalletBalance, GetLocalCurrency, - Policy_Create, Policy_CustomUnitRate_Update, Policy_Employees_Remove, PreferredLocale_Update, diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js index dc5245702c88..525b20760d75 100644 --- a/src/pages/AddPersonalBankAccountPage.js +++ b/src/pages/AddPersonalBankAccountPage.js @@ -209,7 +209,6 @@ class AddPersonalBankAccountPage extends React.Component { AddPersonalBankAccountPage.propTypes = propTypes; AddPersonalBankAccountPage.defaultProps = defaultProps; -AddPersonalBankAccountPage.displayName = 'AddPersonalBankAccountPage'; export default compose( withLocalize, diff --git a/src/pages/EnablePayments/ActivateStep.js b/src/pages/EnablePayments/ActivateStep.js index c02d6451a659..94454e8ce78a 100644 --- a/src/pages/EnablePayments/ActivateStep.js +++ b/src/pages/EnablePayments/ActivateStep.js @@ -88,5 +88,5 @@ class ActivateStep extends React.Component { ActivateStep.propTypes = propTypes; ActivateStep.defaultProps = defaultProps; -ActivateStep.displayName = 'ActivateStep'; + export default withLocalize(ActivateStep); diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js index cc041cbe13e7..9efc59dc940e 100644 --- a/src/pages/ReportSettingsPage.js +++ b/src/pages/ReportSettingsPage.js @@ -288,7 +288,6 @@ class ReportSettingsPage extends Component { ReportSettingsPage.propTypes = propTypes; ReportSettingsPage.defaultProps = defaultProps; -ReportSettingsPage.displayName = 'ReportSettingsPage'; export default compose( withLocalize, diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js index e53fb503c298..42eac4ea43c2 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js @@ -170,7 +170,7 @@ class BaseSidebarScreen extends Component { iconHeight: 40, text: this.props.translate('workspace.new.newWorkspace'), description: this.props.translate('workspace.new.getTheExpensifyCardAndMore'), - onSelected: () => Policy.createAndNavigate(), + onSelected: () => Policy.createWorkspace(), }, ] : []), ]} diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js index 273061aa4e3c..9d0571ab16df 100755 --- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js +++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js @@ -236,7 +236,6 @@ class IOUParticipantsSplit extends Component { } } -IOUParticipantsSplit.displayName = 'IOUParticipantsSplit'; IOUParticipantsSplit.propTypes = propTypes; IOUParticipantsSplit.defaultProps = defaultProps; diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index f23f7b1205a6..621724908951 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -279,7 +279,6 @@ class InitialSettingsPage extends React.Component { InitialSettingsPage.propTypes = propTypes; InitialSettingsPage.defaultProps = defaultProps; -InitialSettingsPage.displayName = 'InitialSettingsPage'; export default compose( withLocalize, diff --git a/src/pages/settings/Payments/AddPayPalMePage.js b/src/pages/settings/Payments/AddPayPalMePage.js index 2e8bdff1d8e7..8fd4d262a487 100644 --- a/src/pages/settings/Payments/AddPayPalMePage.js +++ b/src/pages/settings/Payments/AddPayPalMePage.js @@ -103,7 +103,6 @@ class AddPayPalMePage extends React.Component { AddPayPalMePage.propTypes = propTypes; AddPayPalMePage.defaultProps = defaultProps; -AddPayPalMePage.displayName = 'AddPayPalMePage'; export default compose( withLocalize, diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 458a17d1713a..250a7e2021ff 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -2,7 +2,7 @@ import lodashGet from 'lodash/get'; import React, {Component} from 'react'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; -import {View, ScrollView} from 'react-native'; +import {View} from 'react-native'; import Str from 'expensify-common/lib/str'; import moment from 'moment-timezone'; import _ from 'underscore'; @@ -17,16 +17,16 @@ import styles from '../../../styles/styles'; import Text from '../../../components/Text'; import LoginField from './LoginField'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; +import * as Localize from '../../../libs/Localize'; import compose from '../../../libs/compose'; -import Button from '../../../components/Button'; -import FixedFooter from '../../../components/FixedFooter'; import TextInput from '../../../components/TextInput'; import Picker from '../../../components/Picker'; -import FullNameInputRow from '../../../components/FullNameInputRow'; import CheckboxWithLabel from '../../../components/CheckboxWithLabel'; import AvatarWithImagePicker from '../../../components/AvatarWithImagePicker'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; import * as ValidationUtils from '../../../libs/ValidationUtils'; +import * as ReportUtils from '../../../libs/ReportUtils'; +import Form from '../../../components/Form'; import OfflineWithFeedback from '../../../components/OfflineWithFeedback'; const propTypes = { @@ -65,24 +65,19 @@ class ProfilePage extends Component { constructor(props) { super(props); - const currentUserDetails = this.props.currentUserPersonalDetails || {}; + this.defaultAvatar = ReportUtils.getDefaultAvatar(this.props.currentUserPersonalDetails.login); + this.avatar = {uri: lodashGet(this.props.currentUserPersonalDetails, 'avatar') || this.defaultAvatar}; + this.pronouns = props.currentUserPersonalDetails.pronouns; this.state = { - firstName: currentUserDetails.firstName || '', - hasFirstNameError: false, - lastName: currentUserDetails.lastName || '', - hasLastNameError: false, - pronouns: currentUserDetails.pronouns || '', - hasPronounError: false, - hasSelfSelectedPronouns: !_.isEmpty(currentUserDetails.pronouns) && !currentUserDetails.pronouns.startsWith(CONST.PRONOUNS.PREFIX), - selectedTimezone: lodashGet(currentUserDetails, 'timezone.selected', CONST.DEFAULT_TIME_ZONE.selected), - isAutomaticTimezone: lodashGet(currentUserDetails, 'timezone.automatic', CONST.DEFAULT_TIME_ZONE.automatic), logins: this.getLogins(props.loginList), + selectedTimezone: lodashGet(props.currentUserPersonalDetails.timezone, 'selected', CONST.DEFAULT_TIME_ZONE.selected), + isAutomaticTimezone: lodashGet(props.currentUserPersonalDetails.timezone, 'automatic', CONST.DEFAULT_TIME_ZONE.automatic), + hasSelfSelectedPronouns: !_.isEmpty(props.currentUserPersonalDetails.pronouns) && !props.currentUserPersonalDetails.pronouns.startsWith(CONST.PRONOUNS.PREFIX), }; this.getLogins = this.getLogins.bind(this); - this.setAutomaticTimezone = this.setAutomaticTimezone.bind(this); + this.validate = this.validate.bind(this); this.updatePersonalDetails = this.updatePersonalDetails.bind(this); - this.validateInputs = this.validateInputs.bind(this); } componentDidUpdate(prevProps) { @@ -101,18 +96,6 @@ class ProfilePage extends Component { this.setState(stateToUpdate); } - /** - * Set the form to use automatic timezone - * - * @param {Boolean} isAutomaticTimezone - */ - setAutomaticTimezone(isAutomaticTimezone) { - this.setState(({selectedTimezone}) => ({ - isAutomaticTimezone, - selectedTimezone: isAutomaticTimezone ? moment.tz.guess() : selectedTimezone, - })); - } - /** * Get the most validated login of each type * @@ -145,34 +128,65 @@ class ProfilePage extends Component { /** * Submit form to update personal details + * @param {Object} values + * @param {String} values.firstName + * @param {String} values.lastName + * @param {String} values.pronouns + * @param {Boolean} values.isAutomaticTimezone + * @param {String} values.timezone + * @param {String} values.selfSelectedPronoun */ - updatePersonalDetails() { - if (!this.validateInputs()) { - return; - } - + updatePersonalDetails(values) { PersonalDetails.updateProfile( - this.state.firstName.trim(), - this.state.lastName.trim(), - this.state.pronouns.trim(), + values.firstName.trim(), + values.lastName.trim(), + (this.state.hasSelfSelectedPronouns) ? values.selfSelectedPronoun.trim() : values.pronouns.trim(), { - automatic: this.state.isAutomaticTimezone, - selected: this.state.selectedTimezone, + automatic: values.isAutomaticTimezone, + selected: values.timezone, }, ); } - validateInputs() { - const [hasFirstNameError, hasLastNameError, hasPronounError] = ValidationUtils.doesFailCharacterLimit( - 50, - [this.state.firstName.trim(), this.state.lastName.trim(), this.state.pronouns.trim()], + /** + * @param {Object} values - An object containing the value of each inputID + * @param {String} values.firstName + * @param {String} values.lastName + * @param {String} values.pronouns + * @param {Boolean} values.isAutomaticTimezone + * @param {String} values.timezone + * @param {String} values.selfSelectedPronoun + * @returns {Object} - An object containing the errors for each inputID + */ + validate(values) { + const errors = {}; + + const [hasFirstNameError, hasLastNameError, hasPronounError] = ValidationUtils.doesFailCharacterLimitAfterTrim( + CONST.FORM_CHARACTER_LIMIT, + [values.firstName, values.lastName, values.pronouns], ); + + const hasSelfSelectedPronouns = values.pronouns === CONST.PRONOUNS.SELF_SELECT; + this.pronouns = hasSelfSelectedPronouns ? '' : values.pronouns; this.setState({ - hasFirstNameError, - hasLastNameError, - hasPronounError, + hasSelfSelectedPronouns, + isAutomaticTimezone: values.isAutomaticTimezone, + selectedTimezone: values.isAutomaticTimezone ? moment.tz.guess() : values.timezone, }); - return !hasFirstNameError && !hasLastNameError && !hasPronounError; + + if (hasFirstNameError) { + errors.firstName = Localize.translateLocal('personalDetails.error.characterLimit', {limit: CONST.FORM_CHARACTER_LIMIT}); + } + + if (hasLastNameError) { + errors.lastName = Localize.translateLocal('personalDetails.error.characterLimit', {limit: CONST.FORM_CHARACTER_LIMIT}); + } + + if (hasPronounError) { + errors.pronouns = Localize.translateLocal('personalDetails.error.characterLimit', {limit: CONST.FORM_CHARACTER_LIMIT}); + } + + return errors; } render() { @@ -180,16 +194,8 @@ class ProfilePage extends Component { label: value, value: `${CONST.PRONOUNS.PREFIX}${key}`, })); - - // Disables button if none of the form values have changed const currentUserDetails = this.props.currentUserPersonalDetails || {}; - const isButtonDisabled = (currentUserDetails.firstName === this.state.firstName.trim()) - && (currentUserDetails.lastName === this.state.lastName.trim()) - && (lodashGet(currentUserDetails, 'timezone.selected') === this.state.selectedTimezone) - && (lodashGet(currentUserDetails, 'timezone.automatic') === this.state.isAutomaticTimezone) - && (currentUserDetails.pronouns === this.state.pronouns.trim()); - - const pronounsPickerValue = this.state.hasSelfSelectedPronouns ? CONST.PRONOUNS.SELF_SELECT : this.state.pronouns; + const pronounsPickerValue = this.state.hasSelfSelectedPronouns ? CONST.PRONOUNS.SELF_SELECT : this.pronouns; return ( @@ -199,10 +205,16 @@ class ProfilePage extends Component { onBackButtonPress={() => Navigation.navigate(ROUTES.SETTINGS)} onCloseButtonPress={() => Navigation.dismissModal(true)} /> - +
@@ -213,45 +225,49 @@ class ProfilePage extends Component { onImageRemoved={PersonalDetails.deleteAvatar} anchorPosition={styles.createMenuPositionProfile} size={CONST.AVATAR_SIZE.LARGE} - /> {this.props.translate('profilePage.tellUsAboutYourself')} - this.setState({firstName})} - onChangeLastName={lastName => this.setState({lastName})} - style={[styles.mt4, styles.mb4]} - /> + + + + + + + + + { - const hasSelfSelectedPronouns = pronouns === CONST.PRONOUNS.SELF_SELECT; - this.setState({ - pronouns: hasSelfSelectedPronouns ? '' : pronouns, - hasSelfSelectedPronouns, - }); - }} items={pronounsList} placeholder={{ value: '', label: this.props.translate('profilePage.selectYourPronouns'), }} - value={pronounsPickerValue} + defaultValue={pronounsPickerValue} /> {this.state.hasSelfSelectedPronouns && ( this.setState({pronouns})} + inputID="selfSelectedPronoun" + defaultValue={this.pronouns} placeholder={this.props.translate('profilePage.selfSelectYourPronoun')} - errorText={PersonalDetails.getMaxCharacterError(this.state.hasPronounError)} /> )} @@ -260,37 +276,30 @@ class ProfilePage extends Component { label={this.props.translate('profilePage.emailAddress')} type="email" login={this.state.logins.email} + defaultValue={this.state.logins.email} /> this.setState({selectedTimezone})} items={timezones} isDisabled={this.state.isAutomaticTimezone} + defaultValue={this.state.selectedTimezone} value={this.state.selectedTimezone} /> - - -