diff --git a/.github/actions/awaitStagingDeploys/index.js b/.github/actions/awaitStagingDeploys/index.js index 314a4d03295b..c6ece6eee302 100644 --- a/.github/actions/awaitStagingDeploys/index.js +++ b/.github/actions/awaitStagingDeploys/index.js @@ -226,6 +226,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -295,6 +297,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -304,6 +308,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -376,6 +382,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/actions/checkDeployBlockers/checkDeployBlockers.js b/.github/actions/checkDeployBlockers/checkDeployBlockers.js index 89c4843cc9d3..db156651f9fe 100644 --- a/.github/actions/checkDeployBlockers/checkDeployBlockers.js +++ b/.github/actions/checkDeployBlockers/checkDeployBlockers.js @@ -16,7 +16,7 @@ const run = function () { console.log('Checking for unverified PRs or unresolved deploy blockers', data); // Check the issue description to see if there are any unfinished/un-QAed items in the checklist. - const uncheckedBoxRegex = new RegExp(`-\\s\\[\\s]\\s(?:QA|${GithubUtils.ISSUE_OR_PULL_REQUEST_REGEX.source})`); + const uncheckedBoxRegex = /-\s\[\s]\s(?!Accessibility)/; if (uncheckedBoxRegex.test(data.body)) { console.log('An unverified PR or unresolved deploy blocker was found.'); core.setOutput('HAS_DEPLOY_BLOCKERS', true); diff --git a/.github/actions/checkDeployBlockers/index.js b/.github/actions/checkDeployBlockers/index.js index 51e878382f01..179dc8b4a078 100644 --- a/.github/actions/checkDeployBlockers/index.js +++ b/.github/actions/checkDeployBlockers/index.js @@ -26,7 +26,7 @@ const run = function () { console.log('Checking for unverified PRs or unresolved deploy blockers', data); // Check the issue description to see if there are any unfinished/un-QAed items in the checklist. - const uncheckedBoxRegex = new RegExp(`-\\s\\[\\s]\\s(?:QA|${GithubUtils.ISSUE_OR_PULL_REQUEST_REGEX.source})`); + const uncheckedBoxRegex = /-\s\[\s]\s(?!Accessibility)/; if (uncheckedBoxRegex.test(data.body)) { console.log('An unverified PR or unresolved deploy blocker was found.'); core.setOutput('HAS_DEPLOY_BLOCKERS', true); @@ -196,6 +196,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -265,6 +267,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -274,6 +278,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -346,6 +352,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js b/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js index 56535f03b29c..43992307b912 100644 --- a/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js +++ b/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js @@ -85,6 +85,7 @@ const run = function () { // If we aren't sent a tag, then use the existing tag const tag = newVersion || currentStagingDeployCashData.tag; + const didVersionChange = newVersion ? newVersion !== currentStagingDeployCashData.tag : false; // Find the list of PRs merged between the last StagingDeployCash and the new version const mergedPRs = GitUtils.getPullRequestsMergedBetween(previousStagingDeployCashData.tag, tag); @@ -128,6 +129,8 @@ const run = function () { _.pluck(_.where(PRList, {isAccessible: true}), 'url'), _.pluck(deployBlockers, 'url'), _.pluck(_.where(deployBlockers, {isResolved: true}), 'url'), + didVersionChange ? false : currentStagingDeployCashData.isTimingDashboardChecked, + didVersionChange ? false : currentStagingDeployCashData.isFirebaseChecked, ); }) .then((body) => { diff --git a/.github/actions/createOrUpdateStagingDeploy/index.js b/.github/actions/createOrUpdateStagingDeploy/index.js index 58ecf86f7e9d..43fda09543d8 100644 --- a/.github/actions/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/createOrUpdateStagingDeploy/index.js @@ -95,6 +95,7 @@ const run = function () { // If we aren't sent a tag, then use the existing tag const tag = newVersion || currentStagingDeployCashData.tag; + const didVersionChange = newVersion ? newVersion !== currentStagingDeployCashData.tag : false; // Find the list of PRs merged between the last StagingDeployCash and the new version const mergedPRs = GitUtils.getPullRequestsMergedBetween(previousStagingDeployCashData.tag, tag); @@ -138,6 +139,8 @@ const run = function () { _.pluck(_.where(PRList, {isAccessible: true}), 'url'), _.pluck(deployBlockers, 'url'), _.pluck(_.where(deployBlockers, {isResolved: true}), 'url'), + didVersionChange ? false : currentStagingDeployCashData.isTimingDashboardChecked, + didVersionChange ? false : currentStagingDeployCashData.isFirebaseChecked, ); }) .then((body) => { @@ -386,6 +389,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -455,6 +460,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -464,6 +471,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -536,6 +545,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/actions/getPullRequestDetails/index.js b/.github/actions/getPullRequestDetails/index.js index 0e8fcb725620..8ef5d2f04ebf 100644 --- a/.github/actions/getPullRequestDetails/index.js +++ b/.github/actions/getPullRequestDetails/index.js @@ -267,6 +267,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -336,6 +338,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -345,6 +349,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -417,6 +423,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/actions/getReleaseBody/index.js b/.github/actions/getReleaseBody/index.js index c8c7a0d153d3..a72d730563ff 100644 --- a/.github/actions/getReleaseBody/index.js +++ b/.github/actions/getReleaseBody/index.js @@ -185,6 +185,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -254,6 +256,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -263,6 +267,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -335,6 +341,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/actions/isPullRequestMergeable/index.js b/.github/actions/isPullRequestMergeable/index.js index 3e98b961c4f7..b03366d0c9dd 100644 --- a/.github/actions/isPullRequestMergeable/index.js +++ b/.github/actions/isPullRequestMergeable/index.js @@ -186,6 +186,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -255,6 +257,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -264,6 +268,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -336,6 +342,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/actions/isStagingDeployLocked/index.js b/.github/actions/isStagingDeployLocked/index.js index 4ef5e5aa3c6e..a0e47bbe0662 100644 --- a/.github/actions/isStagingDeployLocked/index.js +++ b/.github/actions/isStagingDeployLocked/index.js @@ -149,6 +149,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -218,6 +220,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -227,6 +231,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -299,6 +305,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/actions/markPullRequestsAsDeployed/index.js b/.github/actions/markPullRequestsAsDeployed/index.js index 1ad199a5f4cf..4f881fae56c4 100644 --- a/.github/actions/markPullRequestsAsDeployed/index.js +++ b/.github/actions/markPullRequestsAsDeployed/index.js @@ -318,6 +318,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -387,6 +389,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -396,6 +400,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -468,6 +474,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/actions/reopenIssueWithComment/index.js b/.github/actions/reopenIssueWithComment/index.js index 42d09f1c766e..7c0219f6e2b3 100644 --- a/.github/actions/reopenIssueWithComment/index.js +++ b/.github/actions/reopenIssueWithComment/index.js @@ -160,6 +160,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -229,6 +231,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -238,6 +242,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -310,6 +316,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/actions/triggerWorkflowAndWait/index.js b/.github/actions/triggerWorkflowAndWait/index.js index 86a6f5039af5..a04038faa3c0 100644 --- a/.github/actions/triggerWorkflowAndWait/index.js +++ b/.github/actions/triggerWorkflowAndWait/index.js @@ -329,6 +329,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -398,6 +400,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -407,6 +411,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -479,6 +485,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/actions/verifySignedCommits/index.js b/.github/actions/verifySignedCommits/index.js index 8060ed706572..406598e3db99 100644 --- a/.github/actions/verifySignedCommits/index.js +++ b/.github/actions/verifySignedCommits/index.js @@ -149,6 +149,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -218,6 +220,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -227,6 +231,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -299,6 +305,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/libs/GithubUtils.js b/.github/libs/GithubUtils.js index be5fe177c54a..220407dd1f2f 100644 --- a/.github/libs/GithubUtils.js +++ b/.github/libs/GithubUtils.js @@ -109,6 +109,8 @@ class GithubUtils { labels: issue.labels, PRList: this.getStagingDeployCashPRList(issue), deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body), + isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body), tag, }; } catch (exception) { @@ -178,6 +180,8 @@ class GithubUtils { * @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @param {Boolean} [isTimingDashboardChecked] + * @param {Boolean} [isFirebaseChecked] * @returns {Promise} */ static generateStagingDeployCashBody( @@ -187,6 +191,8 @@ class GithubUtils { accessiblePRList = [], deployBlockers = [], resolvedDeployBlockers = [], + isTimingDashboardChecked = false, + isFirebaseChecked = false, ) { return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL)) .then((data) => { @@ -259,6 +265,12 @@ class GithubUtils { }); } + issueBody += '\r\n\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) diff --git a/.github/workflows/finishReleaseCycle.yml b/.github/workflows/finishReleaseCycle.yml index fe0eb8aff88a..8553f9b63fbb 100644 --- a/.github/workflows/finishReleaseCycle.yml +++ b/.github/workflows/finishReleaseCycle.yml @@ -47,7 +47,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} ISSUE_NUMBER: ${{ github.event.issue.number }} COMMENT: | - This issue either has unchecked QA steps or has not yet been marked with the `:shipit:` emoji of approval. + This issue either has unchecked items or has not yet been marked with the `:shipit:` emoji of approval. Reopening! # Update the production branch to trigger the production deploy. diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index 85ad459978f6..266f998d4f03 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -216,12 +216,18 @@ jobs: IS_EXPENSIFY_EMPLOYEE: ${{ fromJSON(steps.checkActor.outputs.isTeamMember) }} steps: + - name: Get merged pull request + id: getMergedPullRequest + uses: roryabraham/action-get-merged-pull-request@7a7a194f6ff8f3eef58c822083695a97314ebec1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Check whether the actor is member of Expensify/expensify team id: checkActor uses: tspascoal/get-user-teams-membership@baf2e6adf4c3b897bd65a7e3184305c165aec872 with: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - username: ${{ github.actor }} + username: ${{ steps.getMergedPullRequest.outputs.author }} team: Expensify/expensify newContributorWelcomeMessage: diff --git a/.storybook/main.js b/.storybook/main.js index b229d828bfe8..ad5effb2dbf6 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -6,6 +6,7 @@ module.exports = { addons: [ '@storybook/addon-essentials', '@storybook/addon-a11y', + '@storybook/addon-react-native-web', ], staticDirs: [ './public', diff --git a/android/app/build.gradle b/android/app/build.gradle index b3bb898ee2e3..97ba80bd0d8d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -152,8 +152,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001015200 - versionName "1.1.52-0" + versionCode 1001015400 + versionName "1.1.54-0" } splits { abi { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 92b1d3b79059..d310f3305732 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.52 + 1.1.54 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.1.52.0 + 1.1.54.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3fd8e939d051..4e8be96624b7 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.1.52 + 1.1.54 CFBundleSignature ???? CFBundleVersion - 1.1.52.0 + 1.1.54.0 diff --git a/package-lock.json b/package-lock.json index ca2dc8c48c5d..5c7f3b504abd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.1.52-0", + "version": "1.1.54-0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -6710,9 +6710,9 @@ "integrity": "sha512-2Y9OXWHRutYmdyW+bNMaFTW8uTBpaaK20xdIFoVtqahEOO9++B+ut3CAWKPZcdRtb9ikG0LUKChGqMeocg0PGA==" }, "@react-native-picker/picker": { - "version": "1.9.11", - "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-1.9.11.tgz", - "integrity": "sha512-E7whvvMIl9Ln1sxgug7OAEVWQ69n82iV0d2OWWp5VV52+RW3azKh1IFm4rxdW5/oByMfl7FFL0eHNelGgY4BMQ==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.3.1.tgz", + "integrity": "sha512-DAw1o3bHNRnQPImsK53xCYkgC8bH+t9uTM+0JjNIlmMwvVLFnmDxi9v5iBTIWFMWG/pUHQaw66E29wW6oJG9Tw==" }, "@react-native/assets": { "version": "1.0.0", @@ -8454,6 +8454,12 @@ } } }, + "@storybook/addon-react-native-web": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@storybook/addon-react-native-web/-/addon-react-native-web-0.0.18.tgz", + "integrity": "sha512-tMZumiF+Dgk7sngMWFrbDdBN3h5C001traYM46CqNQvtJoZvwYe2MPGEeWy3wk+5D8vOv8TFw2i96f3wwfralg==", + "dev": true + }, "@storybook/addon-toolbars": { "version": "6.4.12", "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-6.4.12.tgz", diff --git a/package.json b/package.json index b6ed2db2746d..468a96150c38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.1.52-0", + "version": "1.1.54-0", "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.", @@ -50,7 +50,7 @@ "@react-native-firebase/crashlytics": "^12.3.0", "@react-native-firebase/perf": "^12.3.0", "@react-native-masked-view/masked-view": "^0.2.4", - "@react-native-picker/picker": "^1.9.11", + "@react-native-picker/picker": "^2.3.1", "@react-navigation/compat": "^5.3.20", "@react-navigation/drawer": "6.1.8", "@react-navigation/native": "6.0.8", @@ -126,6 +126,7 @@ "@react-native-community/eslint-config": "^2.0.0", "@storybook/addon-a11y": "^6.4.12", "@storybook/addon-essentials": "^6.4.12", + "@storybook/addon-react-native-web": "0.0.18", "@storybook/addons": "^6.4.12", "@storybook/react": "^6.4.12", "@storybook/theming": "^6.4.12", diff --git a/src/CONST.js b/src/CONST.js index 4ffeaba8f616..de79fdcb71cc 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -380,6 +380,8 @@ const CONST = { DECIMAL_PAD: 'decimal-pad', }, + ATTACHMENT_SOURCE_ATTRIBUTE: 'data-expensify-source', + ATTACHMENT_PICKER_TYPE: { FILE: 'file', IMAGE: 'image', diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index f3837f8ad61d..39d1a8f9dd09 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -243,7 +243,7 @@ class AddPlaidBankAccount extends React.Component { { + onInputChange={(index) => { this.setState({selectedIndex: Number(index)}); this.clearError('selectedBank'); }} diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 4fe540bcc813..21ec2d724f15 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -256,6 +256,9 @@ class EmojiPickerMenu extends Component { const isHeader = e => e.header || e.spacer; do { newIndex += steps; + if (newIndex < 0) { + break; + } } while (isHeader(this.state.filteredEmojis[newIndex])); }; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js index 6f53780a4eb4..e768e59bbf98 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js @@ -16,7 +16,7 @@ const AnchorRenderer = (props) => { const htmlAttribs = props.tnode.attributes; // An auth token is needed to download Expensify chat attachments - const isAttachment = Boolean(htmlAttribs['data-expensify-source']); + const isAttachment = Boolean(htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]); const displayName = lodashGet(props.tnode, 'domNode.children[0].data', ''); const parentStyle = lodashGet(props.tnode, 'parent.styles.nativeTextRet', {}); const attrHref = htmlAttribs.href || ''; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js index e1f9b356ab97..152e2f03749c 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js @@ -5,6 +5,7 @@ import AttachmentModal from '../../AttachmentModal'; import styles from '../../../styles/styles'; import ThumbnailImage from '../../ThumbnailImage'; import TouchableWithoutFocus from '../../TouchableWithoutFocus'; +import CONST from '../../../CONST'; const ImageRenderer = (props) => { const htmlAttribs = props.tnode.attributes; @@ -14,7 +15,7 @@ const ImageRenderer = (props) => { // - Chat Attachment images // // Images uploaded by the user via the app or email. - // These have a full-sized image `htmlAttribs['data-expensify-source']` + // These have a full-sized image `htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]` // and a thumbnail `htmlAttribs.src`. Both of these URLs need to have // an authToken added to them in order to control who // can see the images. @@ -26,11 +27,11 @@ const ImageRenderer = (props) => { // Concierge responder attachments are uploaded to S3 without any access // control and thus require no authToken to verify access. // - const isAttachment = Boolean(htmlAttribs['data-expensify-source']); + const isAttachment = Boolean(htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]); const originalFileName = htmlAttribs['data-name']; let previewSource = htmlAttribs.src; let source = isAttachment - ? htmlAttribs['data-expensify-source'] + ? htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] : htmlAttribs.src; // Update the image URL so the images can be accessed depending on the config environment diff --git a/src/components/LocalePicker.js b/src/components/LocalePicker.js index 30eba46aba9e..aafb06ea6fd2 100644 --- a/src/components/LocalePicker.js +++ b/src/components/LocalePicker.js @@ -49,7 +49,7 @@ const LocalePicker = (props) => { return ( { + onInputChange={(locale) => { if (locale === props.preferredLocale) { return; } diff --git a/src/components/Picker/BasePicker/basePickerPropTypes.js b/src/components/Picker/BasePicker/basePickerPropTypes.js index 02f5ff2d2b6d..7af66b59bdf6 100644 --- a/src/components/Picker/BasePicker/basePickerPropTypes.js +++ b/src/components/Picker/BasePicker/basePickerPropTypes.js @@ -6,7 +6,7 @@ import * as Expensicons from '../../Icon/Expensicons'; const propTypes = { /** A callback method that is called when the value changes and it received the selected value as an argument */ - onChange: PropTypes.func.isRequired, + onInputChange: PropTypes.func.isRequired, /** Whether or not to show the disabled styles */ disabled: PropTypes.bool, diff --git a/src/components/Picker/BasePicker/index.js b/src/components/Picker/BasePicker/index.js index 75d06cfbc84c..a56c7d9f0d29 100644 --- a/src/components/Picker/BasePicker/index.js +++ b/src/components/Picker/BasePicker/index.js @@ -1,32 +1,53 @@ import React from 'react'; import RNPickerSelect from 'react-native-picker-select'; +import _ from 'underscore'; import styles from '../../../styles/styles'; import * as basePickerPropTypes from './basePickerPropTypes'; import basePickerStyles from './basePickerStyles'; -const BasePicker = props => ( - props.icon(props.size)} - disabled={props.disabled} - fixAndroidTouchableBug - onOpen={props.onOpen} - onClose={props.onClose} - pickerProps={{ - onFocus: props.onOpen, - onBlur: props.onClose, - }} - /> -); +class BasePicker extends React.Component { + constructor(props) { + super(props); + + this.state = { + selectedValue: this.props.value || this.props.defaultValue, + }; + + this.updateSelectedValueAndExecuteOnChange = this.updateSelectedValueAndExecuteOnChange.bind(this); + } + + updateSelectedValueAndExecuteOnChange(value) { + this.props.onInputChange(value); + this.setState({selectedValue: value}); + } + + render() { + const hasError = !_.isEmpty(this.props.errorText); + return ( + this.props.icon(this.props.size)} + disabled={this.props.disabled} + fixAndroidTouchableBug + onOpen={this.props.onOpen} + onClose={this.props.onClose} + pickerProps={{ + onFocus: this.props.onOpen, + onBlur: this.props.onBlur, + ref: this.props.innerRef, + }} + /> + ); + } +} BasePicker.propTypes = basePickerPropTypes.propTypes; BasePicker.defaultProps = basePickerPropTypes.defaultProps; -BasePicker.displayName = 'BasePicker'; export default BasePicker; diff --git a/src/components/Picker/index.js b/src/components/Picker/index.js index b7277fcda73e..5f2f86374854 100644 --- a/src/components/Picker/index.js +++ b/src/components/Picker/index.js @@ -6,6 +6,7 @@ import BasePicker from './BasePicker'; import Text from '../Text'; import styles from '../../styles/styles'; import InlineErrorText from '../InlineErrorText'; +import * as FormUtils from '../../libs/FormUtils'; const propTypes = { /** Picker label */ @@ -14,22 +15,39 @@ const propTypes = { /** Should the picker appear disabled? */ isDisabled: PropTypes.bool, - /** Should the input be styled for errors */ - hasError: PropTypes.bool, + /** Input value */ + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), /** Error text to display */ errorText: PropTypes.string, /** Customize the Picker container */ containerStyles: PropTypes.arrayOf(PropTypes.object), + + /** Indicates that the input is being used with the Form component */ + isFormInput: PropTypes.bool, + + /** + * The ID used to uniquely identify the input + * + * @param {Object} props - props passed to the input + * @returns {Object} - returns an Error object if isFormInput is supplied but inputID is falsey or not a string + */ + inputID: props => FormUtils.validateInputIDProps(props), + + /** Saves a draft of the input value when used in a form */ + shouldSaveDraft: PropTypes.bool, }; const defaultProps = { label: '', isDisabled: false, - hasError: false, errorText: '', containerStyles: [], + isFormInput: false, + inputID: undefined, + shouldSaveDraft: false, + value: undefined, }; class Picker extends PureComponent { @@ -48,6 +66,7 @@ class Picker extends PureComponent { style={[ styles.pickerContainer, this.props.isDisabled && styles.inputDisabled, + ...this.props.containerStyles, ]} > {this.props.label && ( @@ -58,12 +77,13 @@ class Picker extends PureComponent { onClose={() => this.setState({isOpen: false})} disabled={this.props.isDisabled} focused={this.state.isOpen} - hasError={this.props.hasError} + errorText={this.props.errorText} + value={this.props.value} // eslint-disable-next-line react/jsx-props-no-spreading {...pickerProps} /> - + {this.props.errorText} @@ -74,4 +94,5 @@ class Picker extends PureComponent { Picker.propTypes = propTypes; Picker.defaultProps = defaultProps; -export default Picker; +// eslint-disable-next-line react/jsx-props-no-spreading +export default React.forwardRef((props, ref) => ); diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js index d9fbe810eeb4..11d4cad09f55 100644 --- a/src/components/StatePicker.js +++ b/src/components/StatePicker.js @@ -32,7 +32,7 @@ const StatePicker = props => ( option.login !== searchValue) && ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue)) || Str.isValidPhone(searchValue)) - && (!_.find(loginOptionsToExclude, loginOptionToExclude => loginOptionToExclude.login === searchValue.toLowerCase())) + && (!_.find(loginOptionsToExclude, loginOptionToExclude => loginOptionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase())) && (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) ) { // If the phone number doesn't have an international code then let's prefix it with the @@ -788,7 +789,7 @@ function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, ma // Without a search value, it would be very confusing to see a search validation message. // Therefore, this skips the validation when there is no search value. if (searchValue && !hasSelectableOptions && !hasUserToInvite) { - if (/^\d+$/.test(searchValue)) { + if (/^\d+$/.test(searchValue) && !Str.isValidPhone(searchValue)) { return Localize.translate(preferredLocale, 'messages.errorMessageInvalidPhone'); } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index ba410c460829..d98f62c4aa21 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -193,7 +193,7 @@ function getSimplifiedReportObject(report) { const createTimestamp = lodashGet(report, 'lastActionCreated', 0); const lastMessageTimestamp = moment.utc(createTimestamp).unix(); const lastActionMessage = lodashGet(report, ['lastActionMessage', 'html'], ''); - const isLastMessageAttachment = /]+)\/>/gi.test(lastActionMessage); + const isLastMessageAttachment = new RegExp(`]*${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}\\s*=\\s*"[^"]*"[^>]*>`, 'gi').test(lastActionMessage); const chatType = lodashGet(report, ['reportNameValuePairs', 'chatType'], ''); let lastMessageText = null; diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 31847224d9dc..6a95f2e56b44 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -268,7 +268,7 @@ class CompanyStep extends React.Component { ({value, label}))} - onChange={value => this.clearErrorAndSetValue('incorporationType', value)} + onInputChange={value => this.clearErrorAndSetValue('incorporationType', value)} value={this.state.incorporationType} placeholder={{value: '', label: '-'}} hasError={this.getErrors().incorporationType} diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js index 6ee169d0256d..a8a213d290cf 100644 --- a/src/pages/ReportSettingsPage.js +++ b/src/pages/ReportSettingsPage.js @@ -168,7 +168,7 @@ class ReportSettingsPage extends Component { { + onInputChange={(notificationPreference) => { Report.updateNotificationPreference( this.props.report.reportID, notificationPreference, diff --git a/src/pages/settings/PreferencesPage.js b/src/pages/settings/PreferencesPage.js index 4752dfc291d8..ae7de7e4f51f 100755 --- a/src/pages/settings/PreferencesPage.js +++ b/src/pages/settings/PreferencesPage.js @@ -85,7 +85,7 @@ const PreferencesPage = (props) => { NameValuePair.set(CONST.NVP.PRIORITY_MODE, mode, ONYXKEYS.NVP_PRIORITY_MODE) } items={_.values(priorityModes)} diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index cf4ecd2e5ce2..1dc7a502e788 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -250,7 +250,7 @@ class ProfilePage extends Component { { + onInputChange={(pronouns) => { const hasSelfSelectedPronouns = pronouns === CONST.PRONOUNS.SELF_SELECT; this.setState({ pronouns: hasSelfSelectedPronouns ? '' : pronouns, @@ -288,7 +288,7 @@ class ProfilePage extends Component { this.setState({selectedTimezone})} + onInputChange={selectedTimezone => this.setState({selectedTimezone})} items={timezones} isDisabled={this.state.isAutomaticTimezone} value={this.state.selectedTimezone} diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 780bf75f487e..a31dad1708c5 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -173,7 +173,7 @@ class WorkspaceNewRoomPage extends React.Component { items={this.state.workspaceOptions} errorText={this.state.errors.policyID} hasError={Boolean(this.state.errors.policyID)} - onChange={policyID => this.clearErrorAndSetValue('policyID', policyID)} + onInputChange={policyID => this.clearErrorAndSetValue('policyID', policyID)} /> @@ -181,7 +181,7 @@ class WorkspaceNewRoomPage extends React.Component { value={this.state.visibility} label={this.props.translate('newRoomPage.visibility')} items={visibilityOptions} - onChange={visibility => this.setState({visibility})} + onInputChange={visibility => this.setState({visibility})} /> diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index ee9989d86ff3..a0bb45fcbe6b 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -168,7 +168,7 @@ class WorkspaceSettingsPage extends React.Component { this.setState({currency})} + onInputChange={currency => this.setState({currency})} items={this.getCurrencyItems()} value={this.state.currency} isDisabled={hasVBA} diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js index 6e68c98488cd..e068a8666eeb 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js @@ -181,7 +181,7 @@ class WorkspaceReimburseNoVBAView extends React.Component { label={this.props.translate('workspace.reimburse.trackDistanceUnit')} items={this.unitItems} value={this.state.unitValue} - onChange={value => this.setUnit(value)} + onInputChange={value => this.setUnit(value)} /> diff --git a/src/stories/Form.stories.js b/src/stories/Form.stories.js index 7de80c6192af..e0e551bb17c9 100644 --- a/src/stories/Form.stories.js +++ b/src/stories/Form.stories.js @@ -1,6 +1,7 @@ import React, {useState} from 'react'; import {View} from 'react-native'; import TextInput from '../components/TextInput'; +import Picker from '../components/Picker'; import AddressSearch from '../components/AddressSearch'; import Form from '../components/Form'; import * as FormActions from '../libs/actions/FormActions'; @@ -16,7 +17,12 @@ import Text from '../components/Text'; const story = { title: 'Components/Form', component: Form, - subcomponents: {TextInput, AddressSearch, CheckboxWithLabel}, + subcomponents: { + TextInput, + AddressSearch, + CheckboxWithLabel, + Picker, + }, }; const Template = (args) => { @@ -50,6 +56,49 @@ const Template = (args) => { containerStyles={[styles.mt4]} isFormInput /> + + + + ; + +// Arguments can be passed to the component by binding +// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args + +const Default = Template.bind({}); +Default.args = { + label: 'Default picker', + name: 'Default', + onInputChange: () => {}, + items: [ + { + label: 'Orange', + value: 'orange', + }, + { + label: 'Apple', + value: 'apple', + }, + ], +}; + +const PickerWithValue = Template.bind({}); +PickerWithValue.args = { + label: 'Picker with defined value', + name: 'Picker with defined value', + onInputChange: () => {}, + value: 'apple', + items: [ + { + label: 'Orange', + value: 'orange', + }, + { + label: 'Apple', + value: 'apple', + }, + ], +}; + +const ErrorStory = Template.bind({}); +ErrorStory.args = { + label: 'Picker with error', + name: 'PickerWithError', + errorText: 'This field has an error.', + onInputChange: () => {}, + items: [ + { + label: 'Orange', + value: 'orange', + }, + { + label: 'Apple', + value: 'apple', + }, + ], +}; + +const Disabled = Template.bind({}); +Disabled.args = { + label: 'Picker disabled', + name: 'Disabled', + isDisabled: true, + onInputChange: () => {}, + items: [ + { + label: 'Orange', + value: 'orange', + }, + { + label: 'Apple', + value: 'apple', + }, + ], +}; + +export default story; +export { + Default, + PickerWithValue, + ErrorStory, + Disabled, +}; diff --git a/tests/unit/GithubUtilsTest.js b/tests/unit/GithubUtilsTest.js index ef8818a293ac..a5c8e8a2687f 100644 --- a/tests/unit/GithubUtilsTest.js +++ b/tests/unit/GithubUtilsTest.js @@ -68,6 +68,8 @@ describe('GithubUtils', () => { url: 'https://api.github.com/repos/Andrew-Test-Org/Public-Test-Repo/issues/29', number: 29, deployBlockers: [], + isTimingDashboardChecked: false, + isFirebaseChecked: false, }; const expectedResponseWithDeployBlockers = {...baseExpectedResponse}; expectedResponseWithDeployBlockers.deployBlockers = [ @@ -375,6 +377,11 @@ describe('GithubUtils', () => { const lineBreakDouble = '\r\n\r\n'; const indent = ' '; const assignOctocatHubot = ' - @octocat @hubot'; + const deployerVerificationsHeader = '\r\n**Deployer verifications:**'; + // eslint-disable-next-line max-len + const timingDashboardVerification = 'I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.'; + // eslint-disable-next-line max-len + const firebaseVerification = 'I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.'; // Valid output which will be reused in the deploy blocker tests const allVerifiedExpectedOutput = `${baseExpectedOutput}` @@ -394,6 +401,9 @@ describe('GithubUtils', () => { + `${lineBreakDouble}${listStart}${basePRList[1]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}` + `${lineBreakDouble}${listStart}${basePRList[5]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}` + `${lineBreakDouble}${listStart}${basePRList[6]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${openCheckbox}${timingDashboardVerification}` + + `${lineBreak}${openCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, ); }) @@ -409,6 +419,9 @@ describe('GithubUtils', () => { + `${lineBreakDouble}${listStart}${basePRList[1]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}` + `${lineBreakDouble}${listStart}${basePRList[5]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}` + `${lineBreakDouble}${listStart}${basePRList[6]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${openCheckbox}${timingDashboardVerification}` + + `${lineBreak}${openCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, ); }) @@ -424,6 +437,9 @@ describe('GithubUtils', () => { + `${lineBreakDouble}${listStart}${basePRList[1]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}` + `${lineBreakDouble}${listStart}${basePRList[5]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}` + `${lineBreakDouble}${listStart}${basePRList[6]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${openCheckbox}${timingDashboardVerification}` + + `${lineBreak}${openCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, ); }) @@ -433,7 +449,11 @@ describe('GithubUtils', () => { githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList) .then((issueBody) => { expect(issueBody).toBe( - `${allVerifiedExpectedOutput}${lineBreakDouble}${ccApplauseLeads}`, + `${allVerifiedExpectedOutput}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${openCheckbox}${timingDashboardVerification}` + + `${lineBreak}${openCheckbox}${firebaseVerification}` + + `${lineBreakDouble}${ccApplauseLeads}`, ); }) )); @@ -446,6 +466,9 @@ describe('GithubUtils', () => { + `${lineBreakDouble}${deployBlockerHeader}` + `${lineBreak}${openCheckbox}${baseDeployBlockerList[0]}` + `${lineBreak}${openCheckbox}${baseDeployBlockerList[1]}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${openCheckbox}${timingDashboardVerification}` + + `${lineBreak}${openCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, ); }) @@ -459,6 +482,9 @@ describe('GithubUtils', () => { + `${lineBreakDouble}${deployBlockerHeader}` + `${lineBreak}${closedCheckbox}${baseDeployBlockerList[0]}` + `${lineBreak}${openCheckbox}${baseDeployBlockerList[1]}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${openCheckbox}${timingDashboardVerification}` + + `${lineBreak}${openCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, ); }) @@ -477,6 +503,9 @@ describe('GithubUtils', () => { + `${lineBreakDouble}${deployBlockerHeader}` + `${lineBreak}${closedCheckbox}${baseDeployBlockerList[0]}` + `${lineBreak}${closedCheckbox}${baseDeployBlockerList[1]}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${openCheckbox}${timingDashboardVerification}` + + `${lineBreak}${openCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, ); }) @@ -495,6 +524,9 @@ describe('GithubUtils', () => { + `${lineBreakDouble}${internalQAHeader}` + `${lineBreak}${openCheckbox}${internalQAPRList[0]}${assignOctocatHubot}` + `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignOctocatHubot}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${openCheckbox}${timingDashboardVerification}` + + `${lineBreak}${openCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, ); }) @@ -513,6 +545,9 @@ describe('GithubUtils', () => { + `${lineBreakDouble}${internalQAHeader}` + `${lineBreak}${closedCheckbox}${internalQAPRList[0]}${assignOctocatHubot}` + `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignOctocatHubot}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${openCheckbox}${timingDashboardVerification}` + + `${lineBreak}${openCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, ); }) diff --git a/tests/unit/createOrUpdateStagingDeployTest.js b/tests/unit/createOrUpdateStagingDeployTest.js index 90307c3f469c..3d9d5400e525 100644 --- a/tests/unit/createOrUpdateStagingDeployTest.js +++ b/tests/unit/createOrUpdateStagingDeployTest.js @@ -102,6 +102,11 @@ const closedCheckbox = '- [x] '; const listStart = '- '; const QA = 'QA'; const accessibility = 'Accessibility'; +const deployerVerificationsHeader = '\r\n**Deployer verifications:**'; +// eslint-disable-next-line max-len +const timingDashboardVerification = 'I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.'; +// eslint-disable-next-line max-len +const firebaseVerification = 'I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.'; const ccApplauseLeads = 'cc @Expensify/applauseleads\r\n'; const deployBlockerHeader = '\r\n**Deploy Blockers:**'; const lineBreak = '\r\n'; @@ -169,6 +174,9 @@ describe('createOrUpdateStagingDeployCash', () => { + `${lineBreakDouble}${listStart}${basePRList[5]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}` + `${lineBreakDouble}${listStart}${basePRList[6]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}` + `${lineBreakDouble}${listStart}${basePRList[7]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${openCheckbox}${timingDashboardVerification}` + + `${lineBreak}${openCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, }); }); @@ -189,6 +197,9 @@ describe('createOrUpdateStagingDeployCash', () => { + `${lineBreak}${openCheckbox}${basePRList[5]}` + `${lineBreak}${openCheckbox}${basePRList[8]}` + `${lineBreak}${closedCheckbox}${basePRList[9]}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${closedCheckbox}${timingDashboardVerification}` + + `${lineBreak}${closedCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, state: 'open', }; @@ -285,12 +296,23 @@ describe('createOrUpdateStagingDeployCash', () => { + `${lineBreak}${closedCheckbox}${basePRList[9]}` + `${lineBreak}${openCheckbox}${basePRList[10]}` + `${lineBreak}${openCheckbox}${basePRList[11]}` + + `${lineBreak}${deployerVerificationsHeader}` + + // Note: these will be unchecked with a new app version, and that's intentional + + `${lineBreak}${openCheckbox}${timingDashboardVerification}` + + `${lineBreak}${openCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, }); }); }); test('without NPM_VERSION input, just a new deploy blocker', () => { + mockGetInput.mockImplementation((arg) => { + if (arg !== 'GITHUB_TOKEN') { + return; + } + return 'fake_token'; + }); mockGetPullRequestsMergedBetween.mockImplementation((fromRef, toRef) => { if (fromRef === '1.0.1-0' && toRef === '1.0.2-2') { return [ @@ -335,7 +357,7 @@ describe('createOrUpdateStagingDeployCash', () => { // eslint-disable-next-line max-len html_url: `https://github.com/Expensify/App/issues/${openStagingDeployCashBefore.number}`, // eslint-disable-next-line max-len - body: `${baseExpectedOutput('1.0.2-2')}` + body: `${baseExpectedOutput('1.0.2-1')}` + `${lineBreakDouble}${listStart}${basePRList[5]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}` + `${lineBreakDouble}${listStart}${basePRList[6]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}` + `${lineBreakDouble}${listStart}${basePRList[7]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}` @@ -345,6 +367,9 @@ describe('createOrUpdateStagingDeployCash', () => { + `${lineBreak}${closedCheckbox}${basePRList[9]}` + `${lineBreak}${openCheckbox}${baseIssueList[0]}` + `${lineBreak}${openCheckbox}${baseIssueList[1]}` + + `${lineBreak}${deployerVerificationsHeader}` + + `${lineBreak}${closedCheckbox}${timingDashboardVerification}` + + `${lineBreak}${closedCheckbox}${firebaseVerification}` + `${lineBreakDouble}${ccApplauseLeads}`, }); });