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/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 a575a51cf1c6..903f70bdaa56 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: { 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/AttachmentModal.js b/src/components/AttachmentModal.js index e5235244b719..64999e9005db 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -120,7 +120,7 @@ class AttachmentModal extends PureComponent { const fileName = fullFileName.trim(); const splitFileName = fileName.split('.'); const fileExtension = splitFileName.pop(); - return {fileName, fileExtension}; + return {fileName: splitFileName.join('.'), fileExtension}; } /** 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 7fc61a36e95b..59c05e73ed97 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js @@ -64,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/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/Policy.js b/src/libs/actions/Policy.js index e37c9fc5fe8e..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,6 +793,155 @@ 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}); } @@ -813,22 +953,26 @@ function openWorkspaceMembersPage(policyID, 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, @@ -841,5 +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/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 e652cda0c86d..250a7e2021ff 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -307,7 +307,6 @@ class ProfilePage extends Component { ProfilePage.propTypes = propTypes; ProfilePage.defaultProps = defaultProps; -ProfilePage.displayName = 'ProfilePage'; export default compose( withLocalize, diff --git a/src/pages/settings/Security/CloseAccountPage.js b/src/pages/settings/Security/CloseAccountPage.js index 96e8aa70be4a..f6e411d29135 100644 --- a/src/pages/settings/Security/CloseAccountPage.js +++ b/src/pages/settings/Security/CloseAccountPage.js @@ -143,7 +143,6 @@ class CloseAccountPage extends Component { CloseAccountPage.propTypes = propTypes; CloseAccountPage.defaultProps = defaultProps; -CloseAccountPage.displayName = 'CloseAccountPage'; export default compose( withLocalize, diff --git a/src/pages/signin/ResendValidationForm.js b/src/pages/signin/ResendValidationForm.js index 00f1c1d629a1..38f0cc781cb5 100755 --- a/src/pages/signin/ResendValidationForm.js +++ b/src/pages/signin/ResendValidationForm.js @@ -100,6 +100,7 @@ const ResendValidationForm = (props) => { ResendValidationForm.propTypes = propTypes; ResendValidationForm.defaultProps = defaultProps; +ResendValidationForm.displayName = 'ResendValidationForm'; export default compose( withLocalize, diff --git a/src/pages/wallet/WalletStatementPage.js b/src/pages/wallet/WalletStatementPage.js index d1056ec5c21a..572aa672bff4 100644 --- a/src/pages/wallet/WalletStatementPage.js +++ b/src/pages/wallet/WalletStatementPage.js @@ -120,7 +120,7 @@ class WalletStatementPage extends React.Component { WalletStatementPage.propTypes = propTypes; WalletStatementPage.defaultProps = defaultProps; -WalletStatementPage.displayName = 'WalletStatementPage'; + export default compose( withLocalize, withOnyx({ diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 270927d030db..cbcccda54c62 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -21,7 +21,7 @@ import compose from '../../libs/compose'; import Avatar from '../../components/Avatar'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; import withPolicy, {policyPropTypes, policyDefaultProps} from './withPolicy'; -import * as PolicyActions from '../../libs/actions/Policy'; +import * as Policy from '../../libs/actions/Policy'; import * as PolicyUtils from '../../libs/PolicyUtils'; import CONST from '../../CONST'; import * as ReimbursementAccount from '../../libs/actions/ReimbursementAccount'; @@ -74,7 +74,7 @@ class WorkspaceInitialPage extends React.Component { * Call the delete policy and hide the modal */ confirmDeleteAndHideModal() { - PolicyActions.deletePolicy(this.props.policy.id); + Policy.deletePolicy(this.props.policy.id); this.toggleDeleteModal(false); } @@ -145,7 +145,7 @@ class WorkspaceInitialPage extends React.Component { { icon: Expensicons.Plus, text: this.props.translate('workspace.new.newWorkspace'), - onSelected: () => PolicyActions.createAndNavigate(), + onSelected: () => Policy.createWorkspace(), }, { icon: Expensicons.Trashcan, text: this.props.translate('workspace.common.delete'), @@ -245,7 +245,6 @@ class WorkspaceInitialPage extends React.Component { WorkspaceInitialPage.propTypes = propTypes; WorkspaceInitialPage.defaultProps = defaultProps; -WorkspaceInitialPage.displayName = 'WorkspaceInitialPage'; export default compose( withLocalize, diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 1ee96f4620eb..86fcc48b9757 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -21,6 +21,9 @@ import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndica import * as Link from '../../libs/actions/Link'; import Text from '../../components/Text'; import withPolicy, {policyPropTypes, policyDefaultProps} from './withPolicy'; +import {withNetwork} from '../../components/OnyxProvider'; +import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; +import networkPropTypes from '../../components/networkPropTypes'; const personalDetailsPropTypes = PropTypes.shape({ /** The login of the person (either email or phone number) */ @@ -52,6 +55,7 @@ const propTypes = { ...policyPropTypes, ...withLocalizePropTypes, + ...networkPropTypes, }; const defaultProps = policyDefaultProps; @@ -84,6 +88,19 @@ class WorkspaceInvitePage extends React.Component { componentDidMount() { this.clearErrors(); + + const clientPolicyMembers = _.keys(this.props.policyMemberList); + Policy.openWorkspaceInvitePage(this.props.route.params.policyID, clientPolicyMembers); + } + + componentDidUpdate(prevProps) { + const isReconnecting = prevProps.network.isOffline && !this.props.network.isOffline; + if (!isReconnecting) { + return; + } + + const clientPolicyMembers = _.keys(this.props.policyMemberList); + Policy.openWorkspaceInvitePage(this.props.route.params.policyID, clientPolicyMembers); } getExcludedUsers() { @@ -230,22 +247,23 @@ class WorkspaceInvitePage extends React.Component { return ( {({didScreenTransitionEnd}) => ( - <> - { - this.clearErrors(); - Navigation.dismissModal(); - }} - shouldShowGetAssistanceButton - guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS} - shouldShowBackButton - onBackButtonPress={() => Navigation.goBack()} - /> - - {!didScreenTransitionEnd && } - {didScreenTransitionEnd && ( + + <> + { + this.clearErrors(); + Navigation.dismissModal(); + }} + shouldShowGetAssistanceButton + guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS} + shouldShowBackButton + onBackButtonPress={() => Navigation.goBack()} + /> + + {!didScreenTransitionEnd && } + {didScreenTransitionEnd && ( - )} - - - - this.setState({welcomeNote: text})} + )} + + + + this.setState({welcomeNote: text})} + /> + + {}} + message={this.props.policy.alertMessage} + containerStyles={[styles.flexReset, styles.mb0, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto]} /> + { + e.preventDefault(); + Link.openExternalLink(CONST.PRIVACY_URL); + }} + accessibilityRole="link" + href={CONST.PRIVACY_URL} + style={[styles.mh5, styles.mv2, styles.alignSelfStart]} + > + {({hovered, pressed}) => ( + + + {this.props.translate('common.privacyPolicy')} + + + )} + - {}} - message={this.props.policy.alertMessage} - containerStyles={[styles.flexReset, styles.mb0, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto]} - /> - { - e.preventDefault(); - Link.openExternalLink(CONST.PRIVACY_URL); - }} - accessibilityRole="link" - href={CONST.PRIVACY_URL} - style={[styles.mh5, styles.mv2, styles.alignSelfStart]} - > - {({hovered, pressed}) => ( - - - {this.props.translate('common.privacyPolicy')} - - - )} - - - + + )} ); @@ -334,6 +353,7 @@ WorkspaceInvitePage.defaultProps = defaultProps; export default compose( withLocalize, withPolicy, + withNetwork(), withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS, diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.js b/src/pages/workspace/reimburse/WorkspaceReimburseView.js index 30da75a415b5..5b9c3e6a976d 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseView.js @@ -39,7 +39,7 @@ const propTypes = { attributes: PropTypes.shape({ unit: PropTypes.string, }), - rates: PropTypes.arrayOf( + onyxRates: PropTypes.objectOf( PropTypes.shape({ customUnitRateID: PropTypes.string, name: PropTypes.string, @@ -62,14 +62,14 @@ class WorkspaceReimburseView extends React.Component { constructor(props) { super(props); const distanceCustomUnit = _.find(lodashGet(props, 'policy.customUnits', {}), unit => unit.name === 'Distance'); + const customUnitRate = _.find(lodashGet(distanceCustomUnit, 'onyxRates', {}), rate => rate.name === 'Default Rate'); this.state = { unitID: lodashGet(distanceCustomUnit, 'customUnitID', ''), unitName: lodashGet(distanceCustomUnit, 'name', ''), unitValue: lodashGet(distanceCustomUnit, 'attributes.unit', 'mi'), - rateID: lodashGet(distanceCustomUnit, 'rates[0].customUnitRateID', ''), - rateName: lodashGet(distanceCustomUnit, 'rates[0].name', ''), - rateValue: this.getRateDisplayValue(lodashGet(distanceCustomUnit, 'rates[0].rate', 0) / 100), + unitRateID: lodashGet(customUnitRate, 'customUnitRateID', ''), + unitRateValue: this.getRateDisplayValue(lodashGet(customUnitRate, 'rate', 0) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET), outputCurrency: lodashGet(props, 'policy.outputCurrency', ''), }; @@ -98,14 +98,13 @@ class WorkspaceReimburseView extends React.Component { .values() .findWhere({name: CONST.CUSTOM_UNITS.NAME_DISTANCE}) .value(); - + const customUnitRate = _.find(lodashGet(distanceCustomUnit, 'onyxRates', {}), rate => rate.name === 'Default Rate'); this.setState({ unitID: lodashGet(distanceCustomUnit, 'customUnitID', ''), unitName: lodashGet(distanceCustomUnit, 'name', ''), unitValue: lodashGet(distanceCustomUnit, 'attributes.unit', 'mi'), - rateID: lodashGet(distanceCustomUnit, 'rates[0].customUnitRateID', ''), - rateName: lodashGet(distanceCustomUnit, 'rates[0].name', ''), - rateValue: this.getRateDisplayValue(lodashGet(distanceCustomUnit, 'rates[0].rate', 0) / 100), + unitRateID: lodashGet(customUnitRate, 'customUnitRateID'), + unitRateValue: this.getRateDisplayValue(lodashGet(customUnitRate, 'rate', 0) / 100), }); } @@ -130,10 +129,10 @@ class WorkspaceReimburseView extends React.Component { const isInvalidRateValue = value !== '' && !CONST.REGEX.RATE_VALUE.test(value); this.setState(prevState => ({ - rateValue: !isInvalidRateValue ? value : prevState.rateValue, + unitRateValue: !isInvalidRateValue ? value : prevState.unitRateValue, }), () => { // Set the corrected value with a delay and sync to the server - this.updateRateValueDebounced(this.state.rateValue); + this.updateRateValueDebounced(this.state.unitRateValue); }); } @@ -156,7 +155,7 @@ class WorkspaceReimburseView extends React.Component { return; } - this.updateRateValueDebounced(this.state.rateValue); + this.updateRateValueDebounced(this.state.unitRateValue); } updateRateValue(value) { @@ -167,14 +166,15 @@ class WorkspaceReimburseView extends React.Component { } this.setState({ - rateValue: numValue.toFixed(3), + unitRateValue: numValue.toFixed(3), }); - Policy.setCustomUnitRate(this.props.policyID, this.state.unitID, { - customUnitRateID: this.state.rateID, - name: this.state.rateName, - rate: numValue.toFixed(3) * 100, - }, null); + const distanceCustomUnit = _.find(lodashGet(this.props, 'policy.customUnits', {}), unit => unit.name === 'Distance'); + const currentCustomUnitRate = lodashGet(distanceCustomUnit, ['onyxRates', this.state.unitRateID], {}); + Policy.updateCustomUnitRate(this.props.policyID, currentCustomUnitRate, this.state.unitID, { + ...currentCustomUnitRate, + rate: numValue.toFixed(3) * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, + }); } render() { @@ -214,9 +214,13 @@ class WorkspaceReimburseView extends React.Component { {this.props.translate('workspace.reimburse.trackDistanceCopy')} Policy.removeUnitError(this.props.policyID, this.state.unitID)} + errors={{ + ...lodashGet(this.props, ['policy', 'customUnits', this.state.unitID, 'errors'], {}), + ...lodashGet(this.props, ['policy', 'customUnits', this.state.unitID, 'onyxRates', this.state.unitRateID, 'errors'], {}), + }} + pendingAction={lodashGet(this.props, ['policy', 'customUnits', this.state.unitID, 'pendingAction']) + || lodashGet(this.props, ['policy', 'customUnits', this.state.unitID, 'onyxRates', this.state.unitRateID, 'pendingAction'])} + onClose={() => Policy.clearCustomUnitErrors(this.props.policyID, this.state.unitID, this.state.unitRateID)} > @@ -224,7 +228,7 @@ class WorkspaceReimburseView extends React.Component { label={this.props.translate('workspace.reimburse.trackDistanceRate')} placeholder={this.state.outputCurrency} onChangeText={value => this.setRate(value)} - value={this.state.rateValue} + value={this.state.unitRateValue} autoCompleteType="off" autoCorrect={false} keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD} @@ -289,7 +293,6 @@ class WorkspaceReimburseView extends React.Component { } WorkspaceReimburseView.propTypes = propTypes; -WorkspaceReimburseView.displayName = 'WorkspaceReimburseView'; export default compose( withPolicy, diff --git a/src/pages/workspace/withPolicy.js b/src/pages/workspace/withPolicy.js index ca3d0fa2f450..34182871eb4f 100644 --- a/src/pages/workspace/withPolicy.js +++ b/src/pages/workspace/withPolicy.js @@ -42,9 +42,6 @@ const policyPropTypes = { /** The URL for the policy avatar */ avatar: PropTypes.string, - /** A list of emails for the employees on the policy */ - employeeList: PropTypes.arrayOf(PropTypes.string), - /** Errors on the policy keyed by microtime */ errors: PropTypes.objectOf(PropTypes.string), diff --git a/src/styles/addOutlineWidth/index.js b/src/styles/addOutlineWidth/index.js index db9ba66fe087..a25bc887dc10 100644 --- a/src/styles/addOutlineWidth/index.js +++ b/src/styles/addOutlineWidth/index.js @@ -17,6 +17,7 @@ function withOutlineWidth(obj, val, error = false) { return { ...obj, outlineWidth: val, + outlineStyle: val ? 'auto' : 'none', boxShadow: val !== 0 ? `0px 0px 0px ${val}px ${error ? themeDefault.badgeDangerBG : themeDefault.borderFocus}` : 'none', }; } diff --git a/src/styles/styles.js b/src/styles/styles.js index 1560efac1b03..467a5609a4bf 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -729,7 +729,6 @@ const styles = { paddingBottom: 8, paddingHorizontal: 11, borderWidth: 0, - borderRadius: variables.componentBorderRadiusNormal, }, textInputMultiline: {