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 => (
-
);
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)}
/>
-
+
-
-
-
+
);
}
@@ -298,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 08610ce151b9..6445933dec42 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 withFullPolicy, {fullPolicyPropTypes, fullPolicyDefaultProps} from './withFullPolicy';
-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 bf44055c9c9b..dbefba433b49 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 withFullPolicy, {fullPolicyPropTypes, fullPolicyDefaultProps} from './withFullPolicy';
+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 = {
...fullPolicyPropTypes,
...withLocalizePropTypes,
+ ...networkPropTypes,
};
const defaultProps = fullPolicyDefaultProps;
@@ -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,
withFullPolicy,
+ withNetwork(),
withOnyx({
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS,
diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js
index f687d964ab36..db12309ce7b7 100644
--- a/src/pages/workspace/WorkspaceMembersPage.js
+++ b/src/pages/workspace/WorkspaceMembersPage.js
@@ -28,6 +28,9 @@ import Hoverable from '../../components/Hoverable';
import withFullPolicy, {fullPolicyPropTypes, fullPolicyDefaultProps} from './withFullPolicy';
import CONST from '../../CONST';
import OfflineWithFeedback from '../../components/OfflineWithFeedback';
+import {withNetwork} from '../../components/OnyxProvider';
+import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView';
+import networkPropTypes from '../../components/networkPropTypes';
const propTypes = {
/** The personal details of the person who is logged in */
@@ -45,6 +48,7 @@ const propTypes = {
...fullPolicyPropTypes,
...withLocalizePropTypes,
...windowDimensionsPropTypes,
+ ...networkPropTypes,
};
const defaultProps = fullPolicyDefaultProps;
@@ -67,6 +71,21 @@ class WorkspaceMembersPage extends React.Component {
this.hideConfirmModal = this.hideConfirmModal.bind(this);
}
+ componentDidMount() {
+ const clientMemberEmails = _.keys(this.props.policyMemberList);
+ Policy.openWorkspaceMembersPage(this.props.route.params.policyID, clientMemberEmails);
+ }
+
+ componentDidUpdate(prevProps) {
+ const isReconnecting = prevProps.network.isOffline && !this.props.network.isOffline;
+ if (!isReconnecting) {
+ return;
+ }
+
+ const clientMemberEmails = _.keys(this.props.policyMemberList);
+ Policy.openWorkspaceMembersPage(this.props.route.params.policyID, clientMemberEmails);
+ }
+
/**
* Open the modal to invite a user
*/
@@ -277,64 +296,66 @@ class WorkspaceMembersPage extends React.Component {
return (
- Navigation.dismissModal()}
- onBackButtonPress={() => Navigation.navigate(ROUTES.getWorkspaceInitialRoute(policyID))}
- shouldShowGetAssistanceButton
- guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS}
- shouldShowBackButton
- />
- this.removeUsers()}
- onCancel={this.hideConfirmModal}
- prompt={this.props.translate('workspace.people.removeMembersPrompt')}
- confirmText={this.props.translate('common.remove')}
- cancelText={this.props.translate('common.cancel')}
- />
-
-
-
-
-
-
-
-
- this.toggleAllUsers()}
- />
-
-
-
- {this.props.translate('workspace.people.selectAll')}
-
+
+ Navigation.dismissModal()}
+ onBackButtonPress={() => Navigation.navigate(ROUTES.getWorkspaceInitialRoute(policyID))}
+ shouldShowGetAssistanceButton
+ guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS}
+ shouldShowBackButton
+ />
+ this.removeUsers()}
+ onCancel={this.hideConfirmModal}
+ prompt={this.props.translate('workspace.people.removeMembersPrompt')}
+ confirmText={this.props.translate('common.remove')}
+ cancelText={this.props.translate('common.cancel')}
+ />
+
+
+
+
+
+
+
+
+ this.toggleAllUsers()}
+ />
+
+
+
+ {this.props.translate('workspace.people.selectAll')}
+
+
+ item.login}
+ showsVerticalScrollIndicator={false}
+ />
- item.login}
- showsVerticalScrollIndicator={false}
- />
-
+
);
}
@@ -347,6 +368,7 @@ export default compose(
withLocalize,
withWindowDimensions,
withFullPolicy,
+ withNetwork(),
withOnyx({
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS,
diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.js b/src/pages/workspace/reimburse/WorkspaceReimburseView.js
index e4899ce9426b..549605ebaff2 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(
withFullPolicy,
diff --git a/src/pages/workspace/withFullPolicy.js b/src/pages/workspace/withFullPolicy.js
index 224fdaf77364..45f608d685b2 100644
--- a/src/pages/workspace/withFullPolicy.js
+++ b/src/pages/workspace/withFullPolicy.js
@@ -59,9 +59,6 @@ const fullPolicyPropTypes = {
/** 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 c72806793972..399f02a0c5c4 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: {