diff --git a/.eslintrc.js b/.eslintrc.js
index c0c95d3f5686..5451cfff6534 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,13 +1,14 @@
const restrictedImportPaths = [
{
name: 'react-native',
- importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text'],
+ importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text', 'ScrollView'],
message: [
'',
"For 'useWindowDimensions', please use 'src/hooks/useWindowDimensions' instead.",
"For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from 'src/components/Pressable' instead.",
"For 'StatusBar', please use 'src/libs/StatusBar' instead.",
"For 'Text', please use '@components/Text' instead.",
+ "For 'ScrollView', please use '@components/ScrollView' instead.",
].join('\n'),
},
{
diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js
index 528a0a11498a..e267769dc457 100644
--- a/.github/actions/javascript/authorChecklist/index.js
+++ b/.github/actions/javascript/authorChecklist/index.js
@@ -283,14 +283,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -325,11 +325,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -359,7 +359,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js
index f042dbb38a91..dd2aef38e1ee 100644
--- a/.github/actions/javascript/awaitStagingDeploys/index.js
+++ b/.github/actions/javascript/awaitStagingDeploys/index.js
@@ -395,14 +395,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -437,11 +437,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -471,7 +471,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js
index 8e10f8b1d8b6..82092be7e0eb 100644
--- a/.github/actions/javascript/checkDeployBlockers/index.js
+++ b/.github/actions/javascript/checkDeployBlockers/index.js
@@ -362,14 +362,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -404,11 +404,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -438,7 +438,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js
index 4441348a3c36..1752ae62f86c 100644
--- a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js
+++ b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js
@@ -40,8 +40,11 @@ async function run() {
// Next, we generate the checklist body
let checklistBody = '';
+ let checklistAssignees = [];
if (shouldCreateNewDeployChecklist) {
- checklistBody = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber));
+ const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber));
+ checklistBody = issueBody;
+ checklistAssignees = issueAssignees;
} else {
// Generate the updated PR list, preserving the previous state of `isVerified` for existing PRs
const PRList = _.reduce(
@@ -94,7 +97,7 @@ async function run() {
}
const didVersionChange = newVersionTag !== currentChecklistData.tag;
- checklistBody = await GithubUtils.generateStagingDeployCashBody(
+ const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody(
newVersionTag,
_.pluck(PRList, 'url'),
_.pluck(_.where(PRList, {isVerified: true}), 'url'),
@@ -105,6 +108,8 @@ async function run() {
didVersionChange ? false : currentChecklistData.isFirebaseChecked,
didVersionChange ? false : currentChecklistData.isGHStatusChecked,
);
+ checklistBody = issueBody;
+ checklistAssignees = issueAssignees;
}
// Finally, create or update the checklist
@@ -119,7 +124,7 @@ async function run() {
...defaultPayload,
title: `Deploy Checklist: New Expensify ${format(new Date(), CONST.DATE_FORMAT_STRING)}`,
labels: [CONST.LABELS.STAGING_DEPLOY],
- assignees: [CONST.APPLAUSE_BOT],
+ assignees: [CONST.APPLAUSE_BOT].concat(checklistAssignees),
});
console.log(`Successfully created new StagingDeployCash! 🎉 ${newChecklist.html_url}`);
return newChecklist;
diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js
index 154dacbdc3c3..9c9a42709af0 100644
--- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js
+++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js
@@ -49,8 +49,11 @@ async function run() {
// Next, we generate the checklist body
let checklistBody = '';
+ let checklistAssignees = [];
if (shouldCreateNewDeployChecklist) {
- checklistBody = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber));
+ const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber));
+ checklistBody = issueBody;
+ checklistAssignees = issueAssignees;
} else {
// Generate the updated PR list, preserving the previous state of `isVerified` for existing PRs
const PRList = _.reduce(
@@ -103,7 +106,7 @@ async function run() {
}
const didVersionChange = newVersionTag !== currentChecklistData.tag;
- checklistBody = await GithubUtils.generateStagingDeployCashBody(
+ const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody(
newVersionTag,
_.pluck(PRList, 'url'),
_.pluck(_.where(PRList, {isVerified: true}), 'url'),
@@ -114,6 +117,8 @@ async function run() {
didVersionChange ? false : currentChecklistData.isFirebaseChecked,
didVersionChange ? false : currentChecklistData.isGHStatusChecked,
);
+ checklistBody = issueBody;
+ checklistAssignees = issueAssignees;
}
// Finally, create or update the checklist
@@ -128,7 +133,7 @@ async function run() {
...defaultPayload,
title: `Deploy Checklist: New Expensify ${format(new Date(), CONST.DATE_FORMAT_STRING)}`,
labels: [CONST.LABELS.STAGING_DEPLOY],
- assignees: [CONST.APPLAUSE_BOT],
+ assignees: [CONST.APPLAUSE_BOT].concat(checklistAssignees),
});
console.log(`Successfully created new StagingDeployCash! 🎉 ${newChecklist.html_url}`);
return newChecklist;
@@ -434,14 +439,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -476,11 +481,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -510,7 +515,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js
index ea56ff5f4ebd..e4f7634bd849 100644
--- a/.github/actions/javascript/getArtifactInfo/index.js
+++ b/.github/actions/javascript/getArtifactInfo/index.js
@@ -321,14 +321,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -363,11 +363,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -397,7 +397,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js
index f272929d536a..f941c9524856 100644
--- a/.github/actions/javascript/getDeployPullRequestList/index.js
+++ b/.github/actions/javascript/getDeployPullRequestList/index.js
@@ -377,14 +377,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -419,11 +419,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -453,7 +453,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js
index b8d7d821d64e..f4168af28802 100644
--- a/.github/actions/javascript/getPullRequestDetails/index.js
+++ b/.github/actions/javascript/getPullRequestDetails/index.js
@@ -329,14 +329,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -371,11 +371,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -405,7 +405,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/getReleaseBody/index.js b/.github/actions/javascript/getReleaseBody/index.js
index cc1321ce5cd5..547aafe23038 100644
--- a/.github/actions/javascript/getReleaseBody/index.js
+++ b/.github/actions/javascript/getReleaseBody/index.js
@@ -329,14 +329,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -371,11 +371,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -405,7 +405,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js
index 8124c5795a5a..4938b5bb7745 100644
--- a/.github/actions/javascript/isStagingDeployLocked/index.js
+++ b/.github/actions/javascript/isStagingDeployLocked/index.js
@@ -313,14 +313,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -355,11 +355,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -389,7 +389,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js
index 36cd0aaefe4a..2e6ab7e018dd 100644
--- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js
+++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js
@@ -478,14 +478,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -520,11 +520,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -554,7 +554,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js
index 329e0d3aad5d..9dd23d68ca0a 100644
--- a/.github/actions/javascript/postTestBuildComment/index.js
+++ b/.github/actions/javascript/postTestBuildComment/index.js
@@ -388,14 +388,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -430,11 +430,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -464,7 +464,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js
index 6a5f89badb5e..42196053f63f 100644
--- a/.github/actions/javascript/reopenIssueWithComment/index.js
+++ b/.github/actions/javascript/reopenIssueWithComment/index.js
@@ -283,14 +283,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -325,11 +325,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -359,7 +359,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js
index 322b529b89bf..22335b36bd2b 100644
--- a/.github/actions/javascript/reviewerChecklist/index.js
+++ b/.github/actions/javascript/reviewerChecklist/index.js
@@ -283,14 +283,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -325,11 +325,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -359,7 +359,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js
index ba188d3a2b86..239f20c9d258 100644
--- a/.github/actions/javascript/verifySignedCommits/index.js
+++ b/.github/actions/javascript/verifySignedCommits/index.js
@@ -283,14 +283,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -325,11 +325,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -359,7 +359,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/.github/libs/GithubUtils.js b/.github/libs/GithubUtils.js
index 0cd407c78153..e988167850ec 100644
--- a/.github/libs/GithubUtils.js
+++ b/.github/libs/GithubUtils.js
@@ -250,14 +250,14 @@ class GithubUtils {
.then((data) => {
// The format of this map is following:
// {
- // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ],
- // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ]
+ // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv',
+ // 'https://github.com/Expensify/App/pull/9642': 'mountiny'
// }
const internalQAPRMap = _.reduce(
_.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))),
(map, pr) => {
// eslint-disable-next-line no-param-reassign
- map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login'));
+ map[pr.html_url] = pr.merged_by.login;
return map;
},
{},
@@ -292,11 +292,11 @@ class GithubUtils {
if (!_.isEmpty(internalQAPRMap)) {
console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs);
issueBody += '**Internal QA:**\r\n';
- _.each(internalQAPRMap, (assignees, URL) => {
- const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, '');
+ _.each(internalQAPRMap, (merger, URL) => {
+ const mergerMention = `@${merger}`;
issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `;
issueBody += `${URL}`;
- issueBody += ` -${assigneeMentions}`;
+ issueBody += ` - ${mergerMention}`;
issueBody += '\r\n';
});
issueBody += '\r\n\r\n';
@@ -326,7 +326,9 @@ class GithubUtils {
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
- return issueBody;
+ const issueAssignees = _.values(internalQAPRMap);
+ const issue = {issueBody, issueAssignees};
+ return issue;
})
.catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err));
}
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 73e8e61111dc..cbe11156b093 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001044800
- versionName "1.4.48-0"
+ versionCode 1001044900
+ versionName "1.4.49-0"
}
flavorDimensions "default"
diff --git a/docs/redirects.csv b/docs/redirects.csv
index b9a7826f98b9..097c0ad2679e 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -64,3 +64,4 @@ https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/
https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription,https://use.expensify.com/
https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription,https://use.expensify.com/
https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts
+https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 935fb8a0083f..bccea916e01a 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.48
+ 1.4.49
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.48.0
+ 1.4.49.0
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 9d42e39387b4..058476f03a9d 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.48
+ 1.4.49
CFBundleSignature
????
CFBundleVersion
- 1.4.48.0
+ 1.4.49.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index dd33f988f7c4..869b3aebab44 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 1.4.48
+ 1.4.49
CFBundleVersion
- 1.4.48.0
+ 1.4.49.0
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 8f3a17edec48..bfab80fe3148 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.48-0",
+ "version": "1.4.49-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.48-0",
+ "version": "1.4.49-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 46ee7afd6548..a78c23c3c960 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.48-0",
+ "version": "1.4.49-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.",
diff --git a/src/CONST.ts b/src/CONST.ts
index 6861fe174ffe..70fecab70c39 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -575,6 +575,7 @@ const CONST = {
LIMIT: 50,
TYPE: {
ADDCOMMENT: 'ADDCOMMENT',
+ ACTIONABLEJOINREQUEST: 'ACTIONABLEJOINREQUEST',
APPROVED: 'APPROVED',
CHRONOSOOOLIST: 'CHRONOSOOOLIST',
CLOSED: 'CLOSED',
@@ -678,6 +679,10 @@ const CONST = {
INVITE: 'invited',
NOTHING: 'nothing',
},
+ ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION: {
+ ACCEPT: 'accept',
+ DECLINE: 'decline',
+ },
ARCHIVE_REASON: {
DEFAULT: 'default',
ACCOUNT_CLOSED: 'accountClosed',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 188ab5646d30..31e22491e2b9 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -1,4 +1,4 @@
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type CONST from './CONST';
import type * as FormTypes from './types/form';
@@ -287,6 +287,7 @@ const ONYXKEYS = {
POLICY_MEMBERS: 'policyMembers_',
POLICY_DRAFTS: 'policyDrafts_',
POLICY_MEMBERS_DRAFTS: 'policyMembersDrafts_',
+ POLICY_JOIN_MEMBER: 'policyJoinMember_',
POLICY_CATEGORIES: 'policyCategories_',
POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_',
POLICY_TAGS: 'policyTags_',
@@ -487,6 +488,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.SELECTED_TAB]: string;
[ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string;
[ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep;
+ [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember;
};
type OnyxValuesMapping = {
@@ -589,7 +591,7 @@ type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping;
type OnyxValueKey = keyof OnyxValuesMapping;
type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey;
-type OnyxValue = OnyxEntry;
+type OnyxValue = TOnyxKey extends keyof OnyxCollectionValuesMapping ? OnyxCollection : OnyxEntry;
type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude}`;
/** If this type errors, it means that the `OnyxKey` type is missing some keys. */
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index bc8af4df5686..2ed9fbc3666e 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -478,6 +478,10 @@ const ROUTES = {
route: 'workspace/:policyID/avatar',
getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const,
},
+ WORKSPACE_JOIN_USER: {
+ route: 'workspace/:policyID/join',
+ getRoute: (policyID: string, inviterEmail: string) => `workspace/${policyID}/join?email=${inviterEmail}` as const,
+ },
WORKSPACE_SETTINGS_CURRENCY: {
route: 'workspace/:policyID/settings/currency',
getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const,
@@ -554,6 +558,15 @@ const ROUTES = {
route: 'workspace/:policyID/tags',
getRoute: (policyID: string) => `workspace/${policyID}/tags` as const,
},
+ WORKSPACE_MEMBER_DETAILS: {
+ route: 'workspace/:policyID/members/:accountID',
+ getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}`, backTo),
+ },
+ WORKSPACE_MEMBER_ROLE_SELECTION: {
+ route: 'workspace/:policyID/members/:accountID/role-selection',
+ getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}/role-selection`, backTo),
+ },
+
// Referral program promotion
REFERRAL_DETAILS_MODAL: {
route: 'referral/:contentType',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 17073d289f8f..6fc61aec61a0 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -128,6 +128,7 @@ const SCREENS = {
SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop',
DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect',
SAML_SIGN_IN: 'SAMLSignIn',
+ WORKSPACE_JOIN_USER: 'WorkspaceJoinUser',
MONEY_REQUEST: {
MANUAL_TAB: 'manual',
@@ -226,6 +227,8 @@ const SCREENS = {
CATEGORY_CREATE: 'Category_Create',
CATEGORY_SETTINGS: 'Category_Settings',
CATEGORIES_SETTINGS: 'Categories_Settings',
+ MEMBER_DETAILS: 'Workspace_Member_Details',
+ MEMBER_DETAILS_ROLE_SELECTION: 'Workspace_Member_Details_Role_Selection',
},
EDIT_REQUEST: {
diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx
index 39c91c2a0789..a2e3f5d9948e 100644
--- a/src/components/AddressSearch/index.tsx
+++ b/src/components/AddressSearch/index.tsx
@@ -1,11 +1,12 @@
import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {ForwardedRef} from 'react';
-import {ActivityIndicator, Keyboard, LogBox, ScrollView, View} from 'react-native';
+import {ActivityIndicator, Keyboard, LogBox, View} from 'react-native';
import type {LayoutChangeEvent} from 'react-native';
import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete';
import type {GooglePlaceData, GooglePlaceDetail} from 'react-native-google-places-autocomplete';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import LocationErrorMessage from '@components/LocationErrorMessage';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx
index eed40d75387e..c22958ff53c0 100755
--- a/src/components/AttachmentModal.tsx
+++ b/src/components/AttachmentModal.tsx
@@ -446,7 +446,7 @@ function AttachmentModal({
onSelected: () => downloadAttachment(),
});
}
- if (TransactionUtils.hasReceipt(transaction) && !TransactionUtils.isReceiptBeingScanned(transaction) && canEditReceipt) {
+ if (TransactionUtils.hasReceipt(transaction) && !TransactionUtils.isReceiptBeingScanned(transaction) && canEditReceipt && !TransactionUtils.hasMissingSmartscanFields(transaction)) {
menuItems.push({
icon: Expensicons.Trashcan,
text: translate('receipt.deleteReceipt'),
diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js
index f6a56dc73088..461548f0d2b1 100755
--- a/src/components/Attachments/AttachmentView/index.js
+++ b/src/components/Attachments/AttachmentView/index.js
@@ -1,7 +1,7 @@
import Str from 'expensify-common/lib/str';
import PropTypes from 'prop-types';
import React, {memo, useEffect, useState} from 'react';
-import {ActivityIndicator, ScrollView, View} from 'react-native';
+import {ActivityIndicator, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import * as AttachmentsPropTypes from '@components/Attachments/propTypes';
@@ -9,6 +9,7 @@ import DistanceEReceipt from '@components/DistanceEReceipt';
import EReceipt from '@components/EReceipt';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx
index 5be33e6ff2ec..635645b0035b 100644
--- a/src/components/Badge.tsx
+++ b/src/components/Badge.tsx
@@ -2,8 +2,12 @@ import React, {useCallback} from 'react';
import type {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
import CONST from '@src/CONST';
+import type IconAsset from '@src/types/utils/IconAsset';
+import Icon from './Icon';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import Text from './Text';
@@ -31,11 +35,29 @@ type BadgeProps = {
/** Callback to be called on onPress */
onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void;
+
+ /** The icon asset to display to the left of the text */
+ icon?: IconAsset | null;
+
+ /** Any additional styles to pass to the left icon container. */
+ iconStyles?: StyleProp;
};
-function Badge({success = false, error = false, pressable = false, text, environment = CONST.ENVIRONMENT.DEV, badgeStyles, textStyles, onPress = () => {}}: BadgeProps) {
+function Badge({
+ success = false,
+ error = false,
+ pressable = false,
+ text,
+ environment = CONST.ENVIRONMENT.DEV,
+ badgeStyles,
+ textStyles,
+ onPress = () => {},
+ icon,
+ iconStyles = [],
+}: BadgeProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const theme = useTheme();
const textColorStyles = success || error ? styles.textWhite : undefined;
const Wrapper = pressable ? PressableWithoutFeedback : View;
@@ -53,6 +75,16 @@ function Badge({success = false, error = false, pressable = false, text, environ
aria-label={!pressable ? text : undefined}
accessible={false}
>
+ {icon && (
+
+
+
+ )}
,
+ ref: ForwardedRef,
) {
const theme = useTheme();
const styles = useThemeStyles();
diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx
index 941d63c1bf94..fda0c5441734 100644
--- a/src/components/DistanceEReceipt.tsx
+++ b/src/components/DistanceEReceipt.tsx
@@ -1,5 +1,5 @@
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import EReceiptBackground from '@assets/images/eReceipt_background.svg';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -15,6 +15,7 @@ import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import ImageSVG from './ImageSVG';
import PendingMapView from './MapView/PendingMapView';
+import ScrollView from './ScrollView';
import Text from './Text';
import ThumbnailImage from './ThumbnailImage';
diff --git a/src/components/DistanceRequest/index.tsx b/src/components/DistanceRequest/index.tsx
index 9900656057ce..8920c9a4a92b 100644
--- a/src/components/DistanceRequest/index.tsx
+++ b/src/components/DistanceRequest/index.tsx
@@ -2,6 +2,7 @@ import type {RouteProp} from '@react-navigation/native';
import lodashIsEqual from 'lodash/isEqual';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
import type {ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
diff --git a/src/components/DraggableList/index.tsx b/src/components/DraggableList/index.tsx
index dc78a3ce6222..418f3e93eac4 100644
--- a/src/components/DraggableList/index.tsx
+++ b/src/components/DraggableList/index.tsx
@@ -1,7 +1,9 @@
import React, {useCallback} from 'react';
import {DragDropContext, Draggable, Droppable} from 'react-beautiful-dnd';
import type {OnDragEndResponder} from 'react-beautiful-dnd';
-import {ScrollView} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {ScrollView as RNScrollView} from 'react-native';
+import ScrollView from '@components/ScrollView';
import useThemeStyles from '@hooks/useThemeStyles';
import type {DraggableListProps} from './types';
import useDraggableInPortal from './useDraggableInPortal';
@@ -37,7 +39,7 @@ function DraggableList(
// eslint-disable-next-line @typescript-eslint/naming-convention
ListFooterComponent,
}: DraggableListProps,
- ref: React.ForwardedRef,
+ ref: React.ForwardedRef,
) {
const styles = useThemeStyles();
/**
diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx
index 9b68916c4003..88938f31cd79 100644
--- a/src/components/FloatingActionButton.tsx
+++ b/src/components/FloatingActionButton.tsx
@@ -1,6 +1,7 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useEffect, useRef} from 'react';
-import type {GestureResponderEvent, Role} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {GestureResponderEvent, Role, Text} from 'react-native';
import {Platform, View} from 'react-native';
import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import Svg, {Path} from 'react-native-svg';
@@ -58,12 +59,12 @@ type FloatingActionButtonProps = {
role: Role;
};
-function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef) {
+function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef) {
const {success, buttonDefaultBG, textLight, textDark} = useTheme();
const styles = useThemeStyles();
const borderRadius = styles.floatingActionButton.borderRadius;
const {translate} = useLocalize();
- const fabPressable = useRef(null);
+ const fabPressable = useRef(null);
const sharedValue = useSharedValue(isActive ? 1 : 0);
const buttonRef = ref;
@@ -112,9 +113,9 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
{
- fabPressable.current = el;
+ fabPressable.current = el ?? null;
if (buttonRef && 'current' in buttonRef) {
- buttonRef.current = el;
+ buttonRef.current = el ?? null;
}
}}
accessibilityLabel={accessibilityLabel}
diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx
index 5615f3b87cfa..5c2488ca144a 100644
--- a/src/components/Form/FormWrapper.tsx
+++ b/src/components/Form/FormWrapper.tsx
@@ -1,13 +1,15 @@
import React, {useCallback, useMemo, useRef} from 'react';
import type {RefObject} from 'react';
-import type {StyleProp, View, ViewStyle} from 'react-native';
-import {Keyboard, ScrollView} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {ScrollView as RNScrollView, StyleProp, View, ViewStyle} from 'react-native';
+import {Keyboard} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import FormElement from '@components/FormElement';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types';
+import ScrollView from '@components/ScrollView';
import ScrollViewWithContext from '@components/ScrollViewWithContext';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -60,7 +62,7 @@ function FormWrapper({
disablePressOnEnter = true,
}: FormWrapperProps) {
const styles = useThemeStyles();
- const formRef = useRef(null);
+ const formRef = useRef(null);
const formContentRef = useRef(null);
const errorMessage = useMemo(() => (formState ? ErrorUtils.getLatestErrorMessage(formState) : undefined), [formState]);
diff --git a/src/components/FormScrollView.tsx b/src/components/FormScrollView.tsx
index ade167e9e628..91f5a825a38a 100644
--- a/src/components/FormScrollView.tsx
+++ b/src/components/FormScrollView.tsx
@@ -1,15 +1,16 @@
import type {ForwardedRef} from 'react';
import React from 'react';
-import type {ScrollViewProps} from 'react-native';
-import {ScrollView} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {ScrollView as RNScrollView, ScrollViewProps} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
+import ScrollView from './ScrollView';
type FormScrollViewProps = ScrollViewProps & {
/** Form elements */
children: React.ReactNode;
};
-function FormScrollView({children, ...rest}: FormScrollViewProps, ref: ForwardedRef) {
+function FormScrollView({children, ...rest}: FormScrollViewProps, ref: ForwardedRef) {
const styles = useThemeStyles();
return (
(null);
- const transferBalanceButtonRef = useRef(null);
+ const anchorRef = useRef(null);
+ const transferBalanceButtonRef = useRef(null);
const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false);
@@ -111,7 +111,7 @@ function KYCWall({
return;
}
- const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current);
+ const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current as HTMLDivElement);
const position = getAnchorPosition(buttonPosition);
setPositionAddPaymentMenu(position);
@@ -162,7 +162,7 @@ function KYCWall({
}
// Use event target as fallback if anchorRef is null for safety
- const targetElement = anchorRef.current ?? (event?.currentTarget as HTMLElement);
+ const targetElement = anchorRef.current ?? (event?.currentTarget as HTMLDivElement);
transferBalanceButtonRef.current = targetElement;
@@ -181,7 +181,7 @@ function KYCWall({
return;
}
- const clickedElementLocation = getClickedTargetLocation(targetElement);
+ const clickedElementLocation = getClickedTargetLocation(targetElement as HTMLDivElement);
const position = getAnchorPosition(clickedElementLocation);
setPositionAddPaymentMenu(position);
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 102f85ea49b9..5784be21bac3 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -88,7 +88,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton;
const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0;
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
- const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length;
+ const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length;
const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep;
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency);
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index fe8cc3506b3f..7f9ab3fe0dc9 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -7,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as HeaderUtils from '@libs/HeaderUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
@@ -79,16 +78,11 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
const isPending = TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction);
- const isRequestModifiable = !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction);
- const canModifyRequest = isActionOwner && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction);
- let canDeleteRequest = canModifyRequest;
+ const isDeletedParentAction = ReportActionsUtils.isDeletedAction(parentReportAction);
+ const canHoldOrUnholdRequest = !isSettled && !isApproved && !isDeletedParentAction;
- if (ReportUtils.isPaidGroupPolicyExpenseReport(moneyRequestReport)) {
- // If it's a paid policy expense report, only allow deleting the request if it's in draft state or instantly submitted state or the user is the policy admin
- canDeleteRequest =
- canDeleteRequest &&
- (ReportUtils.isDraftExpenseReport(moneyRequestReport) || ReportUtils.isExpenseReportWithInstantSubmittedState(moneyRequestReport) || PolicyUtils.isPolicyAdmin(policy));
- }
+ // If the report supports adding transactions to it, then it also supports deleting transactions from it.
+ const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction;
const changeMoneyRequestStatus = () => {
if (isOnHold) {
@@ -108,7 +102,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
}, [canDeleteRequest]);
const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)];
- if (isRequestModifiable) {
+ if (canHoldOrUnholdRequest) {
const isRequestIOU = parentReport?.type === 'iou';
const isHoldCreator = ReportUtils.isHoldCreator(transaction, report?.reportID) && isRequestIOU;
const canModifyStatus = isPolicyAdmin || isActionOwner || isApprover;
diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
index e8de4919cfdf..968e1dfbfdca 100755
--- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
+++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
@@ -1,6 +1,7 @@
import {useIsFocused} from '@react-navigation/native';
import {format} from 'date-fns';
import Str from 'expensify-common/lib/str';
+import {isUndefined} from 'lodash';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
@@ -491,6 +492,31 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
IOU.setMoneyRequestMerchant(transaction.transactionID, distanceMerchant, true);
}, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]);
+ // Auto select the category if there is only one enabled category and it is required
+ useEffect(() => {
+ const enabledCategories = _.filter(policyCategories, (category) => category.enabled);
+ if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) {
+ return;
+ }
+ IOU.setMoneyRequestCategory(transaction.transactionID, enabledCategories[0].name);
+ }, [iouCategory, shouldShowCategories, policyCategories, transaction, isCategoryRequired]);
+
+ // Auto select the tag if there is only one enabled tag and it is required
+ useEffect(() => {
+ let updatedTagsString = TransactionUtils.getTag(transaction);
+ policyTagLists.forEach((tagList, index) => {
+ const enabledTags = _.filter(tagList.tags, (tag) => tag.enabled);
+ const isTagListRequired = isUndefined(tagList.required) ? false : tagList.required && canUseViolations;
+ if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) {
+ return;
+ }
+ updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index);
+ });
+ if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) {
+ IOU.setMoneyRequestTag(transaction.transactionID, updatedTagsString);
+ }
+ }, [policyTagLists, transaction, policyTags, isTagRequired, canUseViolations]);
+
/**
* @param {Object} option
*/
diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx
index d14aec90fa10..0bc9130ea4a8 100644
--- a/src/components/OnyxProvider.tsx
+++ b/src/components/OnyxProvider.tsx
@@ -8,8 +8,8 @@ import createOnyxContext from './createOnyxContext';
const [withNetwork, NetworkProvider, NetworkContext] = createOnyxContext(ONYXKEYS.NETWORK);
const [withPersonalDetails, PersonalDetailsProvider, , usePersonalDetails] = createOnyxContext(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE);
-const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
-const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
+const [withReportActionsDrafts, ReportActionsDraftsProvider, , useReportActionsDrafts] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
+const [withBlockedFromConcierge, BlockedFromConciergeProvider, , useBlockedFromConcierge] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
const [withBetas, BetasProvider, BetasContext, useBetas] = createOnyxContext(ONYXKEYS.BETAS);
const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);
const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME);
@@ -66,5 +66,7 @@ export {
useFrequentlyUsedEmojis,
withPreferredEmojiSkinTone,
PreferredEmojiSkinToneContext,
+ useBlockedFromConcierge,
+ useReportActionsDrafts,
useSession,
};
diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index c0258f1252ef..40fb1115ac36 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -1,7 +1,7 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import _ from 'underscore';
import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager';
import Button from '@components/Button';
@@ -9,6 +9,7 @@ import FixedFooter from '@components/FixedFooter';
import FormHelpMessage from '@components/FormHelpMessage';
import OptionsList from '@components/OptionsList';
import ReferralProgramCTA from '@components/ReferralProgramCTA';
+import ScrollView from '@components/ScrollView';
import ShowMoreButton from '@components/ShowMoreButton';
import TextInput from '@components/TextInput';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
diff --git a/src/components/PDFView/PDFPasswordForm.js b/src/components/PDFView/PDFPasswordForm.js
index 10596bb9faf9..97d893b511dd 100644
--- a/src/components/PDFView/PDFPasswordForm.js
+++ b/src/components/PDFView/PDFPasswordForm.js
@@ -1,8 +1,9 @@
import PropTypes from 'prop-types';
import React, {useEffect, useMemo, useRef, useState} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import _ from 'underscore';
import Button from '@components/Button';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx
index 1bee95532104..c86d3b71c1d9 100644
--- a/src/components/Picker/BasePicker.tsx
+++ b/src/components/Picker/BasePicker.tsx
@@ -1,6 +1,7 @@
import lodashDefer from 'lodash/defer';
import type {ForwardedRef, ReactElement, ReactNode, RefObject} from 'react';
import React, {forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {ScrollView} from 'react-native';
import {View} from 'react-native';
import RNPickerSelect from 'react-native-picker-select';
diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts
index e06037f47b63..314c1ba141c3 100644
--- a/src/components/Popover/types.ts
+++ b/src/components/Popover/types.ts
@@ -1,5 +1,6 @@
import type {RefObject} from 'react';
-import type {View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {Text, View} from 'react-native';
import type {PopoverAnchorPosition} from '@components/Modal/types';
import type BaseModalProps from '@components/Modal/types';
import type {WindowDimensionsProps} from '@components/withWindowDimensions/types';
@@ -20,7 +21,7 @@ type PopoverProps = BaseModalProps &
anchorAlignment?: AnchorAlignment;
/** The anchor ref of the popover */
- anchorRef: RefObject;
+ anchorRef: RefObject;
/** Whether disable the animations */
disableAnimation?: boolean;
diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx
index 67481b41d50b..cc6c84477525 100644
--- a/src/components/PopoverProvider/index.tsx
+++ b/src/components/PopoverProvider/index.tsx
@@ -1,6 +1,7 @@
import type {RefObject} from 'react';
import React, {createContext, useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import type {View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {Text, View} from 'react-native';
import type {AnchorRef, PopoverContextProps, PopoverContextValue} from './types';
const PopoverContext = createContext({
@@ -10,7 +11,7 @@ const PopoverContext = createContext({
isOpen: false,
});
-function elementContains(ref: RefObject | undefined, target: EventTarget | null) {
+function elementContains(ref: RefObject | undefined, target: EventTarget | null) {
if (ref?.current && 'contains' in ref.current && ref?.current?.contains(target as Node)) {
return true;
}
@@ -21,7 +22,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
const [isOpen, setIsOpen] = useState(false);
const activePopoverRef = useRef(null);
- const closePopover = useCallback((anchorRef?: RefObject): boolean => {
+ const closePopover = useCallback((anchorRef?: RefObject): boolean => {
if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) {
return false;
}
diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts
index 2a366ae2a712..5022aee0f843 100644
--- a/src/components/PopoverProvider/types.ts
+++ b/src/components/PopoverProvider/types.ts
@@ -1,5 +1,6 @@
import type {ReactNode, RefObject} from 'react';
-import type {View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {Text, View} from 'react-native';
type PopoverContextProps = {
children: ReactNode;
@@ -8,14 +9,14 @@ type PopoverContextProps = {
type PopoverContextValue = {
onOpen?: (popoverParams: AnchorRef) => void;
popover?: AnchorRef | Record | null;
- close: (anchorRef?: RefObject) => void;
+ close: (anchorRef?: RefObject) => void;
isOpen: boolean;
};
type AnchorRef = {
- ref: RefObject;
- close: (anchorRef?: RefObject) => void;
- anchorRef: RefObject;
+ ref: RefObject;
+ close: (anchorRef?: RefObject) => void;
+ anchorRef: RefObject;
};
export type {PopoverContextProps, PopoverContextValue, AnchorRef};
diff --git a/src/components/PopoverWithoutOverlay/types.ts b/src/components/PopoverWithoutOverlay/types.ts
index 0d24cdd4bd9f..8fe40119ca61 100644
--- a/src/components/PopoverWithoutOverlay/types.ts
+++ b/src/components/PopoverWithoutOverlay/types.ts
@@ -1,5 +1,6 @@
import type {RefObject} from 'react';
-import type {View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {Text, View} from 'react-native';
import type BaseModalProps from '@components/Modal/types';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
@@ -14,7 +15,7 @@ type PopoverWithoutOverlayProps = ChildrenProps &
};
/** The anchor ref of the popover */
- anchorRef: RefObject;
+ anchorRef: RefObject;
/** A react-native-animatable animation timing for the modal display animation */
animationInTiming?: number;
diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts
index 2dd2e17e0454..9040a844e5a7 100644
--- a/src/components/Pressable/GenericPressable/types.ts
+++ b/src/components/Pressable/GenericPressable/types.ts
@@ -1,5 +1,6 @@
import type {ElementRef, ForwardedRef, RefObject} from 'react';
-import type {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, StyleProp, View, ViewStyle} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, Text as RNText, StyleProp, View, ViewStyle} from 'react-native';
import type {ValueOf} from 'type-fest';
import type {Shortcut} from '@libs/KeyboardShortcut';
import type CONST from '@src/CONST';
@@ -138,7 +139,7 @@ type PressableProps = RNPressableProps &
noDragArea?: boolean;
};
-type PressableRef = ForwardedRef;
+type PressableRef = ForwardedRef;
export default PressableProps;
export type {PressableRef};
diff --git a/src/components/PressableWithSecondaryInteraction/types.ts b/src/components/PressableWithSecondaryInteraction/types.ts
index aa67d45d66fb..b07c867daeb3 100644
--- a/src/components/PressableWithSecondaryInteraction/types.ts
+++ b/src/components/PressableWithSecondaryInteraction/types.ts
@@ -4,7 +4,7 @@ import type {ParsableStyle} from '@styles/utils/types';
type PressableWithSecondaryInteractionProps = PressableWithFeedbackProps & {
/** The function that should be called when this pressable is pressed */
- onPress: (event?: GestureResponderEvent) => void;
+ onPress?: (event?: GestureResponderEvent) => void;
/** The function that should be called when this pressable is pressedIn */
onPressIn?: (event?: GestureResponderEvent) => void;
diff --git a/src/components/Reactions/ReportActionItemEmojiReactions.tsx b/src/components/Reactions/ReportActionItemEmojiReactions.tsx
index 7e95ab670b7e..c6bf4f9e4016 100644
--- a/src/components/Reactions/ReportActionItemEmojiReactions.tsx
+++ b/src/components/Reactions/ReportActionItemEmojiReactions.tsx
@@ -23,7 +23,7 @@ type ReportActionItemEmojiReactionsProps = WithCurrentUserPersonalDetailsProps &
emojiReactions: OnyxEntry;
/** The user's preferred locale. */
- preferredLocale: OnyxEntry;
+ preferredLocale?: OnyxEntry;
/** The report action that these reactions are for */
reportAction: ReportAction;
@@ -155,7 +155,7 @@ function ReportActionItemEmojiReactions({
shouldDisableOpacity={!!reportAction.pendingAction}
>
(popoverReactionListAnchors.current[reaction.reactionEmojiName] = ref)}
+ ref={(ref) => (popoverReactionListAnchors.current[reaction.reactionEmojiName] = ref ?? null)}
count={reaction.reactionCount}
emojiCodes={reaction.emojiCodes}
onPress={reaction.onPress}
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index f0cd8dc1b4b5..60dbfc07966a 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -2,6 +2,7 @@ import Str from 'expensify-common/lib/str';
import React, {useMemo} from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
@@ -26,7 +27,7 @@ type MoneyReportViewProps = {
report: Report;
/** Policy that the report belongs to */
- policy: Policy;
+ policy: OnyxEntry;
/** Policy report fields */
policyReportFields: PolicyReportField[];
@@ -67,107 +68,111 @@ function MoneyReportView({report, policy, policyReportFields, shouldShowHorizont
- {ReportUtils.reportFieldsEnabled(report) &&
- sortedPolicyReportFields.map((reportField) => {
- const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField);
- const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue;
- const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy);
-
- return (
-
- Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))}
- shouldShowRightIcon
- disabled={isFieldDisabled}
- wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
- shouldGreyOutWhenDisabled={false}
- numberOfLinesTitle={0}
- interactive
- shouldStackHorizontally={false}
- onSecondaryInteraction={() => {}}
- hoverAndPressStyle={false}
- titleWithTooltips={[]}
- />
-
- );
- })}
-
-
-
- {translate('common.total')}
-
-
-
- {isSettled && (
-
-
-
- )}
-
- {formattedTotalAmount}
-
-
-
- {Boolean(shouldShowBreakdown) && (
+ {!ReportUtils.isClosedExpenseReportWithNoExpenses(report) && (
<>
-
-
-
- {translate('cardTransactions.outOfPocket')}
-
-
-
-
- {formattedOutOfPocketAmount}
-
-
-
-
+ {ReportUtils.reportFieldsEnabled(report) &&
+ sortedPolicyReportFields.map((reportField) => {
+ const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField);
+ const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue;
+ const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy);
+
+ return (
+
+ Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))}
+ shouldShowRightIcon
+ disabled={isFieldDisabled}
+ wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
+ shouldGreyOutWhenDisabled={false}
+ numberOfLinesTitle={0}
+ interactive
+ shouldStackHorizontally={false}
+ onSecondaryInteraction={() => {}}
+ hoverAndPressStyle={false}
+ titleWithTooltips={[]}
+ />
+
+ );
+ })}
+
- {translate('cardTransactions.companySpend')}
+ {translate('common.total')}
+ {isSettled && (
+
+
+
+ )}
- {formattedCompanySpendAmount}
+ {formattedTotalAmount}
+ {Boolean(shouldShowBreakdown) && (
+ <>
+
+
+
+ {translate('cardTransactions.outOfPocket')}
+
+
+
+
+ {formattedOutOfPocketAmount}
+
+
+
+
+
+
+ {translate('cardTransactions.companySpend')}
+
+
+
+
+ {formattedCompanySpendAmount}
+
+
+
+ >
+ )}
+
>
)}
-
);
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 0bd18d8ee7ea..1d048640b30b 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -24,7 +24,6 @@ import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
@@ -470,8 +469,8 @@ export default withOnyx {
- const parentReportAction = ReportActionsUtils.getParentReportAction(report);
+ key: ({report, parentReportActions}) => {
+ const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''];
const originalMessage = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage : undefined;
const transactionID = originalMessage?.IOUTransactionID ?? 0;
return `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`;
diff --git a/src/components/ReportActionItem/TaskAction.tsx b/src/components/ReportActionItem/TaskAction.tsx
index b10be4e86fe8..7e9262bb4c05 100644
--- a/src/components/ReportActionItem/TaskAction.tsx
+++ b/src/components/ReportActionItem/TaskAction.tsx
@@ -1,20 +1,24 @@
import React from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import * as TaskUtils from '@libs/TaskUtils';
+import type {ReportAction} from '@src/types/onyx';
type TaskActionProps = {
/** Name of the reportAction action */
- actionName: string;
+ action: OnyxEntry;
};
-function TaskAction({actionName}: TaskActionProps) {
+function TaskAction({action}: TaskActionProps) {
const styles = useThemeStyles();
+ const message = TaskUtils.getTaskReportActionMessage(action);
return (
- {TaskUtils.getTaskReportActionMessage(actionName)}
+ {message.html ? ${message.html}`} /> : {message.text}}
);
}
diff --git a/src/components/ScrollView.tsx b/src/components/ScrollView.tsx
new file mode 100644
index 000000000000..a61c592015ee
--- /dev/null
+++ b/src/components/ScrollView.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import type {ForwardedRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
+import {ScrollView as RNScrollView} from 'react-native';
+import type {ScrollViewProps} from 'react-native';
+
+function ScrollView({children, scrollIndicatorInsets, ...props}: ScrollViewProps, ref: ForwardedRef) {
+ return (
+
+ {children}
+
+ );
+}
+
+ScrollView.displayName = 'ScrollView';
+
+export type {ScrollViewProps};
+
+export default React.forwardRef(ScrollView);
diff --git a/src/components/ScrollViewWithContext.tsx b/src/components/ScrollViewWithContext.tsx
index 1ac53651a542..1b9bb2c09f56 100644
--- a/src/components/ScrollViewWithContext.tsx
+++ b/src/components/ScrollViewWithContext.tsx
@@ -1,13 +1,14 @@
import type {ForwardedRef, ReactNode} from 'react';
import React, {createContext, forwardRef, useMemo, useRef, useState} from 'react';
-import type {NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps} from 'react-native';
-import {ScrollView} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {NativeScrollEvent, NativeSyntheticEvent, ScrollView as RNScrollView, ScrollViewProps} from 'react-native';
+import ScrollView from './ScrollView';
const MIN_SMOOTH_SCROLL_EVENT_THROTTLE = 16;
type ScrollContextValue = {
contentOffsetY: number;
- scrollViewRef: ForwardedRef;
+ scrollViewRef: ForwardedRef;
};
const ScrollContext = createContext({
@@ -28,9 +29,9 @@ type ScrollViewWithContextProps = Partial & {
* Using this wrapper will automatically handle scrolling to the picker's
* when the picker modal is opened
*/
-function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) {
+function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) {
const [contentOffsetY, setContentOffsetY] = useState(0);
- const defaultScrollViewRef = useRef(null);
+ const defaultScrollViewRef = useRef(null);
const scrollViewRef = ref ?? defaultScrollViewRef;
const setContextScrollPosition = (event: NativeSyntheticEvent) => {
diff --git a/src/components/createOnyxContext.tsx b/src/components/createOnyxContext.tsx
index 50cda00b17b4..c19b8006c86c 100644
--- a/src/components/createOnyxContext.tsx
+++ b/src/components/createOnyxContext.tsx
@@ -3,7 +3,7 @@ import type {ComponentType, ForwardedRef, ForwardRefExoticComponent, PropsWithou
import React, {createContext, forwardRef, useContext} from 'react';
import {withOnyx} from 'react-native-onyx';
import getComponentDisplayName from '@libs/getComponentDisplayName';
-import type {OnyxKey, OnyxValue, OnyxValues} from '@src/ONYXKEYS';
+import type {OnyxKey, OnyxValue} from '@src/ONYXKEYS';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
// Provider types
@@ -32,11 +32,11 @@ type CreateOnyxContext = [
WithOnyxKey,
ComponentType, TOnyxKey>>,
React.Context>,
- () => OnyxValues[TOnyxKey],
+ () => NonNullable>,
];
export default (onyxKeyName: TOnyxKey): CreateOnyxContext => {
- const Context = createContext>(null);
+ const Context = createContext>(null as OnyxValue);
function Provider(props: ProviderPropsWithOnyx): ReactNode {
return {props.children};
}
@@ -86,7 +86,7 @@ export default (onyxKeyName: TOnyxKey): CreateOnyxCont
if (value === null) {
throw new Error(`useOnyxContext must be used within a OnyxProvider [key: ${onyxKeyName}]`);
}
- return value;
+ return value as NonNullable>;
};
return [withOnyxKey, ProviderWithOnyx, Context, useOnyxContext];
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 1ba613610723..3575854ee7e2 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1754,6 +1754,7 @@ export default {
workspaceType: 'Workspace type',
workspaceAvatar: 'Workspace avatar',
mustBeOnlineToViewMembers: 'You must be online in order to view members of this workspace.',
+ requested: 'Requested',
},
type: {
free: 'Free',
@@ -1809,6 +1810,9 @@ export default {
genericFailureMessage: 'An error occurred removing a user from the workspace, please try again.',
removeMembersPrompt: 'Are you sure you want to remove these members?',
removeMembersTitle: 'Remove members',
+ removeMemberButtonTitle: 'Remove from workspace',
+ removeMemberPrompt: ({memberName}) => `Are you sure you want to remove ${memberName}`,
+ removeMemberTitle: 'Remove member',
makeMember: 'Make member',
makeAdmin: 'Make admin',
selectAll: 'Select all',
@@ -2200,6 +2204,7 @@ export default {
viewAttachment: 'View attachment',
},
parentReportAction: {
+ deletedReport: '[Deleted report]',
deletedMessage: '[Deleted message]',
deletedRequest: '[Deleted request]',
reversedTransaction: '[Reversed transaction]',
@@ -2243,6 +2248,10 @@ export default {
invite: 'Invite them',
nothing: 'Do nothing',
},
+ actionableMentionJoinWorkspaceOptions: {
+ accept: 'Accept',
+ decline: 'Decline',
+ },
teachersUnitePage: {
teachersUnite: 'Teachers Unite',
joinExpensifyOrg: 'Join Expensify.org in eliminating injustice around the world and help teachers split their expenses for classrooms in need!',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index acbb70308167..51a83e55fee2 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1778,6 +1778,7 @@ export default {
workspaceType: 'Tipo de espacio de trabajo',
workspaceAvatar: 'Espacio de trabajo avatar',
mustBeOnlineToViewMembers: 'Debes estar en línea para poder ver los miembros de este espacio de trabajo.',
+ requested: 'Solicitado',
},
type: {
free: 'Gratis',
@@ -1833,6 +1834,9 @@ export default {
genericFailureMessage: 'Se ha producido un error al intentar eliminar a un usuario del espacio de trabajo. Por favor, inténtalo más tarde.',
removeMembersPrompt: '¿Estás seguro de que deseas eliminar a estos miembros?',
removeMembersTitle: 'Eliminar miembros',
+ removeMemberButtonTitle: 'Quitar del espacio de trabajo',
+ removeMemberPrompt: ({memberName}) => `¿Estás seguro de que deseas eliminar a ${memberName}`,
+ removeMemberTitle: 'Eliminar miembro',
makeMember: 'Hacer miembro',
makeAdmin: 'Hacer administrador',
selectAll: 'Seleccionar todo',
@@ -2688,6 +2692,7 @@ export default {
viewAttachment: 'Ver archivo adjunto',
},
parentReportAction: {
+ deletedReport: '[Informe eliminado]',
deletedMessage: '[Mensaje eliminado]',
deletedRequest: '[Solicitud eliminada]',
reversedTransaction: '[Transacción anulada]',
@@ -2709,6 +2714,10 @@ export default {
invite: 'Invitar',
nothing: 'No hacer nada',
},
+ actionableMentionJoinWorkspaceOptions: {
+ accept: 'Aceptar',
+ decline: 'Rechazar',
+ },
moderation: {
flagDescription: 'Todos los mensajes marcados se enviarán a un moderador para su revisión.',
chooseAReason: 'Elige abajo un motivo para reportarlo:',
diff --git a/src/libs/API/parameters/AcceptJoinRequest.ts b/src/libs/API/parameters/AcceptJoinRequest.ts
new file mode 100644
index 000000000000..4c7b6a00b2fb
--- /dev/null
+++ b/src/libs/API/parameters/AcceptJoinRequest.ts
@@ -0,0 +1,5 @@
+type AcceptJoinRequestParams = {
+ requests: string;
+};
+
+export default AcceptJoinRequestParams;
diff --git a/src/libs/API/parameters/DeclineJoinRequest.ts b/src/libs/API/parameters/DeclineJoinRequest.ts
new file mode 100644
index 000000000000..da0b147254d8
--- /dev/null
+++ b/src/libs/API/parameters/DeclineJoinRequest.ts
@@ -0,0 +1,5 @@
+type DeclineJoinRequestParams = {
+ requests: string;
+};
+
+export default DeclineJoinRequestParams;
diff --git a/src/libs/API/parameters/JoinPolicyInviteLink.ts b/src/libs/API/parameters/JoinPolicyInviteLink.ts
new file mode 100644
index 000000000000..4b280b8cd8c6
--- /dev/null
+++ b/src/libs/API/parameters/JoinPolicyInviteLink.ts
@@ -0,0 +1,6 @@
+type JoinPolicyInviteLinkParams = {
+ policyID: string;
+ inviterEmail: string;
+};
+
+export default JoinPolicyInviteLinkParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 211bc06f26a3..f529032130bb 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -156,3 +156,6 @@ export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWor
export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams';
export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams';
export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams';
+export type {default as AcceptJoinRequestParams} from './AcceptJoinRequest';
+export type {default as DeclineJoinRequestParams} from './DeclineJoinRequest';
+export type {default as JoinPolicyInviteLinkParams} from './JoinPolicyInviteLink';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 115355210f75..1b41ced4f1d7 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -157,6 +157,9 @@ const WRITE_COMMANDS = {
CANCEL_PAYMENT: 'CancelPayment',
ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT: 'AcceptACHContractForBankAccount',
SWITCH_TO_OLD_DOT: 'SwitchToOldDot',
+ JOIN_POLICY_VIA_INVITE_LINK: 'JoinWorkspaceViaInviteLink',
+ ACCEPT_JOIN_REQUEST: 'AcceptJoinRequest',
+ DECLINE_JOIN_REQUEST: 'DeclineJoinRequest',
} as const;
type WriteCommand = ValueOf;
@@ -312,6 +315,9 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET]: Parameters.SetWorkspaceAutoReportingMonthlyOffsetParams;
[WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams;
[WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams;
+ [WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams;
+ [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams;
+ [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams;
};
const READ_COMMANDS = {
@@ -388,6 +394,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = {
OPEN_OLD_DOT_LINK: 'OpenOldDotLink',
REVEAL_EXPENSIFY_CARD_DETAILS: 'RevealExpensifyCardDetails',
GET_MISSING_ONYX_MESSAGES: 'GetMissingOnyxMessages',
+ JOIN_POLICY_VIA_INVITE_LINK: 'JoinWorkspaceViaInviteLink',
RECONNECT_APP: 'ReconnectApp',
} as const;
@@ -399,6 +406,7 @@ type SideEffectRequestCommandParameters = {
[SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK]: Parameters.OpenOldDotLinkParams;
[SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS]: Parameters.RevealExpensifyCardDetailsParams;
[SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]: Parameters.GetMissingOnyxMessagesParams;
+ [SIDE_EFFECT_REQUEST_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams;
[SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams;
};
diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts
index cab0f48d75fd..33cda171f24b 100644
--- a/src/libs/EmojiUtils.ts
+++ b/src/libs/EmojiUtils.ts
@@ -242,9 +242,13 @@ function getFrequentlyUsedEmojis(newEmoji: Emoji | Emoji[]): FrequentlyUsedEmoji
/**
* Given an emoji item object, return an emoji code based on its type.
*/
-const getEmojiCodeWithSkinColor = (item: Emoji, preferredSkinToneIndex: number): string => {
+const getEmojiCodeWithSkinColor = (item: Emoji, preferredSkinToneIndex: OnyxEntry): string | undefined => {
const {code, types} = item;
- if (types?.[preferredSkinToneIndex]) {
+ if (!preferredSkinToneIndex) {
+ return;
+ }
+
+ if (typeof preferredSkinToneIndex === 'number' && types?.[preferredSkinToneIndex]) {
return types[preferredSkinToneIndex];
}
@@ -305,7 +309,7 @@ function getAddedEmojis(currentEmojis: Emoji[], formerEmojis: Emoji[]): Emoji[]
* Replace any emoji name in a text with the emoji icon.
* If we're on mobile, we also add a space after the emoji granted there's no text after it.
*/
-function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji {
+function replaceEmojis(text: string, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji {
// emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it
const emojisTrie = require('./EmojiTrie').default;
@@ -345,9 +349,9 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF
// Set the cursor to the end of the last replaced Emoji. Note that we position after
// the extra space, if we added one.
- cursorPosition = newText.indexOf(emoji) + emojiReplacement.length;
+ cursorPosition = newText.indexOf(emoji) + (emojiReplacement?.length ?? 0);
- newText = newText.replace(emoji, emojiReplacement);
+ newText = newText.replace(emoji, emojiReplacement ?? '');
}
}
@@ -369,7 +373,7 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF
/**
* Find all emojis in a text and replace them with their code.
*/
-function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji {
+function replaceAndExtractEmojis(text: string, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji {
const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, lang);
return {
diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts
index 20313ee8912d..784d339a4a0d 100644
--- a/src/libs/ErrorUtils.ts
+++ b/src/libs/ErrorUtils.ts
@@ -180,3 +180,5 @@ export {
getMicroSecondOnyxErrorObject,
isReceiptError,
};
+
+export type {OnyxDataWithErrors};
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index fc89b53fbefd..6f5dcdf9cda9 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -63,6 +63,7 @@ const loadConciergePage = () => require('../../../pages/ConciergePage').default
const loadProfileAvatar = () => require('../../../pages/settings/Profile/ProfileAvatar').default as React.ComponentType;
const loadWorkspaceAvatar = () => require('../../../pages/workspace/WorkspaceAvatar').default as React.ComponentType;
const loadReportAvatar = () => require('../../../pages/ReportAvatar').default as React.ComponentType;
+const loadWorkspaceJoinUser = () => require('@pages/workspace/WorkspaceJoinUserPage').default as React.ComponentType;
let timezone: Timezone | null;
let currentAccountID = -1;
@@ -356,6 +357,14 @@ function AuthScreens({session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = f
options={screenOptions.fullScreen}
component={DesktopSignInRedirectPage}
/>
+
);
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index cee9e31cedea..978e338796ea 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -251,6 +251,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../pages/workspace/categories/CategorySettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.MEMBER_DETAILS]: () => require('../../../pages/workspace/members/WorkspaceMemberDetailsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: () => require('../../../pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType,
[SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType,
[SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType,
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
index f38ec213a466..58d9efb43df5 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
@@ -11,6 +11,7 @@ import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as Session from '@libs/actions/Session';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import Navigation from '@libs/Navigation/Navigation';
@@ -47,7 +48,8 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
// When we are redirected to the Settings tab from the OldDot, we don't want to call the Welcome.show() method.
// To prevent this, the value of the bottomTabRoute?.name is checked here
bottomTabRoute?.name === SCREENS.WORKSPACE.INITIAL ||
- (currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR)
+ Boolean(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) ||
+ Session.isAnonymousUser()
) {
return;
}
diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx
index 20c426a74c71..2ca4c5178a5e 100644
--- a/src/libs/Navigation/NavigationRoot.tsx
+++ b/src/libs/Navigation/NavigationRoot.tsx
@@ -45,7 +45,7 @@ function parseAndLogRoute(state: NavigationState) {
const focusedRoute = findFocusedRoute(state);
- if (focusedRoute?.name !== SCREENS.NOT_FOUND) {
+ if (focusedRoute?.name !== SCREENS.NOT_FOUND && focusedRoute?.name !== SCREENS.SAML_SIGN_IN) {
updateLastVisitedPath(currentPath);
}
diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
index 7cca4ed466d4..5bc7d52230a8 100755
--- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
@@ -4,7 +4,7 @@ import SCREENS from '@src/SCREENS';
const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = {
[SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE],
[SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT],
- [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE],
+ [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION],
[SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET],
[SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS],
};
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index d3eb6d6a4c2b..8a24dc177a80 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -22,6 +22,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.PROFILE_AVATAR]: ROUTES.PROFILE_AVATAR.route,
[SCREENS.WORKSPACE_AVATAR]: ROUTES.WORKSPACE_AVATAR.route,
[SCREENS.REPORT_AVATAR]: ROUTES.REPORT_AVATAR.route,
+ [SCREENS.WORKSPACE_JOIN_USER]: ROUTES.WORKSPACE_JOIN_USER.route,
// Sidebar
[NAVIGATORS.BOTTOM_TAB_NAVIGATOR]: {
@@ -280,6 +281,12 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route,
},
+ [SCREENS.WORKSPACE.MEMBER_DETAILS]: {
+ path: ROUTES.WORKSPACE_MEMBER_DETAILS.route,
+ },
+ [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: {
+ path: ROUTES.WORKSPACE_MEMBER_ROLE_SELECTION.route,
+ },
[SCREENS.WORKSPACE.CATEGORY_CREATE]: {
path: ROUTES.WORKSPACE_CATEGORY_CREATE.route,
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 12b2802cb3fa..decb905ac52f 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -208,6 +208,16 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.MEMBER_DETAILS]: {
+ policyID: string;
+ accountID: string;
+ backTo: Routes;
+ };
+ [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: {
+ policyID: string;
+ accountID: string;
+ backTo: Routes;
+ };
[SCREENS.GET_ASSISTANCE]: {
backTo: Routes;
};
@@ -570,6 +580,10 @@ type AuthScreensParamList = SharedScreensParamList & {
[SCREENS.WORKSPACE_AVATAR]: {
policyID: string;
};
+ [SCREENS.WORKSPACE_JOIN_USER]: {
+ policyID: string;
+ email: string;
+ };
[SCREENS.REPORT_AVATAR]: {
reportID: string;
};
diff --git a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts
index 9c7e6402d69b..82410b120df2 100644
--- a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts
+++ b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts
@@ -1,6 +1,7 @@
-import type {OnyxValue} from '@src/ONYXKEYS';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {Report} from '@src/types/onyx';
-export default function reportWithoutHasDraftSelector(report: OnyxValue<'report_'>) {
+export default function reportWithoutHasDraftSelector(report: OnyxEntry) {
if (!report) {
return report;
}
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 07f0df962455..fd803a508b4a 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -107,7 +107,7 @@ type Hierarchy = Record;
selectedOptions?: Option[];
maxRecentReportsToShow?: number;
excludeLogins?: string[];
@@ -156,7 +156,6 @@ type SectionForSearchTerm = {
section: CategorySection;
newIndexOffset: number;
};
-
type GetOptions = {
recentReports: ReportUtils.OptionData[];
personalDetails: ReportUtils.OptionData[];
@@ -533,7 +532,6 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
// some types of actions are filtered out for lastReportAction, in some cases we need to check the actual last action
const lastOriginalReportAction = lastReportActions[report?.reportID ?? ''] ?? null;
let lastMessageTextFromReport = '';
- const lastActionName = lastReportAction?.actionName ?? '';
if (ReportUtils.isArchivedRoom(report)) {
const archiveReason =
@@ -585,12 +583,8 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
} else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) {
const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getForReportAction(report?.reportID, lastReportAction);
lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true);
- } else if (
- lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED ||
- lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED ||
- lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED
- ) {
- lastMessageTextFromReport = lastReportAction?.message?.[0].text ?? '';
+ } else if (ReportActionUtils.isTaskAction(lastReportAction)) {
+ lastMessageTextFromReport = TaskUtils.getTaskReportActionMessage(lastReportAction).text;
} else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) {
lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction);
} else if (ReportActionUtils.isApprovedOrSubmittedReportAction(lastReportAction)) {
@@ -1441,7 +1435,8 @@ function getOptions(
const {parentReportID, parentReportActionID} = report ?? {};
const canGetParentReport = parentReportID && parentReportActionID && allReportActions;
const parentReportAction = canGetParentReport ? allReportActions[parentReportID]?.[parentReportActionID] ?? null : null;
- const doesReportHaveViolations = betas.includes(CONST.BETAS.VIOLATIONS) && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction);
+ const doesReportHaveViolations =
+ (betas?.includes(CONST.BETAS.VIOLATIONS) && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction)) ?? false;
return ReportUtils.shouldReportBeInOptionList({
report,
@@ -1805,7 +1800,7 @@ function getIOUConfirmationOptionsFromParticipants(participants: Participant[],
function getFilteredOptions(
reports: OnyxCollection,
personalDetails: OnyxEntry,
- betas: Beta[] = [],
+ betas: OnyxEntry = [],
searchValue = '',
selectedOptions: Array> = [],
excludeLogins: string[] = [],
@@ -1852,9 +1847,9 @@ function getFilteredOptions(
*/
function getShareDestinationOptions(
- reports: Record,
+ reports: Record,
personalDetails: OnyxEntry,
- betas: Beta[] = [],
+ betas: OnyxEntry = [],
searchValue = '',
selectedOptions: Array> = [],
excludeLogins: string[] = [],
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index b510edd7dcf4..f6534e075773 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -91,7 +91,9 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMemb
*/
function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean {
return (
- !!policy && policy?.isPolicyExpenseChatEnabled && (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0)
+ !!policy &&
+ (policy?.isPolicyExpenseChatEnabled || Boolean(policy?.isJoinRequestPending)) &&
+ (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0)
);
}
@@ -227,7 +229,7 @@ function isPaidGroupPolicy(policy: OnyxEntry | EmptyObject): boolean {
* Checks if policy's scheduled submit / auto reporting frequency is "instant".
* Note: Free policies have "instant" submit always enabled.
*/
-function isInstantSubmitEnabled(policy: OnyxEntry): boolean {
+function isInstantSubmitEnabled(policy: OnyxEntry | EmptyObject): boolean {
return policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT || policy?.type === CONST.POLICY.TYPE.FREE;
}
diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts
index bc48111eadc5..3cb15c0f3fc3 100644
--- a/src/libs/Pusher/pusher.ts
+++ b/src/libs/Pusher/pusher.ts
@@ -5,7 +5,7 @@ import type {LiteralUnion, ValueOf} from 'type-fest';
import Log from '@libs/Log';
import type CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {OnyxUpdateEvent, OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx';
+import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import TYPE from './EventType';
import Pusher from './library';
@@ -22,8 +22,6 @@ type Args = {
authEndpoint: string;
};
-type PushJSON = OnyxUpdateEvent[] | OnyxUpdatesFromServer;
-
type UserIsTypingEvent = ReportUserIsTyping & {
userLogin?: string;
};
@@ -37,7 +35,7 @@ type PusherEventMap = {
[TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent;
};
-type EventData = EventName extends keyof PusherEventMap ? PusherEventMap[EventName] : PushJSON;
+type EventData = EventName extends keyof PusherEventMap ? PusherEventMap[EventName] : OnyxUpdatesFromServer;
type EventCallbackError = {type: ValueOf; data: {code: number}};
@@ -413,4 +411,4 @@ export {
getPusherSocketID,
};
-export type {EventCallbackError, States, PushJSON, UserIsTypingEvent, UserIsLeavingRoomEvent};
+export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent};
diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts
index 1ee75eb9c2f6..2bd79adef516 100644
--- a/src/libs/PusherUtils.ts
+++ b/src/libs/PusherUtils.ts
@@ -1,10 +1,10 @@
import type {OnyxUpdate} from 'react-native-onyx';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
+import type {OnyxUpdatesFromServer} from '@src/types/onyx';
import Log from './Log';
import NetworkConnection from './NetworkConnection';
import * as Pusher from './Pusher/pusher';
-import type {PushJSON} from './Pusher/pusher';
type Callback = (data: OnyxUpdate[]) => Promise;
@@ -25,10 +25,10 @@ function triggerMultiEventHandler(eventType: string, data: OnyxUpdate[]): Promis
/**
* Abstraction around subscribing to private user channel events. Handles all logs and errors automatically.
*/
-function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: PushJSON) => void) {
+function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: OnyxUpdatesFromServer) => void) {
const pusherChannelName = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${accountID}${CONFIG.PUSHER.SUFFIX}` as const;
- function logPusherEvent(pushJSON: PushJSON) {
+ function logPusherEvent(pushJSON: OnyxUpdatesFromServer) {
Log.info(`[Report] Handled ${eventName} event sent by Pusher`, false, pushJSON);
}
@@ -36,7 +36,7 @@ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string
NetworkConnection.triggerReconnectionCallbacks('Pusher re-subscribed to private user channel');
}
- function onEventPush(pushJSON: PushJSON) {
+ function onEventPush(pushJSON: OnyxUpdatesFromServer) {
logPusherEvent(pushJSON);
onEvent(pushJSON);
}
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 0b6c9671d69c..b12469941fd9 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -6,7 +6,15 @@ import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {ActionName, ChangeLog, IOUMessage, OriginalMessageActionableMentionWhisper, OriginalMessageIOU, OriginalMessageReimbursementDequeued} from '@src/types/onyx/OriginalMessage';
+import type {
+ ActionName,
+ ChangeLog,
+ IOUMessage,
+ OriginalMessageActionableMentionWhisper,
+ OriginalMessageIOU,
+ OriginalMessageJoinPolicyChangeLog,
+ OriginalMessageReimbursementDequeued,
+} from '@src/types/onyx/OriginalMessage';
import type Report from '@src/types/onyx/Report';
import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction';
import type ReportAction from '@src/types/onyx/ReportAction';
@@ -382,10 +390,6 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key:
return false;
}
- if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.TASKEDITED) {
- return false;
- }
-
// Filter out any unsupported reportAction types
if (!supportedActionTypes.includes(reportAction.actionName)) {
return false;
@@ -675,7 +679,8 @@ function isTaskAction(reportAction: OnyxEntry): boolean {
return (
reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED ||
reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED ||
- reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED
+ reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED ||
+ reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKEDITED
);
}
@@ -832,7 +837,7 @@ function hasRequestFromCurrentAccount(reportID: string, currentAccountID: number
* Checks if a given report action corresponds to an actionable mention whisper.
* @param reportAction
*/
-function isActionableMentionWhisper(reportAction: OnyxEntry): boolean {
+function isActionableMentionWhisper(reportAction: OnyxEntry): reportAction is ReportActionBase & OriginalMessageActionableMentionWhisper {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLEMENTIONWHISPER;
}
@@ -884,6 +889,26 @@ function isCurrentActionUnread(report: Report | EmptyObject, reportAction: Repor
return isReportActionUnread(reportAction, lastReadTime) && (!prevReportAction || !isReportActionUnread(prevReportAction, lastReadTime));
}
+/**
+ * Checks if a given report action corresponds to a join request action.
+ * @param reportAction
+ */
+function isActionableJoinRequest(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLEJOINREQUEST;
+}
+
+/**
+ * Checks if any report actions correspond to a join request action that is still pending.
+ * @param reportID
+ */
+function isActionableJoinRequestPending(reportID: string): boolean {
+ const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(reportID)));
+ const findPendingRequest = sortedReportActions.find(
+ (reportActionItem) => isActionableJoinRequest(reportActionItem) && (reportActionItem as OriginalMessageJoinPolicyChangeLog)?.originalMessage?.choice === '',
+ );
+ return !!findPendingRequest;
+}
+
function isApprovedOrSubmittedReportAction(action: OnyxEntry | EmptyObject) {
return [CONST.REPORT.ACTIONS.TYPE.APPROVED, CONST.REPORT.ACTIONS.TYPE.SUBMITTED].some((type) => type === action?.actionName);
}
@@ -949,6 +974,8 @@ export {
isActionableMentionWhisper,
getActionableMentionWhisperMessage,
isCurrentActionUnread,
+ isActionableJoinRequest,
+ isActionableJoinRequestPending,
};
export type {LastVisibleMessage};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 2bfcb6712109..e062a4857e19 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -28,6 +28,7 @@ import type {
ReportAction,
ReportMetadata,
Session,
+ Task,
Transaction,
TransactionViolation,
} from '@src/types/onyx';
@@ -515,6 +516,14 @@ Onyx.connect({
},
});
+function getCurrentUserAvatarOrDefault(): UserUtils.AvatarSource {
+ return currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID);
+}
+
+function getCurrentUserDisplayNameOrEmail(): string | undefined {
+ return currentUserPersonalDetails?.displayName ?? currentUserEmail;
+}
+
function getChatType(report: OnyxEntry | Participant | EmptyObject): ValueOf | undefined {
return report?.chatType;
}
@@ -959,14 +968,6 @@ function isProcessingReport(report: OnyxEntry | EmptyObject): boolean {
return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED;
}
-/**
- * Returns true if the policy has `instant` reporting frequency and if the report is still being processed (i.e. submitted state)
- */
-function isExpenseReportWithInstantSubmittedState(report: OnyxEntry | EmptyObject): boolean {
- const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`] ?? null;
- return isExpenseReport(report) && isProcessingReport(report) && PolicyUtils.isInstantSubmitEnabled(policy);
-}
-
/**
* Check if the report is a single chat report that isn't a thread
* and personal detail of participant is optimistic data
@@ -1075,6 +1076,20 @@ function findLastAccessedReport(
return adminReport ?? sortedReports.at(-1) ?? null;
}
+/**
+ * Whether the provided report has expenses
+ */
+function hasExpenses(reportID?: string): boolean {
+ return !!Object.values(allTransactions ?? {}).find((transaction) => `${transaction?.reportID}` === `${reportID}`);
+}
+
+/**
+ * Whether the provided report is a closed expense report with no expenses
+ */
+function isClosedExpenseReportWithNoExpenses(report: OnyxEntry): boolean {
+ return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED && isExpenseReport(report) && !hasExpenses(report.reportID);
+}
+
/**
* Whether the provided report is an archived room
*/
@@ -1082,6 +1097,16 @@ function isArchivedRoom(report: OnyxEntry | EmptyObject): boolean {
return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED;
}
+/**
+ * Whether the provided report is the admin's room
+ */
+function isJoinRequestInAdminRoom(report: OnyxEntry): boolean {
+ if (!report) {
+ return false;
+ }
+ return ReportActionsUtils.isActionableJoinRequestPending(report.reportID);
+}
+
/**
* Checks if the current user is allowed to comment on the given report.
*/
@@ -1279,6 +1304,29 @@ function getChildReportNotificationPreference(reportAction: OnyxEntry): boolean {
+ if (!isMoneyRequestReport(moneyRequestReport)) {
+ return false;
+ }
+
+ if (isReportApproved(moneyRequestReport) || isSettled(moneyRequestReport?.reportID)) {
+ return false;
+ }
+
+ if (isGroupPolicy(moneyRequestReport) && isProcessingReport(moneyRequestReport) && !PolicyUtils.isInstantSubmitEnabled(getPolicy(moneyRequestReport?.policyID))) {
+ return false;
+ }
+
+ return true;
+}
+
/**
* Can only delete if the author is this user and the action is an ADDCOMMENT action or an IOU action in an unsettled report, or if the user is a
* policy admin
@@ -1293,14 +1341,13 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID:
// For now, users cannot delete split actions
const isSplitAction = reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT;
- if (isSplitAction || isSettled(String(reportAction?.originalMessage?.IOUReportID)) || (!isEmptyObject(report) && isReportApproved(report))) {
+ if (isSplitAction) {
return false;
}
if (isActionOwner) {
- if (!isEmptyObject(report) && isPaidGroupPolicyExpenseReport(report)) {
- // If it's a paid policy expense report, only allow deleting the request if it's a draft or is instantly submitted or the user is the policy admin
- return isDraftExpenseReport(report) || isExpenseReportWithInstantSubmittedState(report) || PolicyUtils.isPolicyAdmin(policy);
+ if (!isEmptyObject(report) && isMoneyRequestReport(report)) {
+ return canAddOrDeleteTransactions(report);
}
return true;
}
@@ -1820,7 +1867,7 @@ function buildOptimisticCancelPaymentReportAction(expenseReportID: string, amoun
person: [
{
style: 'strong',
- text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
type: 'TEXT',
},
],
@@ -1887,6 +1934,10 @@ function requiresAttentionFromCurrentUser(optionOrReport: OnyxEntry | Op
return false;
}
+ if (isJoinRequestInAdminRoom(optionOrReport)) {
+ return true;
+ }
+
if (isArchivedRoom(optionOrReport) || isArchivedRoom(getReport(optionOrReport.parentReportID))) {
return false;
}
@@ -2583,6 +2634,10 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu
return parentReportActionMessage;
}
+ if (isClosedExpenseReportWithNoExpenses(report)) {
+ return Localize.translateLocal('parentReportAction.deletedReport');
+ }
+
if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) {
return Localize.translateLocal('parentReportAction.deletedTask');
}
@@ -2983,11 +3038,10 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa
const formattedTotal = CurrencyUtils.convertToDisplayString(storedTotal, currency);
const policy = getPolicy(policyID);
- const isFree = policy?.type === CONST.POLICY.TYPE.FREE;
+ const isInstantSubmitEnabled = PolicyUtils.isInstantSubmitEnabled(policy);
- // Define the state and status of the report based on whether the policy is free or paid
- const stateNum = isFree ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.OPEN;
- const statusNum = isFree ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN;
+ const stateNum = isInstantSubmitEnabled ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.OPEN;
+ const statusNum = isInstantSubmitEnabled ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN;
const expenseReport: OptimisticExpenseReport = {
reportID: generateReportID(),
@@ -3158,14 +3212,14 @@ function buildOptimisticIOUReportAction(
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
originalMessage,
message: getIOUReportActionMessage(iouReportID, type, amount, comment, currency, paymentType, isSettlingUp),
person: [
{
style: 'strong',
- text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
type: 'TEXT',
},
],
@@ -3191,14 +3245,14 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e
actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
originalMessage,
message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.APPROVED, Math.abs(amount), '', currency),
person: [
{
style: 'strong',
- text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
type: 'TEXT',
},
],
@@ -3233,14 +3287,14 @@ function buildOptimisticMovedReportAction(fromPolicyID: string, toPolicyID: stri
actionName: CONST.REPORT.ACTIONS.TYPE.MOVED,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
originalMessage,
message: movedActionMessage,
person: [
{
style: 'strong',
- text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
type: 'TEXT',
},
],
@@ -3266,14 +3320,14 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string,
actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
originalMessage,
message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.SUBMITTED, Math.abs(amount), '', currency),
person: [
{
style: 'strong',
- text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
type: 'TEXT',
},
],
@@ -3339,7 +3393,7 @@ function buildOptimisticModifiedExpenseReportAction(
actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
created: DateUtils.getDBTime(),
isAttachment: false,
message: [
@@ -3422,7 +3476,7 @@ function buildOptimisticTaskReportAction(taskReportID: string, actionName: Origi
actionName,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
originalMessage,
message: [
@@ -3498,10 +3552,6 @@ function buildOptimisticChatReport(
};
}
-function getCurrentUserAvatarOrDefault(): UserUtils.AvatarSource {
- return allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID);
-}
-
/**
* Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically
* @param [created] - Action created time
@@ -3528,7 +3578,7 @@ function buildOptimisticCreatedReportAction(emailCreatingAction: string, created
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
automatic: false,
@@ -3564,7 +3614,7 @@ function buildOptimisticRenamedRoomReportAction(newName: string, oldName: string
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
originalMessage: {
@@ -3605,11 +3655,11 @@ function buildOptimisticHoldReportAction(comment: string, created = DateUtils.ge
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
automatic: false,
- avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
created,
shouldShow: true,
};
@@ -3636,42 +3686,79 @@ function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): Opt
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'normal',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
automatic: false,
- avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
created,
shouldShow: true,
};
}
-/**
- * Returns the necessary reportAction onyx data to indicate that a task report has been edited
- */
-function buildOptimisticEditedTaskReportAction(emailEditingTask: string): OptimisticEditedTaskReportAction {
+function buildOptimisticEditedTaskFieldReportAction({title, description}: Task): OptimisticEditedTaskReportAction {
+ // We do not modify title & description in one request, so we need to create a different optimistic action for each field modification
+ let field = '';
+ let value = '';
+ if (title !== undefined) {
+ field = 'task title';
+ value = title;
+ } else if (description !== undefined) {
+ field = 'description';
+ value = description;
+ }
+
+ let changelog = 'edited this task';
+ if (field && value) {
+ changelog = `updated the ${field} to ${value}`;
+ } else if (field) {
+ changelog = `removed the ${field}`;
+ }
+
return {
reportActionID: NumberUtils.rand64(),
actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
actorAccountID: currentUserAccountID,
message: [
+ {
+ type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
+ text: changelog,
+ html: changelog,
+ },
+ ],
+ person: [
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: emailEditingTask,
+ text: getCurrentUserDisplayNameOrEmail(),
},
+ ],
+ automatic: false,
+ avatar: getCurrentUserAvatarOrDefault(),
+ created: DateUtils.getDBTime(),
+ shouldShow: false,
+ };
+}
+
+function buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID: number): OptimisticEditedTaskReportAction {
+ return {
+ reportActionID: NumberUtils.rand64(),
+ actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ actorAccountID: currentUserAccountID,
+ message: [
{
- type: CONST.REPORT.MESSAGE.TYPE.TEXT,
- style: 'normal',
- text: ' edited this task',
+ type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
+ text: `assigned to ${getDisplayNameForParticipant(assigneeAccountID)}`,
+ html: `assigned to `,
},
],
person: [
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
automatic: false,
@@ -3714,7 +3801,7 @@ function buildOptimisticClosedReportAction(emailClosingReport: string, policyNam
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
reportActionID: NumberUtils.rand64(),
@@ -3979,7 +4066,7 @@ function shouldReportBeInOptionList({
report: OnyxEntry;
currentReportId: string;
isInGSDMode: boolean;
- betas: Beta[];
+ betas: OnyxEntry;
policies: OnyxCollection;
excludeEmptyChats: boolean;
doesReportHaveViolations: boolean;
@@ -4138,7 +4225,13 @@ function chatIncludesChronos(report: OnyxEntry | EmptyObject): boolean {
* - It's an ADDCOMMENT that is not an attachment
*/
function canFlagReportAction(reportAction: OnyxEntry, reportID: string | undefined): boolean {
- const report = getReport(reportID);
+ let report = getReport(reportID);
+
+ // If the childReportID exists in reportAction and is equal to the reportID,
+ // the report action being evaluated is the parent report action in a thread, and we should get the parent report to evaluate instead.
+ if (reportAction?.childReportID?.toString() === reportID?.toString()) {
+ report = getReport(report?.parentReportID);
+ }
const isCurrentUserAction = reportAction?.actorAccountID === currentUserAccountID;
const isOriginalMessageHaveHtml =
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT ||
@@ -4313,7 +4406,6 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o
return false;
}
- // In case of expense reports, we have to look at the parent workspace chat to get the isOwnPolicyExpenseChat property
let isOwnPolicyExpenseChat = report?.isOwnPolicyExpenseChat ?? false;
if (isExpenseReport(report) && getParentReport(report)) {
isOwnPolicyExpenseChat = Boolean(getParentReport(report)?.isOwnPolicyExpenseChat);
@@ -4327,12 +4419,8 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o
// User can request money in any IOU report, unless paid, but user can only request money in an expense report
// which is tied to their workspace chat.
if (isMoneyRequestReport(report)) {
- const isOwnExpenseReport = isExpenseReport(report) && isOwnPolicyExpenseChat;
- if (isOwnExpenseReport && PolicyUtils.isPaidGroupPolicy(policy)) {
- return isDraftExpenseReport(report) || isExpenseReportWithInstantSubmittedState(report);
- }
-
- return (isOwnExpenseReport || isIOUReport(report)) && !isReportApproved(report) && !isSettled(report?.reportID);
+ const canAddTransactions = canAddOrDeleteTransactions(report);
+ return isGroupPolicy(report) ? isOwnPolicyExpenseChat && canAddTransactions : canAddTransactions;
}
// In case of policy expense chat, users can only request money from their own policy expense chat
@@ -5088,6 +5176,17 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry | undefined | null, chatReport: OnyxEntry | null): boolean {
+ return !existingIOUReport || hasIOUWaitingOnCurrentUserBankAccount(chatReport) || !canAddOrDeleteTransactions(existingIOUReport);
+}
+
export {
getReportParticipantsTitle,
isReportMessageAttachment,
@@ -5111,6 +5210,7 @@ export {
getPolicyName,
getPolicyType,
isArchivedRoom,
+ isClosedExpenseReportWithNoExpenses,
isExpensifyOnlyParticipantInReport,
canCreateTaskInReport,
isPolicyExpenseChatAdmin,
@@ -5119,7 +5219,6 @@ export {
isPublicAnnounceRoom,
isConciergeChatReport,
isProcessingReport,
- isExpenseReportWithInstantSubmittedState,
isCurrentUserTheOnlyParticipant,
hasAutomatedExpensifyAccountIDs,
hasExpensifyGuidesEmails,
@@ -5159,7 +5258,8 @@ export {
buildOptimisticClosedReportAction,
buildOptimisticCreatedReportAction,
buildOptimisticRenamedRoomReportAction,
- buildOptimisticEditedTaskReportAction,
+ buildOptimisticEditedTaskFieldReportAction,
+ buildOptimisticChangedTaskAssigneeReportAction,
buildOptimisticIOUReport,
buildOptimisticApprovedReportAction,
buildOptimisticMovedReportAction,
@@ -5291,6 +5391,9 @@ export {
canEditRoomVisibility,
canEditPolicyDescription,
getPolicyDescriptionText,
+ isJoinRequestInAdminRoom,
+ canAddOrDeleteTransactions,
+ shouldCreateNewMoneyRequestReport,
};
export type {
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 8d53e992cb2d..3aa4cb63df9a 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -328,7 +328,7 @@ function getOptionData({
const newName = lastAction?.originalMessage?.newName ?? '';
result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName});
} else if (ReportActionsUtils.isTaskAction(lastAction)) {
- result.alternateText = TaskUtils.getTaskReportActionMessage(lastAction.actionName);
+ result.alternateText = TaskUtils.getTaskReportActionMessage(lastAction).text;
} else if (
lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM ||
lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.REMOVE_FROM_ROOM ||
@@ -386,6 +386,12 @@ function getOptionData({
result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result as Report);
+ if (ReportActionsUtils.isActionableJoinRequestPending(report.reportID)) {
+ result.isPinned = true;
+ result.isUnread = true;
+ result.brickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO;
+ }
+
if (!hasMultipleParticipants) {
result.accountID = personalDetail?.accountID;
result.login = personalDetail?.login;
diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts
index 623d449db885..81a079003d0e 100644
--- a/src/libs/TaskUtils.ts
+++ b/src/libs/TaskUtils.ts
@@ -3,6 +3,7 @@ import Onyx from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Report} from '@src/types/onyx';
+import type {Message} from '@src/types/onyx/ReportAction';
import type ReportAction from '@src/types/onyx/ReportAction';
import * as CollectionUtils from './CollectionUtils';
import * as Localize from './Localize';
@@ -22,16 +23,21 @@ Onyx.connect({
/**
* Given the Task reportAction name, return the appropriate message to be displayed and copied to clipboard.
*/
-function getTaskReportActionMessage(actionName: string): string {
- switch (actionName) {
+function getTaskReportActionMessage(action: OnyxEntry): Pick {
+ switch (action?.actionName) {
case CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED:
- return Localize.translateLocal('task.messages.completed');
+ return {text: Localize.translateLocal('task.messages.completed')};
case CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED:
- return Localize.translateLocal('task.messages.canceled');
+ return {text: Localize.translateLocal('task.messages.canceled')};
case CONST.REPORT.ACTIONS.TYPE.TASKREOPENED:
- return Localize.translateLocal('task.messages.reopened');
+ return {text: Localize.translateLocal('task.messages.reopened')};
+ case CONST.REPORT.ACTIONS.TYPE.TASKEDITED:
+ return {
+ text: action?.message?.[0].text ?? '',
+ html: action?.message?.[0].html,
+ };
default:
- return Localize.translateLocal('task.task');
+ return {text: Localize.translateLocal('task.task')};
}
}
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 36da40ff5eb9..cb3aa20ab6a7 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -845,37 +845,26 @@ function getMoneyRequestInformation(
// STEP 2: Get the money request report. If the moneyRequestReportID has been provided, we want to add the transaction to this specific report.
// If no such reportID has been provided, let's use the chatReport.iouReportID property. In case that is not present, build a new optimistic money request report.
let iouReport: OnyxEntry = null;
- const shouldCreateNewMoneyRequestReport = !moneyRequestReportID && (!chatReport.iouReportID || ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(chatReport));
if (moneyRequestReportID) {
iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReportID}`] ?? null;
- } else if (!shouldCreateNewMoneyRequestReport) {
+ } else {
iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`] ?? null;
}
- let isFromPaidPolicy = false;
- if (isPolicyExpenseChat) {
- isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy ?? null);
+ const shouldCreateNewMoneyRequestReport = ReportUtils.shouldCreateNewMoneyRequestReport(iouReport, chatReport);
- // If the linked expense report on paid policy is not draft and not instantly submitted, we need to create a new draft expense report
- if (iouReport && isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport) && !ReportUtils.isExpenseReportWithInstantSubmittedState(iouReport)) {
- iouReport = null;
- }
- }
-
- if (iouReport) {
- if (isPolicyExpenseChat) {
- iouReport = {...iouReport};
- if (iouReport?.currency === currency && typeof iouReport.total === 'number') {
- // Because of the Expense reports are stored as negative values, we subtract the total from the amount
- iouReport.total -= amount;
- }
- } else {
- iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, payeeAccountID, amount, currency);
- }
- } else {
+ if (!iouReport || shouldCreateNewMoneyRequestReport) {
iouReport = isPolicyExpenseChat
? ReportUtils.buildOptimisticExpenseReport(chatReport.reportID, chatReport.policyID ?? '', payeeAccountID, amount, currency)
: ReportUtils.buildOptimisticIOUReport(payeeAccountID, payerAccountID, amount, chatReport.reportID, currency);
+ } else if (isPolicyExpenseChat) {
+ iouReport = {...iouReport};
+ if (iouReport?.currency === currency && typeof iouReport.total === 'number') {
+ // Because of the Expense reports are stored as negative values, we subtract the total from the amount
+ iouReport.total -= amount;
+ }
+ } else {
+ iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, payeeAccountID, amount, currency);
}
// STEP 3: Build optimistic receipt and transaction
@@ -1860,10 +1849,8 @@ function createSplitsAndOnyxData(
}
// STEP 2: Get existing IOU/Expense report and update its total OR build a new optimistic one
- // For Control policy expense chats, if the report is already approved, create a new expense report
let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport.iouReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] : null;
- const shouldCreateNewOneOnOneIOUReport =
- !oneOnOneIOUReport || (isOwnPolicyExpenseChat && ReportUtils.isControlPolicyExpenseReport(oneOnOneIOUReport) && ReportUtils.isReportApproved(oneOnOneIOUReport));
+ const shouldCreateNewOneOnOneIOUReport = ReportUtils.shouldCreateNewMoneyRequestReport(oneOnOneIOUReport, oneOnOneChatReport);
if (!oneOnOneIOUReport || shouldCreateNewOneOnOneIOUReport) {
oneOnOneIOUReport = isOwnPolicyExpenseChat
@@ -2501,8 +2488,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
}
let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport?.iouReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] : null;
- const shouldCreateNewOneOnOneIOUReport =
- !oneOnOneIOUReport || (isPolicyExpenseChat && ReportUtils.isControlPolicyExpenseReport(oneOnOneIOUReport) && ReportUtils.isReportApproved(oneOnOneIOUReport));
+ const shouldCreateNewOneOnOneIOUReport = ReportUtils.shouldCreateNewMoneyRequestReport(oneOnOneIOUReport, oneOnOneChatReport);
if (!oneOnOneIOUReport || shouldCreateNewOneOnOneIOUReport) {
oneOnOneIOUReport = isPolicyExpenseChat
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index 38cc11756068..f6a1ec3ec340 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -57,7 +57,8 @@ import type {
ReportAction,
Transaction,
} from '@src/types/onyx';
-import type {Errors} from '@src/types/onyx/OnyxCommon';
+import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
+import type {OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage';
import type {Attributes, CustomUnit, Rate, Unit} from '@src/types/onyx/Policy';
import type {OnyxData} from '@src/types/onyx/Request';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
@@ -749,9 +750,9 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`,
value: {
- ...memberRoles.reduce((member: Record, current) => {
+ ...memberRoles.reduce((member: Record, current) => {
// eslint-disable-next-line no-param-reassign
- member[current.accountID] = {role: current?.role};
+ member[current.accountID] = {role: current?.role, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE};
return member;
}, {}),
errors: null,
@@ -764,6 +765,11 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`,
value: {
+ ...memberRoles.reduce((member: Record, current) => {
+ // eslint-disable-next-line no-param-reassign
+ member[current.accountID] = {role: current?.role, pendingAction: null};
+ return member;
+ }, {}),
errors: null,
},
},
@@ -964,6 +970,37 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
API.write(WRITE_COMMANDS.ADD_MEMBERS_TO_WORKSPACE, params, {optimisticData, successData, failureData});
}
+/**
+ * Invite member to the specified policyID
+ * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
+ */
+function inviteMemberToWorkspace(policyID: string, inviterEmail: string) {
+ const memberJoinKey = `${ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER}${policyID}` as const;
+
+ const optimisticMembersState = {policyID, inviterEmail};
+ const failureMembersState = {policyID, inviterEmail};
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: memberJoinKey,
+ value: optimisticMembersState,
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: memberJoinKey,
+ value: {...failureMembersState, errors: ErrorUtils.getMicroSecondOnyxError('common.genericEditFailureMessage')},
+ },
+ ];
+
+ const params = {policyID, inviterEmail};
+
+ API.write(WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK, params, {optimisticData, failureData});
+}
+
/**
* Updates a workspace avatar image
*/
@@ -2553,6 +2590,123 @@ function clearCategoryErrors(policyID: string, categoryName: string) {
});
}
+/**
+ * Accept user join request to a workspace
+ */
+function acceptJoinRequest(reportID: string, reportAction: OnyxEntry) {
+ const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT;
+ if (!reportAction) {
+ return;
+ }
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice: ''},
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const parameters = {
+ requests: JSON.stringify({
+ [(reportAction.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage']).policyID]: {
+ requests: [{accountID: reportAction?.actorAccountID, adminsRoomMessageReportActionID: reportAction.reportActionID}],
+ },
+ }),
+ };
+
+ API.write(WRITE_COMMANDS.ACCEPT_JOIN_REQUEST, parameters, {optimisticData, failureData, successData});
+}
+
+/**
+ * Decline user join request to a workspace
+ */
+function declineJoinRequest(reportID: string, reportAction: OnyxEntry) {
+ if (!reportAction) {
+ return;
+ }
+ const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE;
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice: ''},
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const parameters = {
+ requests: JSON.stringify({
+ [(reportAction.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage']).policyID]: {
+ requests: [{accountID: reportAction?.actorAccountID, adminsRoomMessageReportActionID: reportAction.reportActionID}],
+ },
+ }),
+ };
+
+ API.write(WRITE_COMMANDS.DECLINE_JOIN_REQUEST, parameters, {optimisticData, failureData, successData});
+}
+
export {
removeMembers,
updateWorkspaceMembersRole,
@@ -2602,6 +2756,9 @@ export {
updateWorkspaceDescription,
setWorkspaceCategoryEnabled,
setWorkspaceRequiresCategory,
+ inviteMemberToWorkspace,
+ acceptJoinRequest,
+ declineJoinRequest,
createPolicyCategory,
clearCategoryErrors,
};
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index e328460c37eb..27c7f3e36fd4 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -399,7 +399,7 @@ function reopenTask(taskReport: OnyxEntry) {
function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task) {
// Create the EditedReportAction on the task
- const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskReportAction(currentUserEmail);
+ const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskFieldReportAction({title, description});
// Sometimes title or description is undefined, so we need to check for that, and we provide it to multiple functions
const reportName = (title ?? report?.reportName)?.trim();
@@ -429,6 +429,11 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task
];
const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
+ value: {[editTaskReportAction.reportActionID]: {pendingAction: null}},
+ },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`,
@@ -467,16 +472,22 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task
API.write(WRITE_COMMANDS.EDIT_TASK, parameters, {optimisticData, successData, failureData});
}
-function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assigneeEmail: string, assigneeAccountID = 0, assigneeChatReport: OnyxEntry = null) {
+function editTaskAssignee(
+ report: OnyxTypes.Report,
+ ownerAccountID: number,
+ assigneeEmail: string,
+ assigneeAccountID: number | null = 0,
+ assigneeChatReport: OnyxEntry = null,
+) {
// Create the EditedReportAction on the task
- const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskReportAction(currentUserEmail);
+ const editTaskReportAction = ReportUtils.buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID ?? 0);
const reportName = report.reportName?.trim();
let assigneeChatReportOnyxData;
const assigneeChatReportID = assigneeChatReport ? assigneeChatReport.reportID : '0';
const optimisticReport: OptimisticReport = {
reportName,
- managerID: assigneeAccountID || report.managerID,
+ managerID: assigneeAccountID ?? report.managerID,
pendingFields: {
...(assigneeAccountID && {managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
},
@@ -499,6 +510,11 @@ function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assi
];
const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
+ value: {[editTaskReportAction.reportActionID]: {pendingAction: null}},
+ },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`,
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 708fc5e8591d..fdd657f801f2 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -578,21 +578,8 @@ function subscribeToUserEvents() {
// Handles the mega multipleEvents from Pusher which contains an array of single events.
// Each single event is passed to PusherUtils in order to trigger the callbacks for that event
PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.MULTIPLE_EVENTS, currentUserAccountID.toString(), (pushJSON) => {
- // The data for this push event comes in two different formats:
- // 1. Original format - this is what was sent before the RELIABLE_UPDATES project and will go away once RELIABLE_UPDATES is fully complete
- // - The data is an array of objects, where each object is an onyx update
- // Example: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]
- // 1. Reliable updates format - this is what was sent with the RELIABLE_UPDATES project and will be the format from now on
- // - The data is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above)
- // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]}
- if (Array.isArray(pushJSON)) {
- Log.warn('Received pusher event with array format');
- pushJSON.forEach((multipleEvent) => {
- PusherUtils.triggerMultiEventHandler(multipleEvent.eventType, multipleEvent.data);
- });
- return;
- }
-
+ // The data for the update is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above)
+ // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]}
const updates = {
type: CONST.ONYX_UPDATE_TYPES.PUSHER,
lastUpdateID: Number(pushJSON.lastUpdateID || 0),
diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts
index 3dc5924d023a..9fe6e8f018d8 100644
--- a/src/libs/calculateAnchorPosition.ts
+++ b/src/libs/calculateAnchorPosition.ts
@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-imports */
-import type {Text as RNText, View} from 'react-native';
import type {ValueOf} from 'type-fest';
+import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
@@ -13,9 +13,9 @@ type AnchorOrigin = {
/**
* Gets the x,y position of the passed in component for the purpose of anchoring another component to it.
*/
-export default function calculateAnchorPosition(anchorComponent: View | RNText, anchorOrigin?: AnchorOrigin): Promise {
+export default function calculateAnchorPosition(anchorComponent: ContextMenuAnchor, anchorOrigin?: AnchorOrigin): Promise {
return new Promise((resolve) => {
- if (!anchorComponent) {
+ if (!anchorComponent || !('measureInWindow' in anchorComponent)) {
resolve({horizontal: 0, vertical: 0});
return;
}
diff --git a/src/libs/focusTextInputAfterAnimation/index.android.ts b/src/libs/focusTextInputAfterAnimation/index.android.ts
index 31c748f5daa4..cca8a6588103 100644
--- a/src/libs/focusTextInputAfterAnimation/index.android.ts
+++ b/src/libs/focusTextInputAfterAnimation/index.android.ts
@@ -19,7 +19,7 @@ import type FocusTextInputAfterAnimation from './types';
*/
const focusTextInputAfterAnimation: FocusTextInputAfterAnimation = (inputRef, animationLength = 0) => {
setTimeout(() => {
- inputRef.focus();
+ inputRef?.focus();
}, animationLength);
};
diff --git a/src/libs/focusTextInputAfterAnimation/index.ts b/src/libs/focusTextInputAfterAnimation/index.ts
index 3f7c6555b5ce..66d0c35c1a63 100644
--- a/src/libs/focusTextInputAfterAnimation/index.ts
+++ b/src/libs/focusTextInputAfterAnimation/index.ts
@@ -4,7 +4,7 @@ import type FocusTextInputAfterAnimation from './types';
* This library is a no-op for all platforms except for Android and iOS and will immediately focus the given input without any delays.
*/
const focusTextInputAfterAnimation: FocusTextInputAfterAnimation = (inputRef) => {
- inputRef.focus();
+ inputRef?.focus();
};
export default focusTextInputAfterAnimation;
diff --git a/src/libs/focusTextInputAfterAnimation/types.ts b/src/libs/focusTextInputAfterAnimation/types.ts
index a6a14165598b..bfe29317c1ef 100644
--- a/src/libs/focusTextInputAfterAnimation/types.ts
+++ b/src/libs/focusTextInputAfterAnimation/types.ts
@@ -1,5 +1,5 @@
import type {TextInput} from 'react-native';
-type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement, animationLength: number) => void;
+type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement | undefined, animationLength: number) => void;
export default FocusTextInputAfterAnimation;
diff --git a/src/libs/getClickedTargetLocation/types.ts b/src/libs/getClickedTargetLocation/types.ts
index 7b1e85e63b17..eed10238be2d 100644
--- a/src/libs/getClickedTargetLocation/types.ts
+++ b/src/libs/getClickedTargetLocation/types.ts
@@ -1,5 +1,5 @@
type DOMRectProperties = 'top' | 'bottom' | 'left' | 'right' | 'height' | 'x' | 'y';
-type GetClickedTargetLocation = (target: Element) => Pick;
+type GetClickedTargetLocation = (target: HTMLDivElement) => Pick;
export default GetClickedTargetLocation;
diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts
index fd03adcffd93..330ba4470097 100644
--- a/src/libs/isReportMessageAttachment.ts
+++ b/src/libs/isReportMessageAttachment.ts
@@ -8,15 +8,15 @@ import type {Message} from '@src/types/onyx/ReportAction';
*
* @param reportActionMessage report action's message as text, html and translationKey
*/
-export default function isReportMessageAttachment({text, html, translationKey}: Message): boolean {
- if (!text || !html) {
+export default function isReportMessageAttachment(message: Message | undefined): boolean {
+ if (!message?.text || !message.html) {
return false;
}
- if (translationKey && text === CONST.ATTACHMENT_MESSAGE_TEXT) {
- return translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT;
+ if (message.translationKey && message.text === CONST.ATTACHMENT_MESSAGE_TEXT) {
+ return message?.translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT;
}
const regex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i');
- return (text === CONST.ATTACHMENT_MESSAGE_TEXT || !!Str.isVideo(text)) && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML);
+ return (message.text === CONST.ATTACHMENT_MESSAGE_TEXT || !!Str.isVideo(message.text)) && (!!message.html.match(regex) || message.html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML);
}
diff --git a/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts b/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts
index dbf2829a6c28..c8ef72ca15e7 100644
--- a/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts
+++ b/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts
@@ -47,6 +47,7 @@ export default function () {
// If newReportActionsDrafts[newOnyxKey] isn't set, fall back on the migrated draft if there is one
const currentActionsDrafts = newReportActionsDrafts[newOnyxKey] ?? allReportActionsDrafts[newOnyxKey];
+
newReportActionsDrafts[newOnyxKey] = {
...currentActionsDrafts,
[reportActionID]: reportActionDraft,
diff --git a/src/libs/navigateAfterJoinRequest/index.desktop.ts b/src/libs/navigateAfterJoinRequest/index.desktop.ts
new file mode 100644
index 000000000000..47180c6a1368
--- /dev/null
+++ b/src/libs/navigateAfterJoinRequest/index.desktop.ts
@@ -0,0 +1,8 @@
+import Navigation from '@navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+
+const navigateAfterJoinRequest = () => {
+ Navigation.goBack(undefined, false, true);
+ Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
+};
+export default navigateAfterJoinRequest;
diff --git a/src/libs/navigateAfterJoinRequest/index.ts b/src/libs/navigateAfterJoinRequest/index.ts
new file mode 100644
index 000000000000..b9e533208ec2
--- /dev/null
+++ b/src/libs/navigateAfterJoinRequest/index.ts
@@ -0,0 +1,8 @@
+import Navigation from '@navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+
+const navigateAfterJoinRequest = () => {
+ Navigation.goBack(undefined, false, true);
+ Navigation.navigate(ROUTES.ALL_SETTINGS);
+};
+export default navigateAfterJoinRequest;
diff --git a/src/libs/navigateAfterJoinRequest/index.web.ts b/src/libs/navigateAfterJoinRequest/index.web.ts
new file mode 100644
index 000000000000..47180c6a1368
--- /dev/null
+++ b/src/libs/navigateAfterJoinRequest/index.web.ts
@@ -0,0 +1,8 @@
+import Navigation from '@navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+
+const navigateAfterJoinRequest = () => {
+ Navigation.goBack(undefined, false, true);
+ Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
+};
+export default navigateAfterJoinRequest;
diff --git a/src/pages/DetailsPage.tsx b/src/pages/DetailsPage.tsx
index a9adb5310e58..b3b0f0782ba0 100755
--- a/src/pages/DetailsPage.tsx
+++ b/src/pages/DetailsPage.tsx
@@ -1,7 +1,7 @@
import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import AttachmentModal from '@components/AttachmentModal';
@@ -15,6 +15,7 @@ import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import UserDetailsTooltip from '@components/UserDetailsTooltip';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/EnablePayments/TermsStep.js b/src/pages/EnablePayments/TermsStep.js
index 9fa3a4becea3..a55816d207be 100644
--- a/src/pages/EnablePayments/TermsStep.js
+++ b/src/pages/EnablePayments/TermsStep.js
@@ -1,9 +1,9 @@
import React, {useEffect, useState} from 'react';
-import {ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import CheckboxWithLabel from '@components/CheckboxWithLabel';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
diff --git a/src/pages/FlagCommentPage.tsx b/src/pages/FlagCommentPage.tsx
index 00c38dabc4ec..216196c17d55 100644
--- a/src/pages/FlagCommentPage.tsx
+++ b/src/pages/FlagCommentPage.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {SvgProps} from 'react-native-svg';
import type {ValueOf} from 'type-fest';
@@ -9,6 +9,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/GetAssistancePage.tsx b/src/pages/GetAssistancePage.tsx
index 948e0c239de9..b543524fc68e 100644
--- a/src/pages/GetAssistancePage.tsx
+++ b/src/pages/GetAssistancePage.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -8,6 +8,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import type {MenuItemWithLink} from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/KeyboardShortcutsPage.tsx b/src/pages/KeyboardShortcutsPage.tsx
index 9b70defbf8af..d68643e74a5a 100644
--- a/src/pages/KeyboardShortcutsPage.tsx
+++ b/src/pages/KeyboardShortcutsPage.tsx
@@ -1,8 +1,9 @@
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItem from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx b/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx
index f27c821abd8c..559da335cf13 100644
--- a/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx
+++ b/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx
@@ -1,5 +1,5 @@
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import Button from '@components/Button';
import FixedFooter from '@components/FixedFooter';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -7,6 +7,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import type {MenuItemProps} from '@components/MenuItem';
import MenuItemList from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx b/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx
index 747b23e943ca..3c7520b850b4 100644
--- a/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx
+++ b/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx
@@ -1,5 +1,5 @@
import React, {useCallback, useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -8,6 +8,7 @@ import LottieAnimations from '@components/LottieAnimations';
import type {MenuItemProps} from '@components/MenuItem';
import MenuItemList from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx
index 0a6a2659ffb6..1893f81da2fe 100644
--- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx
+++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx
@@ -1,11 +1,11 @@
import React, {useMemo} from 'react';
-import {ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxCollection} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js
index a4c740250908..cc533dbc3a08 100755
--- a/src/pages/ProfilePage.js
+++ b/src/pages/ProfilePage.js
@@ -2,7 +2,7 @@ import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useEffect} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import AutoUpdateTime from '@components/AutoUpdateTime';
@@ -17,6 +17,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import UserDetailsTooltip from '@components/UserDetailsTooltip';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js
index 75ae02587486..e18155ea6139 100644
--- a/src/pages/ReimbursementAccount/BankAccountStep.js
+++ b/src/pages/ReimbursementAccount/BankAccountStep.js
@@ -1,7 +1,7 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -11,6 +11,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import MenuItem from '@components/MenuItem';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
diff --git a/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx
index b128d6dc75e8..af4b251952de 100644
--- a/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx
+++ b/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx
@@ -1,10 +1,11 @@
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx
index 2b742ad65699..4228b1da9d12 100644
--- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx
+++ b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx
@@ -1,10 +1,11 @@
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx
index 25ce2d7b81da..42bf43d78910 100644
--- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx
+++ b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx
@@ -1,11 +1,12 @@
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx
index 6a94a7b456f3..a5b839118edc 100644
--- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx
+++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx
@@ -1,6 +1,5 @@
import type {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
import React, {useMemo} from 'react';
-import {ScrollView} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import CheckboxWithLabel from '@components/CheckboxWithLabel';
@@ -8,6 +7,7 @@ import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx b/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx
index 2bf76d714cf5..65f7f14d6c91 100644
--- a/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx
+++ b/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx
@@ -1,9 +1,9 @@
import React from 'react';
-import {ScrollView} from 'react-native';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import MenuItem from '@components/MenuItem';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js b/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js
index d1ac0989ae38..9c28fe928d33 100644
--- a/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js
+++ b/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js
@@ -1,7 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React from 'react';
-import {ScrollView} from 'react-native';
import _ from 'underscore';
import Button from '@components/Button';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -10,6 +9,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
diff --git a/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx b/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx
index fd2f05493098..4c4bd9a20b71 100644
--- a/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx
+++ b/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx
@@ -1,5 +1,4 @@
import React from 'react';
-import {ScrollView} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
@@ -10,6 +9,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
index b4272f094071..f05bb70bcd5a 100644
--- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
+++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
@@ -1,10 +1,11 @@
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/ReimbursementAccount/RequestorOnfidoStep.js b/src/pages/ReimbursementAccount/RequestorOnfidoStep.js
index 8cca56779059..fac405090de7 100644
--- a/src/pages/ReimbursementAccount/RequestorOnfidoStep.js
+++ b/src/pages/ReimbursementAccount/RequestorOnfidoStep.js
@@ -1,12 +1,12 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React from 'react';
-import {ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Onfido from '@components/Onfido';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Growl from '@libs/Growl';
diff --git a/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx b/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx
index d17166365a39..cb9763b5cc25 100644
--- a/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx
+++ b/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx
@@ -1,5 +1,5 @@
import React, {useCallback} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
@@ -8,6 +8,7 @@ import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
// @ts-expect-error TODO: Remove this once Onfido (https://github.com/Expensify/App/issues/25136) is migrated to TypeScript.
import Onfido from '@components/Onfido';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Growl from '@libs/Growl';
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index e94c0cc80952..d96e01c1a4d3 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -1,7 +1,7 @@
import {useRoute} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useEffect, useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
@@ -17,6 +17,7 @@ import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import RoomHeaderAvatars from '@components/RoomHeaderAvatars';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/ShareCodePage.tsx b/src/pages/ShareCodePage.tsx
index f2bba4b17a9a..4f1bac01b556 100644
--- a/src/pages/ShareCodePage.tsx
+++ b/src/pages/ShareCodePage.tsx
@@ -1,5 +1,5 @@
import React, {useMemo, useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {ImageSourcePropType} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png';
@@ -10,6 +10,7 @@ import MenuItem from '@components/MenuItem';
import QRShareWithDownload from '@components/QRShare/QRShareWithDownload';
import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/home/ReportScreenContext.ts b/src/pages/home/ReportScreenContext.ts
index e9440ab932d6..6f177098c2c4 100644
--- a/src/pages/home/ReportScreenContext.ts
+++ b/src/pages/home/ReportScreenContext.ts
@@ -1,8 +1,9 @@
import type {RefObject, SyntheticEvent} from 'react';
import {createContext} from 'react';
-import type {FlatList, GestureResponderEvent, View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {FlatList, GestureResponderEvent, Text, View} from 'react-native';
-type ReactionListAnchor = View | HTMLDivElement | null;
+type ReactionListAnchor = View | Text | HTMLDivElement | null;
type ReactionListEvent = GestureResponderEvent | MouseEvent | SyntheticEvent;
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
index 4f6e0548eb72..974a8824f5ff 100755
--- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
@@ -22,7 +22,7 @@ import type {Beta, ReportAction, ReportActions} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions';
import ContextMenuActions from './ContextMenuActions';
-import type {ContextMenuType} from './ReportActionContextMenu';
+import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu';
import {hideContextMenu, showContextMenu} from './ReportActionContextMenu';
type BaseReportActionContextMenuOnyxProps = {
@@ -64,7 +64,7 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & {
type?: ContextMenuType;
/** Target node which is the target of ContentMenu */
- anchor?: MutableRefObject;
+ anchor?: MutableRefObject;
/** Flag to check if the chat participant is Chronos */
isChronosReport?: boolean;
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index 2ebe08573066..ffdbcab577b7 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -1,7 +1,8 @@
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import type {MutableRefObject} from 'react';
import React from 'react';
-import type {GestureResponderEvent} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {GestureResponderEvent, Text, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {Emoji} from '@assets/emojis/types';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -29,6 +30,7 @@ import type {TranslationPaths} from '@src/languages/types';
import ROUTES from '@src/ROUTES';
import type {Beta, ReportAction, ReportActionReactions, Report as ReportType} from '@src/types/onyx';
import type IconAsset from '@src/types/utils/IconAsset';
+import type {ContextMenuAnchor} from './ReportActionContextMenu';
import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu';
/** Gets the HTML version of the message in an action */
@@ -53,7 +55,7 @@ type ShouldShow = (
reportAction: OnyxEntry,
isArchivedRoom: boolean,
betas: OnyxEntry,
- menuTarget: MutableRefObject | undefined,
+ menuTarget: MutableRefObject | undefined,
isChronosReport: boolean,
reportID: string,
isPinnedChat: boolean,
@@ -70,6 +72,8 @@ type ContextMenuActionPayload = {
close: () => void;
openContextMenu: () => void;
interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void;
+ anchor?: MutableRefObject;
+ checkIfContextMenuActive?: () => void;
openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void;
event?: GestureResponderEvent | MouseEvent | KeyboardEvent;
setIsEmojiPickerActive?: (state: boolean) => void;
@@ -343,9 +347,8 @@ const ContextMenuActions: ContextMenuAction[] = [
// `ContextMenuItem` with `successText` and `successIcon` which will fall back to
// the `text` and `icon`
onPress: (closePopover, {reportAction, selection, reportID}) => {
- const isTaskAction = ReportActionsUtils.isTaskAction(reportAction);
const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction);
- const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction?.actionName) : getActionHtml(reportAction);
+ const messageHtml = getActionHtml(reportAction);
const messageText = ReportActionsUtils.getReportActionMessageText(reportAction);
const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction);
@@ -355,6 +358,9 @@ const ContextMenuActions: ContextMenuAction[] = [
const iouReport = ReportUtils.getReport(ReportActionsUtils.getIOUReportIDFromReportActionPreview(reportAction));
const displayMessage = ReportUtils.getReportPreviewMessage(iouReport, reportAction);
Clipboard.setString(displayMessage);
+ } else if (ReportActionsUtils.isTaskAction(reportAction)) {
+ const displayMessage = TaskUtils.getTaskReportActionMessage(reportAction).text;
+ Clipboard.setString(displayMessage);
} else if (ReportActionsUtils.isModifiedExpenseAction(reportAction)) {
const modifyExpenseMessage = ModifiedExpenseMessage.getForReportAction(reportID, reportAction);
Clipboard.setString(modifyExpenseMessage);
@@ -404,7 +410,7 @@ const ContextMenuActions: ContextMenuAction[] = [
const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction);
// Only hide the copylink menu item when context menu is opened over img element.
- const isAttachmentTarget = menuTarget?.current?.tagName === 'IMG' && isAttachment;
+ const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment;
return Permissions.canUseCommentLinking(betas) && type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction);
},
onPress: (closePopover, {reportAction, reportID}) => {
diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts
index 98b38dcb6968..b7c3d6214094 100644
--- a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts
+++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts
@@ -1,6 +1,6 @@
import type {BaseReportActionContextMenuProps} from '@pages/home/report/ContextMenu/BaseReportActionContextMenu';
-type MiniReportActionContextMenuProps = Omit & {
+type MiniReportActionContextMenuProps = Omit & {
/** Should the reportAction this menu is attached to have the appearance of being grouped with the previous reportAction? */
displayAsGroup?: boolean;
};
diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
index 862d5f01c2fc..931b87704ce5 100644
--- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
@@ -67,8 +67,8 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef(null);
const anchorRef = useRef(null);
const dimensionsEventListener = useRef(null);
- const contextMenuAnchorRef = useRef(null);
- const contextMenuTargetNode = useRef(null);
+ const contextMenuAnchorRef = useRef(null);
+ const contextMenuTargetNode = useRef(null);
const onPopoverShow = useRef(() => {});
const onPopoverHide = useRef(() => {});
@@ -83,7 +83,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef
new Promise((resolve) => {
- if (contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') {
+ if (contextMenuAnchorRef.current && 'measureInWindow' in contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') {
contextMenuAnchorRef.current.measureInWindow((x, y) => resolve({x, y}));
} else {
resolve({x: 0, y: 0});
@@ -169,7 +169,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef {
const {pageX = 0, pageY = 0} = extractPointerEvent(event);
contextMenuAnchorRef.current = contextMenuAnchor;
- contextMenuTargetNode.current = event.target as HTMLElement;
+ contextMenuTargetNode.current = event.target as HTMLDivElement;
if (shouldCloseOnTarget) {
anchorRef.current = event.target as HTMLDivElement;
} else {
diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts
index f2537c56a5af..21c1eea18e03 100644
--- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts
+++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts
@@ -16,7 +16,7 @@ type OnCancel = () => void;
type ContextMenuType = ValueOf;
-type ContextMenuAnchor = View | RNText | null | undefined;
+type ContextMenuAnchor = View | RNText | HTMLDivElement | null | undefined;
type ShowContextMenu = (
type: ContextMenuType,
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.tsx
similarity index 52%
rename from src/pages/home/report/ReportActionItem.js
rename to src/pages/home/report/ReportActionItem.tsx
index 12588845214b..a37138b43039 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -1,9 +1,10 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
+import lodashIsEqual from 'lodash/isEqual';
import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
+import type {GestureResponderEvent, TextInput} from 'react-native';
import {InteractionManager, View} from 'react-native';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {Emoji} from '@assets/emojis/types';
import Button from '@components/Button';
import DisplayNames from '@components/DisplayNames';
import Hoverable from '@components/Hoverable';
@@ -12,11 +13,11 @@ import * as Expensicons from '@components/Icon/Expensicons';
import InlineSystemMessage from '@components/InlineSystemMessage';
import KYCWall from '@components/KYCWall';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
-import {usePersonalDetails, withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '@components/OnyxProvider';
+import {useBlockedFromConcierge, usePersonalDetails, useReportActionsDrafts} from '@components/OnyxProvider';
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
-import EmojiReactionsPropTypes from '@components/Reactions/EmojiReactionsPropTypes';
import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions';
import RenderHTML from '@components/RenderHTML';
+import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons';
import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons';
import ChronosOOOListActions from '@components/ReportActionItem/ChronosOOOListActions';
import MoneyReportView from '@components/ReportActionItem/MoneyReportView';
@@ -30,14 +31,13 @@ import TaskView from '@components/ReportActionItem/TaskView';
import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import Text from '@components/Text';
import UnreadActionIndicator from '@components/UnreadActionIndicator';
-import withLocalize from '@components/withLocalize';
-import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
+import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useReportScrollManager from '@hooks/useReportScrollManager';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
+import useWindowDimensions from '@hooks/useWindowDimensions';
import ControlSelection from '@libs/ControlSelection';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -49,11 +49,10 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import SelectionScraper from '@libs/SelectionScraper';
-import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes';
import {ReactionListContext} from '@pages/home/ReportScreenContext';
-import reportPropTypes from '@pages/reportPropTypes';
import * as BankAccounts from '@userActions/BankAccounts';
import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
+import * as Policy from '@userActions/Policy';
import * as store from '@userActions/ReimbursementAccount/store';
import * as Report from '@userActions/Report';
import * as ReportActions from '@userActions/ReportActions';
@@ -62,6 +61,9 @@ import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type * as OnyxTypes from '@src/types/onyx';
+import type {OriginalMessageActionableMentionWhisper, OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground';
import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu';
import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu';
@@ -75,88 +77,113 @@ import ReportActionItemMessage from './ReportActionItemMessage';
import ReportActionItemMessageEdit from './ReportActionItemMessageEdit';
import ReportActionItemSingle from './ReportActionItemSingle';
import ReportActionItemThread from './ReportActionItemThread';
-import reportActionPropTypes from './reportActionPropTypes';
import ReportAttachmentsContext from './ReportAttachmentsContext';
-const propTypes = {
- ...windowDimensionsPropTypes,
+const getDraftMessage = (drafts: OnyxCollection, reportID: string, action: OnyxTypes.ReportAction): string | undefined => {
+ const originalReportID = ReportUtils.getOriginalReportID(reportID, action);
+ const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`;
+ const draftMessage = drafts?.[draftKey]?.[action.reportActionID];
+ return typeof draftMessage === 'string' ? draftMessage : draftMessage?.message;
+};
+
+type ReportActionItemOnyxProps = {
+ /** Stores user's preferred skin tone */
+ preferredSkinTone: OnyxEntry;
+
+ /** IOU report for this action, if any */
+ iouReport: OnyxEntry;
+
+ emojiReactions: OnyxEntry;
+ /** The user's wallet account */
+ userWallet: OnyxEntry;
+
+ /** All the report actions belonging to the report's parent */
+ parentReportActions: OnyxEntry;
+
+ /** All policy report fields */
+ policyReportFields: OnyxEntry;
+
+ /** The policy which the user has access to and which the report is tied to */
+ policy: OnyxEntry;
+};
+
+type ReportActionItemProps = {
/** Report for this action */
- report: reportPropTypes.isRequired,
+ report: OnyxTypes.Report;
/** All the data of the action item */
- action: PropTypes.shape(reportActionPropTypes).isRequired,
+ action: OnyxTypes.ReportAction;
/** Should the comment have the appearance of being grouped with the previous comment? */
- displayAsGroup: PropTypes.bool.isRequired,
+ displayAsGroup: boolean;
/** Is this the most recent IOU Action? */
- isMostRecentIOUReportAction: PropTypes.bool.isRequired,
+ isMostRecentIOUReportAction: boolean;
/** Should we display the new marker on top of the comment? */
- shouldDisplayNewMarker: PropTypes.bool.isRequired,
+ shouldDisplayNewMarker: boolean;
/** Determines if the avatar is displayed as a subscript (positioned lower than normal) */
- shouldShowSubscriptAvatar: PropTypes.bool,
+ shouldShowSubscriptAvatar?: boolean;
/** Position index of the report action in the overall report FlatList view */
- index: PropTypes.number.isRequired,
-
- /** Draft message - if this is set the comment is in 'edit' mode */
- draftMessage: PropTypes.string,
-
- /** Stores user's preferred skin tone */
- preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
-
- emojiReactions: EmojiReactionsPropTypes,
-
- /** IOU report for this action, if any */
- iouReport: reportPropTypes,
+ index: number;
/** Flag to show, hide the thread divider line */
- shouldHideThreadDividerLine: PropTypes.bool,
+ shouldHideThreadDividerLine?: boolean;
- /** The user's wallet account */
- userWallet: userWalletPropTypes,
-
- /** All the report actions belonging to the report's parent */
- parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+ linkedReportActionID?: string;
/** Callback to be called on onPress */
- onPress: PropTypes.func,
-};
-
-const defaultProps = {
- draftMessage: undefined,
- preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
- emojiReactions: {},
- shouldShowSubscriptAvatar: false,
- iouReport: undefined,
- shouldHideThreadDividerLine: false,
- userWallet: {},
- parentReportActions: {},
- onPress: undefined,
-};
-
-function ReportActionItem(props) {
+ onPress?: () => void;
+} & ReportActionItemOnyxProps;
+
+const isIOUReport = (actionObj: OnyxEntry): actionObj is OnyxTypes.ReportActionBase & OnyxTypes.OriginalMessageIOU =>
+ actionObj?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU;
+
+function ReportActionItem({
+ action,
+ report,
+ linkedReportActionID,
+ displayAsGroup,
+ emojiReactions,
+ index,
+ iouReport,
+ isMostRecentIOUReportAction,
+ parentReportActions,
+ preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE,
+ shouldDisplayNewMarker,
+ userWallet,
+ shouldHideThreadDividerLine = false,
+ shouldShowSubscriptAvatar = false,
+ policyReportFields,
+ policy,
+ onPress = undefined,
+}: ReportActionItemProps) {
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const blockedFromConcierge = useBlockedFromConcierge();
+ const reportActionDrafts = useReportActionsDrafts();
+ const draftMessage = useMemo(() => getDraftMessage(reportActionDrafts, report.reportID, action), [action, report.reportID, reportActionDrafts]);
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
- const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(props.action.reportActionID));
- const [isEmojiPickerActive, setIsEmojiPickerActive] = useState();
+ const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(action.reportActionID));
+ const [isEmojiPickerActive, setIsEmojiPickerActive] = useState();
const [isHidden, setIsHidden] = useState(false);
- const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
+ const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
const reactionListRef = useContext(ReactionListContext);
const {updateHiddenAttachments} = useContext(ReportAttachmentsContext);
- const textInputRef = useRef();
- const popoverAnchorRef = useRef();
- const downloadedPreviews = useRef([]);
- const prevDraftMessage = usePrevious(props.draftMessage);
- const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action);
- const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID);
- const isReportActionLinked = props.linkedReportActionID && props.action.reportActionID && props.linkedReportActionID === props.action.reportActionID;
+ const textInputRef = useRef();
+ const popoverAnchorRef = useRef(null);
+ const downloadedPreviews = useRef([]);
+ const prevDraftMessage = usePrevious(draftMessage);
+ const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action);
+ const originalReport = report.reportID === originalReportID ? report : ReportUtils.getReport(originalReportID);
+ const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID;
const reportScrollManager = useReportScrollManager();
@@ -164,117 +191,120 @@ function ReportActionItem(props) {
() => (isReportActionLinked ? StyleUtils.getBackgroundColorStyle(theme.hoverComponentBG) : {}),
[StyleUtils, isReportActionLinked, theme.hoverComponentBG],
);
- const originalMessage = lodashGet(props.action, 'originalMessage', {});
- const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(props.action);
- const prevActionResolution = usePrevious(lodashGet(props.action, 'originalMessage.resolution', null));
+
+ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action);
+ const prevActionResolution = usePrevious(ReportActionsUtils.isActionableMentionWhisper(action) ? action.originalMessage.resolution : null);
// IOUDetails only exists when we are sending money
- const isSendingMoney = originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && _.has(originalMessage, 'IOUDetails');
+ const isSendingMoney = isIOUReport(action) && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && action.originalMessage.IOUDetails;
const updateHiddenState = useCallback(
- (isHiddenValue) => {
+ (isHiddenValue: boolean) => {
setIsHidden(isHiddenValue);
- const isAttachment = ReportUtils.isReportMessageAttachment(_.last(props.action.message));
+ const isAttachment = ReportUtils.isReportMessageAttachment(action.message?.at(-1));
if (!isAttachment) {
return;
}
- updateHiddenAttachments(props.action.reportActionID, isHiddenValue);
+ updateHiddenAttachments(action.reportActionID, isHiddenValue);
},
- [props.action.reportActionID, props.action.message, updateHiddenAttachments],
+ [action.reportActionID, action.message, updateHiddenAttachments],
);
useEffect(
() => () => {
// ReportActionContextMenu, EmojiPicker and PopoverReactionList are global components,
// we should also hide them when the current component is destroyed
- if (ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) {
+ if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) {
ReportActionContextMenu.hideContextMenu();
ReportActionContextMenu.hideDeleteModal();
}
- if (EmojiPickerAction.isActive(props.action.reportActionID)) {
+ if (EmojiPickerAction.isActive(action.reportActionID)) {
EmojiPickerAction.hideEmojiPicker(true);
}
- if (reactionListRef.current && reactionListRef.current.isActiveReportAction(props.action.reportActionID)) {
- reactionListRef.current.hideReactionList();
+ if (reactionListRef?.current?.isActiveReportAction(action.reportActionID)) {
+ reactionListRef?.current?.hideReactionList();
}
},
- [props.action.reportActionID, reactionListRef],
+ [action.reportActionID, reactionListRef],
);
useEffect(() => {
// We need to hide EmojiPicker when this is a deleted parent action
- if (!isDeletedParentAction || !EmojiPickerAction.isActive(props.action.reportActionID)) {
+ if (!isDeletedParentAction || !EmojiPickerAction.isActive(action.reportActionID)) {
return;
}
EmojiPickerAction.hideEmojiPicker(true);
- }, [isDeletedParentAction, props.action.reportActionID]);
+ }, [isDeletedParentAction, action.reportActionID]);
useEffect(() => {
- if (!_.isUndefined(prevDraftMessage) || _.isUndefined(props.draftMessage)) {
+ if (prevDraftMessage !== undefined || draftMessage === undefined) {
return;
}
focusTextInputAfterAnimation(textInputRef.current, 100);
- }, [prevDraftMessage, props.draftMessage]);
+ }, [prevDraftMessage, draftMessage]);
useEffect(() => {
if (!Permissions.canUseLinkPreviews()) {
return;
}
- const urls = ReportActionsUtils.extractLinksFromMessageHtml(props.action);
- if (_.isEqual(downloadedPreviews.current, urls) || props.action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ const urls = ReportActionsUtils.extractLinksFromMessageHtml(action);
+ if (lodashIsEqual(downloadedPreviews.current, urls) || action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
return;
}
downloadedPreviews.current = urls;
- Report.expandURLPreview(props.report.reportID, props.action.reportActionID);
- }, [props.action, props.report.reportID]);
+ Report.expandURLPreview(report.reportID, action.reportActionID);
+ }, [action, report.reportID]);
useEffect(() => {
- if (_.isUndefined(props.draftMessage) || !ReportActionsUtils.isDeletedAction(props.action)) {
+ if (draftMessage === undefined || !ReportActionsUtils.isDeletedAction(action)) {
return;
}
- Report.deleteReportActionDraft(props.report.reportID, props.action);
- }, [props.draftMessage, props.action, props.report.reportID]);
+ Report.deleteReportActionDraft(report.reportID, action);
+ }, [draftMessage, action, report.reportID]);
// Hide the message if it is being moderated for a higher offense, or is hidden by a moderator
// Removed messages should not be shown anyway and should not need this flow
- const latestDecision = lodashGet(props, ['action', 'message', 0, 'moderationDecision', 'decision'], '');
+ const latestDecision = action.message?.[0].moderationDecision?.decision ?? '';
useEffect(() => {
- if (props.action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) {
+ if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) {
return;
}
// Hide reveal message button and show the message if latestDecision is changed to empty
- if (_.isEmpty(latestDecision)) {
+ if (!latestDecision) {
setModerationDecision(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
setIsHidden(false);
return;
}
setModerationDecision(latestDecision);
- if (!_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], latestDecision) && !ReportActionsUtils.isPendingRemove(props.action)) {
+ if (
+ ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === latestDecision) &&
+ !ReportActionsUtils.isPendingRemove(action)
+ ) {
setIsHidden(true);
return;
}
setIsHidden(false);
- }, [latestDecision, props.action]);
+ }, [latestDecision, action]);
const toggleContextMenuFromActiveReportAction = useCallback(() => {
- setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(props.action.reportActionID));
- }, [props.action.reportActionID]);
+ setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(action.reportActionID));
+ }, [action.reportActionID]);
/**
* Show the ReportActionContextMenu modal popover.
*
- * @param {Object} [event] - A press event.
+ * @param [event] - A press event.
*/
const showPopover = useCallback(
- (event) => {
+ (event: GestureResponderEvent | MouseEvent) => {
// Block menu on the message being Edited or if the report action item has errors
- if (!_.isUndefined(props.draftMessage) || !_.isEmpty(props.action.errors)) {
+ if (draftMessage !== undefined || !isEmptyObject(action.errors)) {
return;
}
@@ -284,11 +314,11 @@ function ReportActionItem(props) {
CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
event,
selection,
- popoverAnchorRef,
- props.report.reportID,
- props.action.reportActionID,
+ popoverAnchorRef.current,
+ report.reportID,
+ action.reportActionID,
originalReportID,
- props.draftMessage,
+ draftMessage ?? '',
() => setIsContextMenuActive(true),
toggleContextMenuFromActiveReportAction,
ReportUtils.isArchivedRoom(originalReport),
@@ -297,168 +327,184 @@ function ReportActionItem(props) {
false,
[],
false,
- setIsEmojiPickerActive,
+ setIsEmojiPickerActive as () => void,
);
},
- [props.draftMessage, props.action, props.report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID],
+ [draftMessage, action, report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID],
);
// Handles manual scrolling to the bottom of the chat when the last message is an actionable mention whisper and it's resolved.
// This fixes an issue where InvertedFlatList fails to auto scroll down and results in an empty space at the bottom of the chat in IOS.
useEffect(() => {
- if (props.index !== 0 || !ReportActionsUtils.isActionableMentionWhisper(props.action)) {
+ if (index !== 0 || !ReportActionsUtils.isActionableMentionWhisper(action)) {
return;
}
- if (prevActionResolution !== lodashGet(props.action, 'originalMessage.resolution', null)) {
- reportScrollManager.scrollToIndex(props.index);
+ if (ReportActionsUtils.isActionableMentionWhisper(action) && prevActionResolution !== (action.originalMessage.resolution ?? null)) {
+ reportScrollManager.scrollToIndex(index);
}
- }, [props.index, props.action, prevActionResolution, reportScrollManager]);
+ }, [index, action, prevActionResolution, reportScrollManager]);
const toggleReaction = useCallback(
- (emoji) => {
- Report.toggleEmojiReaction(props.report.reportID, props.action, emoji, props.emojiReactions);
+ (emoji: Emoji) => {
+ Report.toggleEmojiReaction(report.reportID, action, emoji, emojiReactions);
},
- [props.report, props.action, props.emojiReactions],
+ [report, action, emojiReactions],
);
const contextValue = useMemo(
() => ({
- anchor: popoverAnchorRef,
- report: props.report,
- action: props.action,
+ anchor: popoverAnchorRef.current,
+ report,
+ action,
checkIfContextMenuActive: toggleContextMenuFromActiveReportAction,
}),
- [props.report, props.action, toggleContextMenuFromActiveReportAction],
+ [report, action, toggleContextMenuFromActiveReportAction],
);
- const actionableItemButtons = useMemo(() => {
- if (!(ReportActionsUtils.isActionableMentionWhisper(props.action) && !lodashGet(props.action, 'originalMessage.resolution', null))) {
+ const actionableItemButtons: ActionableItem[] = useMemo(() => {
+ const isWhisperResolution = (action?.originalMessage as OriginalMessageActionableMentionWhisper['originalMessage'])?.resolution !== null;
+ const isJoinChoice = (action?.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage'])?.choice === '';
+
+ if (!((ReportActionsUtils.isActionableMentionWhisper(action) && isWhisperResolution) || (ReportActionsUtils.isActionableJoinRequest(action) && isJoinChoice))) {
return [];
}
+
+ if (ReportActionsUtils.isActionableJoinRequest(action)) {
+ return [
+ {
+ text: 'actionableMentionJoinWorkspaceOptions.accept',
+ key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT}`,
+ onPress: () => Policy.acceptJoinRequest(report.reportID, action),
+ isPrimary: true,
+ },
+ {
+ text: 'actionableMentionJoinWorkspaceOptions.decline',
+ key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE}`,
+ onPress: () => Policy.declineJoinRequest(report.reportID, action),
+ },
+ ];
+ }
return [
{
text: 'actionableMentionWhisperOptions.invite',
- key: `${props.action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`,
- onPress: () => Report.resolveActionableMentionWhisper(props.report.reportID, props.action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE),
+ key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`,
+ onPress: () => Report.resolveActionableMentionWhisper(report.reportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE),
isPrimary: true,
},
{
text: 'actionableMentionWhisperOptions.nothing',
- key: `${props.action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`,
- onPress: () => Report.resolveActionableMentionWhisper(props.report.reportID, props.action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING),
+ key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`,
+ onPress: () => Report.resolveActionableMentionWhisper(report.reportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING),
},
];
- }, [props.action, props.report.reportID]);
+ }, [action, report.reportID]);
/**
* Get the content of ReportActionItem
- * @param {Boolean} hovered whether the ReportActionItem is hovered
- * @param {Boolean} isWhisper whether the report action is a whisper
- * @param {Boolean} hasErrors whether the report action has any errors
- * @returns {Object} child component(s)
+ * @param hovered whether the ReportActionItem is hovered
+ * @param isWhisper whether the report action is a whisper
+ * @param hasErrors whether the report action has any errors
+ * @returns child component(s)
*/
- const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false) => {
+ const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false): React.JSX.Element => {
let children;
// Show the MoneyRequestPreview for when request was created, bill was split or money was sent
if (
- props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU &&
- originalMessage &&
+ isIOUReport(action) &&
+ action.originalMessage &&
// For the pay flow, we only want to show MoneyRequestAction when sending money. When paying, we display a regular system message
- (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney)
+ (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney)
) {
// There is no single iouReport for bill splits, so only 1:1 requests require an iouReportID
- const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0';
+ const iouReportID = action.originalMessage.IOUReportID ? action.originalMessage.IOUReportID.toString() : '0';
children = (
);
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) {
- children = (
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) {
+ children = ReportUtils.isClosedExpenseReportWithNoExpenses(iouReport) ? (
+ ${translate('parentReportAction.deletedReport')}`} />
+ ) : (
);
- } else if (
- props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED ||
- props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED ||
- props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED
- ) {
- children = ;
- } else if (ReportActionsUtils.isCreatedTaskReportAction(props.action)) {
+ } else if (ReportActionsUtils.isTaskAction(action)) {
+ children = ;
+ } else if (ReportActionsUtils.isCreatedTaskReportAction(action)) {
children = (
);
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) {
- const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID));
- const paymentType = lodashGet(props.action, 'originalMessage.paymentType', '');
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) {
+ const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails[report.ownerAccountID ?? -1]);
+ const paymentType = action.originalMessage.paymentType ?? '';
- const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID);
+ const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(report.reportID) && !ReportUtils.isSettled(report.reportID);
const shouldShowAddCreditBankAccountButton = isSubmitterOfUnsettledReport && !store.hasCreditBankAccount() && paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY;
const shouldShowEnableWalletButton =
- isSubmitterOfUnsettledReport &&
- (_.isEmpty(props.userWallet) || props.userWallet.tierName === CONST.WALLET.TIER_NAME.SILVER) &&
- paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY;
+ isSubmitterOfUnsettledReport && (isEmptyObject(userWallet) || userWallet?.tierName === CONST.WALLET.TIER_NAME.SILVER) && paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY;
children = (
<>
{shouldShowAddCreditBankAccountButton && (
);
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) {
- children = ;
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) {
- children = ;
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) {
- children = ;
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) {
- children = ;
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) {
+ children = ;
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) {
+ children = ;
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) {
+ children = ;
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) {
+ children = ;
} else {
const hasBeenFlagged =
- !_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], moderationDecision) &&
- !ReportActionsUtils.isPendingRemove(props.action);
+ ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) &&
+ !ReportActionsUtils.isPendingRemove(action);
children = (
- {_.isUndefined(props.draftMessage) ? (
-
+ {draftMessage === undefined ? (
+
{hasBeenFlagged && (
@@ -499,7 +545,7 @@ function ReportActionItem(props) {
style={[styles.buttonSmallText, styles.userSelectNone]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
- {isHidden ? props.translate('moderation.revealMessage') : props.translate('moderation.hideMessage')}
+ {isHidden ? translate('moderation.revealMessage') : translate('moderation.hideMessage')}
)}
@@ -508,49 +554,46 @@ function ReportActionItem(props) {
for example: Invite a user mentioned but not a member of the room
https://github.com/Expensify/App/issues/32741
*/}
- {actionableItemButtons.length > 0 && (
-
- )}
+ {actionableItemButtons.length > 0 && }
) : (
)}
);
}
- const numberOfThreadReplies = _.get(props, ['action', 'childVisibleActionCount'], 0);
+ const numberOfThreadReplies = action.childVisibleActionCount ?? 0;
- const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(props.action, props.report.reportID);
- const oldestFourAccountIDs = _.map(lodashGet(props.action, 'childOldestFourAccountIDs', '').split(','), (accountID) => Number(accountID));
- const draftMessageRightAlign = !_.isUndefined(props.draftMessage) ? styles.chatItemReactionsDraftRight : {};
+ const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(action, report.reportID);
+ const oldestFourAccountIDs =
+ action.childOldestFourAccountIDs
+ ?.split(',')
+ .map((accountID) => Number(accountID))
+ .filter((accountID): accountID is number => typeof accountID === 'number') ?? [];
+ const draftMessageRightAlign = draftMessage !== undefined ? styles.chatItemReactionsDraftRight : {};
return (
<>
{children}
- {Permissions.canUseLinkPreviews() && !isHidden && !_.isEmpty(props.action.linkMetadata) && (
-
- !_.isEmpty(item))} />
+ {Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && (
+
+ !isEmptyObject(item))} />
)}
- {!ReportActionsUtils.isMessageDeleted(props.action) && (
+ {!ReportActionsUtils.isMessageDeleted(action) && (
{
if (Session.isAnonymousUser()) {
@@ -571,9 +614,9 @@ function ReportActionItem(props) {
{shouldDisplayThreadReplies && (
{
+ const renderReportActionItem = (hovered: boolean, isWhisper: boolean, hasErrors: boolean): React.JSX.Element => {
const content = renderItemContent(hovered || isContextMenuActive || isEmojiPickerActive, isWhisper, hasErrors);
- if (!_.isUndefined(props.draftMessage)) {
+ if (draftMessage !== undefined) {
return {content};
}
- if (!props.displayAsGroup) {
+ if (!displayAsGroup) {
return (
item === moderationDecision) &&
+ !ReportActionsUtils.isPendingRemove(action)
}
>
{content}
@@ -621,23 +664,23 @@ function ReportActionItem(props) {
return {content};
};
- if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) {
- const parentReportAction = props.parentReportActions[props.report.parentReportActionID];
+ if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) {
+ const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''] ?? null;
if (ReportActionsUtils.isTransactionThread(parentReportAction)) {
const isReversedTransaction = ReportActionsUtils.isReversedTransaction(parentReportAction);
if (ReportActionsUtils.isDeletedParentAction(parentReportAction) || isReversedTransaction) {
return (
-
+
-
-
+
+
${props.translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`}
+ html={`${translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`}
/>
@@ -649,25 +692,25 @@ function ReportActionItem(props) {
return (
);
}
- if (ReportUtils.isTaskReport(props.report)) {
- if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) {
+ if (ReportUtils.isTaskReport(report)) {
+ if (ReportUtils.isCanceledTaskReport(report, parentReportAction)) {
return (
-
+
-
-
+
+
- ${props.translate('parentReportAction.deletedTask')}`} />
+ ${translate('parentReportAction.deletedTask')}`} />
@@ -676,25 +719,25 @@ function ReportActionItem(props) {
);
}
return (
-
+
-
+
);
}
- if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) {
+ if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) {
return (
-
+
);
@@ -702,96 +745,94 @@ function ReportActionItem(props) {
return (
);
}
- if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) {
- return ;
+ if (action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) {
+ return ;
}
- if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) {
+ if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) {
return (
);
}
// For the `pay` IOU action on non-send money flow, we don't want to render anything if `isWaitingOnBankAccount` is true
// Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet
- if (
- props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU &&
- lodashGet(props.report, 'isWaitingOnBankAccount', false) &&
- originalMessage &&
- originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY &&
- !isSendingMoney
- ) {
+ if (isIOUReport(action) && !!report?.isWaitingOnBankAccount && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney) {
return null;
}
// if action is actionable mention whisper and resolved by user, then we don't want to render anything
- if (ReportActionsUtils.isActionableMentionWhisper(props.action) && lodashGet(props.action, 'originalMessage.resolution', null)) {
+ if (ReportActionsUtils.isActionableMentionWhisper(action) && (action.originalMessage.resolution ?? null)) {
return null;
}
// We currently send whispers to all report participants and hide them in the UI for users that shouldn't see them.
// This is a temporary solution needed for comment-linking.
// The long term solution will leverage end-to-end encryption and only targeted users will be able to decrypt.
- if (ReportActionsUtils.isWhisperActionTargetedToOthers(props.action)) {
+ if (ReportActionsUtils.isWhisperActionTargetedToOthers(action)) {
return null;
}
- const hasErrors = !_.isEmpty(props.action.errors);
- const whisperedToAccountIDs = props.action.whisperedToAccountIDs || [];
+ const hasErrors = !isEmptyObject(action.errors);
+ const whisperedToAccountIDs = action.whisperedToAccountIDs ?? [];
const isWhisper = whisperedToAccountIDs.length > 0;
const isMultipleParticipant = whisperedToAccountIDs.length > 1;
const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs);
- const whisperedToPersonalDetails = isWhisper ? _.filter(personalDetails, (details) => _.includes(whisperedToAccountIDs, details.accountID)) : [];
+ const whisperedToPersonalDetails = isWhisper
+ ? (Object.values(personalDetails ?? {}).filter((details) => whisperedToAccountIDs.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[])
+ : [];
const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : [];
return (
props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
+ onPress={onPress}
+ style={[action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? styles.pointerEventsNone : styles.pointerEventsAuto]}
+ onPressIn={() => isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
onSecondaryInteraction={showPopover}
- preventDefaultContextMenu={_.isUndefined(props.draftMessage) && !hasErrors}
+ preventDefaultContextMenu={draftMessage === undefined && !hasErrors}
withoutFocusOnSecondaryInteraction
- accessibilityLabel={props.translate('accessibilityHints.chatMessage')}
+ accessibilityLabel={translate('accessibilityHints.chatMessage')}
+ accessible
>
{(hovered) => (
- {props.shouldDisplayNewMarker && }
+ {shouldDisplayNewMarker && }
-
+
ReportActions.clearReportActionErrors(props.report.reportID, props.action)}
+ onClose={() => ReportActions.clearReportActionErrors(report.reportID, action)}
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
pendingAction={
- !_.isUndefined(props.draftMessage) ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '')
+ draftMessage !== undefined ? undefined : action.pendingAction ?? (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined)
}
- shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)}
- errors={ErrorUtils.getLatestErrorMessageField(props.action)}
+ shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)}
+ errors={ErrorUtils.getLatestErrorMessageField(action as ErrorUtils.OnyxDataWithErrors)}
errorRowStyles={[styles.ml10, styles.mr2]}
- needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(props.action)}
+ needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)}
shouldDisableStrikeThrough
>
{isWhisper && (
@@ -804,11 +845,11 @@ function ReportActionItem(props) {
/>
- {props.translate('reportActionContextMenu.onlyVisible')}
+ {translate('reportActionContextMenu.onlyVisible')}
)}
- {renderReportActionItem(hovered || isReportActionLinked, isWhisper, hasErrors)}
+ {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)}
)}
-
+ {/* @ts-expect-error TODO check if there is a field on the reportAction object */}
+
);
}
-ReportActionItem.propTypes = propTypes;
-ReportActionItem.defaultProps = defaultProps;
-
-export default compose(
- withWindowDimensions,
- withLocalize,
- withNetwork(),
- withBlockedFromConcierge({propName: 'blockedFromConcierge'}),
- withReportActionsDrafts({
- propName: 'draftMessage',
- transformValue: (drafts, props) => {
- const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action);
- const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`;
- return lodashGet(drafts, [draftKey, props.action.reportActionID, 'message']);
- },
- }),
- withOnyx({
- preferredSkinTone: {
- key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
- initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE,
- },
- iouReport: {
- key: ({action}) => {
- const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action);
- return iouReportID ? `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}` : undefined;
- },
- initialValue: {},
- },
- policyReportFields: {
- key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined),
- initialValue: [],
+export default withOnyx({
+ preferredSkinTone: {
+ key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
+ initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE,
+ },
+ iouReport: {
+ key: ({action}) => {
+ const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action);
+ return `${ONYXKEYS.COLLECTION.REPORT}${iouReportID ?? ''}`;
},
- policy: {
- key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}` : undefined),
- initialValue: {},
- },
- emojiReactions: {
- key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`,
- initialValue: {},
- },
- userWallet: {
- key: ONYXKEYS.USER_WALLET,
- },
- parentReportActions: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID || 0}`,
- canEvict: false,
- },
- }),
-)(
+ initialValue: {} as OnyxTypes.Report,
+ },
+ policyReportFields: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID ?? ''}`,
+ initialValue: {},
+ },
+ policy: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID ?? ''}`,
+ initialValue: {} as OnyxTypes.Policy,
+ },
+ emojiReactions: {
+ key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`,
+ initialValue: {},
+ },
+ userWallet: {
+ key: ONYXKEYS.USER_WALLET,
+ },
+ parentReportActions: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? 0}`,
+ canEvict: false,
+ },
+})(
memo(ReportActionItem, (prevProps, nextProps) => {
- const prevParentReportAction = prevProps.parentReportActions[prevProps.report.parentReportActionID];
- const nextParentReportAction = nextProps.parentReportActions[nextProps.report.parentReportActionID];
+ const prevParentReportAction = prevProps.parentReportActions?.[prevProps.report.parentReportActionID ?? ''];
+ const nextParentReportAction = nextProps.parentReportActions?.[nextProps.report.parentReportActionID ?? ''];
return (
prevProps.displayAsGroup === nextProps.displayAsGroup &&
- prevProps.draftMessage === nextProps.draftMessage &&
prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction &&
prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker &&
- _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) &&
- _.isEqual(prevProps.action, nextProps.action) &&
- _.isEqual(prevProps.iouReport, nextProps.iouReport) &&
- _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) &&
- _.isEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) &&
- _.isEqual(prevProps.report.errorFields, nextProps.report.errorFields) &&
- lodashGet(prevProps.report, 'statusNum') === lodashGet(nextProps.report, 'statusNum') &&
- lodashGet(prevProps.report, 'stateNum') === lodashGet(nextProps.report, 'stateNum') &&
- lodashGet(prevProps.report, 'parentReportID') === lodashGet(nextProps.report, 'parentReportID') &&
- lodashGet(prevProps.report, 'parentReportActionID') === lodashGet(nextProps.report, 'parentReportActionID') &&
- prevProps.translate === nextProps.translate &&
+ lodashIsEqual(prevProps.emojiReactions, nextProps.emojiReactions) &&
+ lodashIsEqual(prevProps.action, nextProps.action) &&
+ lodashIsEqual(prevProps.iouReport, nextProps.iouReport) &&
+ lodashIsEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) &&
+ lodashIsEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) &&
+ lodashIsEqual(prevProps.report.errorFields, nextProps.report.errorFields) &&
+ prevProps.report?.statusNum === nextProps.report?.statusNum &&
+ prevProps.report?.stateNum === nextProps.report?.stateNum &&
+ prevProps.report?.parentReportID === nextProps.report?.parentReportID &&
+ prevProps.report?.parentReportActionID === nextProps.report?.parentReportActionID &&
// TaskReport's created actions render the TaskView, which updates depending on certain fields in the TaskReport
ReportUtils.isTaskReport(prevProps.report) === ReportUtils.isTaskReport(nextProps.report) &&
prevProps.action.actionName === nextProps.action.actionName &&
@@ -906,13 +929,13 @@ export default compose(
ReportUtils.isCompletedTaskReport(prevProps.report) === ReportUtils.isCompletedTaskReport(nextProps.report) &&
prevProps.report.managerID === nextProps.report.managerID &&
prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine &&
- lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) &&
- lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) &&
+ prevProps.report?.total === nextProps.report?.total &&
+ prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal &&
prevProps.linkedReportActionID === nextProps.linkedReportActionID &&
- _.isEqual(prevProps.policyReportFields, nextProps.policyReportFields) &&
- _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields) &&
- _.isEqual(prevProps.policy, nextProps.policy) &&
- _.isEqual(prevParentReportAction, nextParentReportAction)
+ lodashIsEqual(prevProps.policyReportFields, nextProps.policyReportFields) &&
+ lodashIsEqual(prevProps.report.reportFields, nextProps.report.reportFields) &&
+ lodashIsEqual(prevProps.policy, nextProps.policy) &&
+ lodashIsEqual(prevParentReportAction, nextParentReportAction)
);
}),
);
diff --git a/src/pages/home/report/ReportActionItemBasicMessage.tsx b/src/pages/home/report/ReportActionItemBasicMessage.tsx
index 35141a42b726..a28f2af24448 100644
--- a/src/pages/home/report/ReportActionItemBasicMessage.tsx
+++ b/src/pages/home/report/ReportActionItemBasicMessage.tsx
@@ -5,7 +5,7 @@ import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
-type ReportActionItemBasicMessageProps = ChildrenProps & {
+type ReportActionItemBasicMessageProps = Partial & {
message: string;
};
diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx
index 95578c10e816..4fe52f6adf41 100644
--- a/src/pages/home/report/ReportActionItemCreated.tsx
+++ b/src/pages/home/report/ReportActionItemCreated.tsx
@@ -35,7 +35,7 @@ type ReportActionItemCreatedProps = ReportActionItemCreatedOnyxProps & {
/** The id of the policy */
// eslint-disable-next-line react/no-unused-prop-types
- policyID: string;
+ policyID: string | undefined;
};
function ReportActionItemCreated(props: ReportActionItemCreatedProps) {
const styles = useThemeStyles();
diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx
index e16d94eb7db7..04391bb19cd5 100644
--- a/src/pages/home/report/ReportActionItemFragment.tsx
+++ b/src/pages/home/report/ReportActionItemFragment.tsx
@@ -70,6 +70,7 @@ const MUTED_ACTIONS = [
CONST.REPORT.ACTIONS.TYPE.IOU,
CONST.REPORT.ACTIONS.TYPE.APPROVED,
CONST.REPORT.ACTIONS.TYPE.MOVED,
+ CONST.REPORT.ACTIONS.TYPE.ACTIONABLEJOINREQUEST,
] as ActionName[];
function ReportActionItemFragment({
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx
index 2c9a4cbd21e8..fbf2da69aa31 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx
@@ -5,6 +5,7 @@ import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {Keyboard, View} from 'react-native';
import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import type {Emoji} from '@assets/emojis/types';
import Composer from '@components/Composer';
import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton';
@@ -58,7 +59,7 @@ type ReportActionItemMessageEditProps = {
shouldDisableEmojiPicker?: boolean;
/** Stores user's preferred skin tone */
- preferredSkinTone?: number;
+ preferredSkinTone?: OnyxEntry;
};
// native ids
@@ -69,7 +70,7 @@ const isMobileSafari = Browser.isMobileSafari();
function ReportActionItemMessageEdit(
{action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps,
- forwardedRef: ForwardedRef,
+ forwardedRef: ForwardedRef<(TextInput & HTMLTextAreaElement) | undefined>,
) {
const theme = useTheme();
const styles = useThemeStyles();
diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx
index af1c4e85104e..4a041fc495c0 100644
--- a/src/pages/home/report/ReportActionItemParentAction.tsx
+++ b/src/pages/home/report/ReportActionItemParentAction.tsx
@@ -83,7 +83,6 @@ function ReportActionItemParentAction({report, index = 0, shouldHideThreadDivide
onClose={() => Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)}
>
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID))}
report={ancestor.report}
action={ancestor.reportAction}
diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 741422cc7e82..696cd7a7d850 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -1,6 +1,7 @@
import React, {useCallback, useMemo} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import Avatar from '@components/Avatar';
import MultipleAvatars from '@components/MultipleAvatars';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
@@ -29,7 +30,7 @@ import ReportActionItemFragment from './ReportActionItemFragment';
type ReportActionItemSingleProps = Partial & {
/** All the data of the action */
- action: ReportAction;
+ action: OnyxEntry;
/** Styles for the outermost View */
wrapperStyle?: StyleProp;
@@ -38,7 +39,7 @@ type ReportActionItemSingleProps = Partial & {
report: Report;
/** IOU Report for this action, if any */
- iouReport?: Report;
+ iouReport?: OnyxEntry;
/** Show header for action */
showHeader?: boolean;
@@ -77,12 +78,12 @@ function ReportActionItemSingle({
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT;
- const actorAccountID = action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action.actorAccountID;
+ const actorAccountID = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action?.actorAccountID;
let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID);
const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {};
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '');
- const displayAllActors = useMemo(() => action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action.actionName, iouReport]);
+ const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action?.actionName, iouReport]);
const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors);
let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID);
@@ -90,7 +91,7 @@ function ReportActionItemSingle({
displayName = ReportUtils.getPolicyName(report);
actorHint = displayName;
avatarSource = ReportUtils.getWorkspaceAvatar(report);
- } else if (action.delegateAccountID && personalDetails[action.delegateAccountID]) {
+ } else if (action?.delegateAccountID && personalDetails[action?.delegateAccountID]) {
// We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their
// details. This will be improved upon when the Copilot feature is implemented.
const delegateDetails = personalDetails[action.delegateAccountID];
@@ -141,7 +142,7 @@ function ReportActionItemSingle({
text: displayName,
},
]
- : action.person;
+ : action?.person;
const reportID = report?.reportID;
const iouReportID = iouReport?.reportID;
@@ -155,14 +156,14 @@ function ReportActionItemSingle({
Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID));
return;
}
- showUserDetails(action.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID));
+ showUserDetails(action?.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID));
}
- }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]);
+ }, [isWorkspaceActor, reportID, actorAccountID, action?.delegateAccountID, iouReportID, displayAllActors]);
const shouldDisableDetailPage = useMemo(
() =>
CONST.RESTRICTED_ACCOUNT_IDS.includes(actorAccountID ?? 0) ||
- (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)),
+ (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action?.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)),
[action, isWorkspaceActor, actorAccountID],
);
@@ -189,7 +190,7 @@ function ReportActionItemSingle({
return (
@@ -237,13 +238,13 @@ function ReportActionItemSingle({
{personArray?.map((fragment, index) => (
))}
@@ -255,7 +256,7 @@ function ReportActionItemSingle({
>{`${status?.emojiCode}`}
)}
-
+
) : null}
{children}
diff --git a/src/pages/home/report/ReportActionItemThread.tsx b/src/pages/home/report/ReportActionItemThread.tsx
index f7c7e5fcf91d..c0dbe2a3825d 100644
--- a/src/pages/home/report/ReportActionItemThread.tsx
+++ b/src/pages/home/report/ReportActionItemThread.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import type {GestureResponderEvent} from 'react-native';
import {View} from 'react-native';
import MultipleAvatars from '@components/MultipleAvatars';
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
@@ -26,7 +27,7 @@ type ReportActionItemThreadProps = {
isHovered: boolean;
/** The function that should be called when the thread is LongPressed or right-clicked */
- onSecondaryInteraction: () => void;
+ onSecondaryInteraction: (event: GestureResponderEvent | MouseEvent) => void;
};
function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childReportID, isHovered, onSecondaryInteraction}: ReportActionItemThreadProps) {
diff --git a/src/pages/home/sidebar/AllSettingsScreen.tsx b/src/pages/home/sidebar/AllSettingsScreen.tsx
index a9e284329421..7151cc84e735 100644
--- a/src/pages/home/sidebar/AllSettingsScreen.tsx
+++ b/src/pages/home/sidebar/AllSettingsScreen.tsx
@@ -1,11 +1,11 @@
import React, {useMemo} from 'react';
-import {ScrollView} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Breadcrumbs from '@components/Breadcrumbs';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItemList from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
diff --git a/src/pages/iou/request/step/IOURequestStepTag.js b/src/pages/iou/request/step/IOURequestStepTag.js
index af1de64f8930..1b53dab12fa3 100644
--- a/src/pages/iou/request/step/IOURequestStepTag.js
+++ b/src/pages/iou/request/step/IOURequestStepTag.js
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
-import React from 'react';
+import React, {useMemo} from 'react';
import {withOnyx} from 'react-native-onyx';
import categoryPropTypes from '@components/categoryPropTypes';
import TagPicker from '@components/TagPicker';
@@ -11,7 +11,9 @@ import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import * as IOUUtils from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
+import * as ReportUtils from '@libs/ReportUtils';
import {canEditMoneyRequest} from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
@@ -78,10 +80,12 @@ function IOURequestStepTag({
const tag = TransactionUtils.getTag(transaction, tagIndex);
const isEditing = action === CONST.IOU.ACTION.EDIT;
const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT;
+ const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]);
const parentReportAction = parentReportActions[report.parentReportActionID];
+ const shouldShowTag = ReportUtils.isGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists));
// eslint-disable-next-line rulesdir/no-negated-variables
- const shouldShowNotFoundPage = isEditing && !canEditMoneyRequest(parentReportAction);
+ const shouldShowNotFoundPage = !shouldShowTag || (isEditing && !canEditMoneyRequest(parentReportAction));
const navigateBack = () => {
Navigation.goBack(backTo);
diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.tsx b/src/pages/iou/steps/MoneyRequestAmountForm.tsx
index cb1f73ae2207..55bf77e9ae88 100644
--- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx
+++ b/src/pages/iou/steps/MoneyRequestAmountForm.tsx
@@ -1,11 +1,12 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import type {ForwardedRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native';
import type {ValueOf} from 'type-fest';
import BigNumberPad from '@components/BigNumberPad';
import Button from '@components/Button';
import FormHelpMessage from '@components/FormHelpMessage';
+import ScrollView from '@components/ScrollView';
import TextInputWithCurrencySymbol from '@components/TextInputWithCurrencySymbol';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/settings/AboutPage/AboutPage.tsx b/src/pages/settings/AboutPage/AboutPage.tsx
index 3346b044ceca..0c087b2c93d6 100644
--- a/src/pages/settings/AboutPage/AboutPage.tsx
+++ b/src/pages/settings/AboutPage/AboutPage.tsx
@@ -1,5 +1,5 @@
import React, {useCallback, useMemo, useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, Text as RNText, StyleProp, ViewStyle} from 'react-native';
import DeviceInfo from 'react-native-device-info';
@@ -9,6 +9,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import LottieAnimations from '@components/LottieAnimations';
import MenuItemList from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
diff --git a/src/pages/settings/AppDownloadLinks.tsx b/src/pages/settings/AppDownloadLinks.tsx
index 352b3772923a..e4165178ff2f 100644
--- a/src/pages/settings/AppDownloadLinks.tsx
+++ b/src/pages/settings/AppDownloadLinks.tsx
@@ -1,11 +1,11 @@
import React, {useRef} from 'react';
-import {ScrollView} from 'react-native';
import type {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import type {MenuItemProps} from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
diff --git a/src/pages/settings/Preferences/PreferencesPage.js b/src/pages/settings/Preferences/PreferencesPage.js
index 0fd6121fe512..36a26ccffaa2 100755
--- a/src/pages/settings/Preferences/PreferencesPage.js
+++ b/src/pages/settings/Preferences/PreferencesPage.js
@@ -1,13 +1,14 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Illustrations from '@components/Icon/Illustrations';
import LottieAnimations from '@components/LottieAnimations';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Switch from '@components/Switch';
import Text from '@components/Text';
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
index 18589beb6353..2ba4fc33580b 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
+++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
@@ -1,7 +1,7 @@
import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import {InteractionManager, Keyboard, ScrollView, View} from 'react-native';
+import {InteractionManager, Keyboard, View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -13,6 +13,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
index 5d150e782c44..3851ef7153fb 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
+++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
@@ -1,7 +1,7 @@
import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
import React, {useCallback} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
@@ -11,6 +11,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js
index 968d9e502806..2fa133f41616 100755
--- a/src/pages/settings/Profile/ProfilePage.js
+++ b/src/pages/settings/Profile/ProfilePage.js
@@ -1,7 +1,7 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useEffect} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
@@ -10,6 +10,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import MenuItemGroup from '@components/MenuItemGroup';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
diff --git a/src/pages/settings/Report/ReportSettingsPage.tsx b/src/pages/settings/Report/ReportSettingsPage.tsx
index 54057f7c05bb..383cbbcb0833 100644
--- a/src/pages/settings/Report/ReportSettingsPage.tsx
+++ b/src/pages/settings/Report/ReportSettingsPage.tsx
@@ -1,12 +1,13 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import DisplayNames from '@components/DisplayNames';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx
index 8600c9e08471..01563e586792 100644
--- a/src/pages/settings/Security/SecuritySettingsPage.tsx
+++ b/src/pages/settings/Security/SecuritySettingsPage.tsx
@@ -1,11 +1,12 @@
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import LottieAnimations from '@components/LottieAnimations';
import MenuItemList from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx
index d6c7a1abcd4f..b4c1bc249c81 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx
@@ -1,5 +1,5 @@
import React, {useEffect, useState} from 'react';
-import {ActivityIndicator, ScrollView, View} from 'react-native';
+import {ActivityIndicator, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import FixedFooter from '@components/FixedFooter';
@@ -7,6 +7,7 @@ import FormHelpMessage from '@components/FormHelpMessage';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import PressableWithDelayToggle from '@components/Pressable/PressableWithDelayToggle';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx
index 59c145f9e348..ad9a4060af45 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx
@@ -1,8 +1,9 @@
import React, {useState} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import ConfirmModal from '@components/ConfirmModal';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx
index d9998c777f3b..58e7d98d69de 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx
@@ -1,5 +1,5 @@
import React, {useEffect, useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png';
import Button from '@components/Button';
@@ -8,6 +8,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import {useSession} from '@components/OnyxProvider';
import PressableWithDelayToggle from '@components/Pressable/PressableWithDelayToggle';
import QRCode from '@components/QRCode';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx
index a8b676f6c379..097b2cf28ed0 100644
--- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx
+++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useEffect, useMemo, useState} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
@@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/settings/Wallet/TransferBalancePage.tsx b/src/pages/settings/Wallet/TransferBalancePage.tsx
index 93ead17e9523..85b7bef0550c 100644
--- a/src/pages/settings/Wallet/TransferBalancePage.tsx
+++ b/src/pages/settings/Wallet/TransferBalancePage.tsx
@@ -1,5 +1,5 @@
import React, {useEffect} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
index b9f49049d51a..88236e06f9a9 100644
--- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
+++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
@@ -2,7 +2,7 @@ import _ from 'lodash';
import type {ForwardedRef, RefObject} from 'react';
import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
import type {GestureResponderEvent} from 'react-native';
-import {ActivityIndicator, Dimensions, ScrollView, View} from 'react-native';
+import {ActivityIndicator, Dimensions, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu';
import Button from '@components/Button';
@@ -18,6 +18,7 @@ import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Popover from '@components/Popover';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
@@ -74,7 +75,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi
});
const addPaymentMethodAnchorRef = useRef(null);
- const paymentMethodButtonRef = useRef(null);
+ const paymentMethodButtonRef = useRef(null);
const [anchorPosition, setAnchorPosition] = useState({
anchorPositionHorizontal: 0,
anchorPositionVertical: 0,
@@ -163,7 +164,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi
setShouldShowDefaultDeleteMenu(false);
return;
}
- paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLElement;
+ paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLDivElement;
// The delete/default menu
if (accountType) {
diff --git a/src/pages/signin/SAMLSignInPage/index.tsx b/src/pages/signin/SAMLSignInPage/index.tsx
index 701c2917bea6..1ff9d02672be 100644
--- a/src/pages/signin/SAMLSignInPage/index.tsx
+++ b/src/pages/signin/SAMLSignInPage/index.tsx
@@ -7,7 +7,7 @@ import type {SAMLSignInPageOnyxProps, SAMLSignInPageProps} from './types';
function SAMLSignInPage({credentials}: SAMLSignInPageProps) {
useEffect(() => {
- window.open(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`, '_self');
+ window.location.replace(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`);
}, [credentials?.login]);
return ;
diff --git a/src/pages/signin/SignInPageLayout/index.tsx b/src/pages/signin/SignInPageLayout/index.tsx
index b65da7eba0a5..3532c17181db 100644
--- a/src/pages/signin/SignInPageLayout/index.tsx
+++ b/src/pages/signin/SignInPageLayout/index.tsx
@@ -1,8 +1,11 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useEffect, useImperativeHandle, useMemo, useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {ScrollView as RNScrollView} from 'react-native';
+import {View} from 'react-native';
import SignInGradient from '@assets/images/home-fade-gradient.svg';
import ImageSVG from '@components/ImageSVG';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -38,7 +41,7 @@ function SignInPageLayout(
const StyleUtils = useStyleUtils();
const {preferredLocale} = useLocalize();
const {top: topInsets, bottom: bottomInsets} = useSafeAreaInsets();
- const scrollViewRef = useRef(null);
+ const scrollViewRef = useRef(null);
const prevPreferredLocale = usePrevious(preferredLocale);
const {windowHeight, isMediumScreenWidth, isLargeScreenWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js
index f77285190e62..352c08115114 100644
--- a/src/pages/tasks/NewTaskPage.js
+++ b/src/pages/tasks/NewTaskPage.js
@@ -1,7 +1,7 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useEffect, useMemo, useState} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
@@ -155,14 +156,7 @@ function NewTaskPage(props) {
Navigation.goBack(ROUTES.NEW_TASK_DETAILS);
}}
/>
-
+
;
/** Grab the Share destination of the Task */
- task: PropTypes.shape({
- /** Share destination of the Task */
- shareDestination: PropTypes.string,
-
- /** The task report if it's currently being edited */
- report: reportPropTypes,
- }),
-
- /** The policy of root parent report */
- rootParentReportPolicy: PropTypes.shape({
- /** The role of current user */
- role: PropTypes.string,
- }),
+ task: OnyxEntry;
};
-const defaultProps = {
- reports: {},
- task: {},
- rootParentReportPolicy: {},
+type UseOptions = {
+ reports: OnyxCollection;
};
-function useOptions({reports}) {
+type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps;
+
+function useOptions({reports}: UseOptions) {
const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
const betas = useBetas();
const [isLoading, setIsLoading] = useState(true);
@@ -78,7 +69,7 @@ function useOptions({reports}) {
);
const headerMessage = OptionsListUtils.getHeaderMessage(
- (recentReports.length || 0 + personalDetails.length || 0) !== 0 || currentUserOption,
+ (recentReports?.length || 0) + (personalDetails?.length || 0) !== 0 || Boolean(currentUserOption),
Boolean(userToInvite),
debouncedSearchValue,
);
@@ -99,20 +90,20 @@ function useOptions({reports}) {
return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue};
}
-function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
+function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalProps) {
const styles = useThemeStyles();
- const route = useRoute();
+ const route = useRoute>();
const {translate} = useLocalize();
const session = useSession();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
- const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports, task});
+ const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports});
const onChangeText = (newSearchTerm = '') => {
setSearchValue(newSearchTerm);
};
- const report = useMemo(() => {
- if (!route.params || !route.params.reportID) {
+ const report: OnyxEntry = useMemo(() => {
+ if (!route.params?.reportID) {
return null;
}
if (report && !ReportUtils.isTaskReport(report)) {
@@ -120,7 +111,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
Navigation.dismissModal(report.reportID);
});
}
- return reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`];
+ return reports?.[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID}`] ?? null;
}, [reports, route]);
const sections = useMemo(() => {
@@ -155,17 +146,29 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
if (userToInvite) {
sectionsList.push({
+ title: '',
data: [userToInvite],
shouldShow: true,
indexOffset,
});
}
- return sectionsList;
- }, [currentUserOption, personalDetails, recentReports, userToInvite, translate]);
+ return sectionsList.map((section) => ({
+ ...section,
+ data: section.data.map((option) => ({
+ ...option,
+ text: option.text ?? '',
+ alternateText: option.alternateText ?? undefined,
+ keyForList: option.keyForList ?? '',
+ isDisabled: option.isDisabled ?? undefined,
+ login: option.login ?? undefined,
+ shouldShowSubscript: option.shouldShowSubscript ?? undefined,
+ })),
+ }));
+ }, [currentUserOption, personalDetails, recentReports, translate, userToInvite]);
const selectReport = useCallback(
- (option) => {
+ (option: ListItem) => {
if (!option) {
return;
}
@@ -173,25 +176,35 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
// Check to see if we're editing a task and if so, update the assignee
if (report) {
if (option.accountID !== report.managerID) {
- const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, report.reportID, OptionsListUtils.isCurrentUser(option));
+ const assigneeChatReport = TaskActions.setAssigneeValue(
+ option?.login ?? '',
+ option?.accountID ?? -1,
+ report.reportID,
+ OptionsListUtils.isCurrentUser({...option, accountID: option?.accountID ?? -1, login: option?.login ?? ''}),
+ );
// Pass through the selected assignee
- Task.editTaskAssignee(report, session.accountID, option.login, option.accountID, assigneeChatReport);
+ TaskActions.editTaskAssignee(report, session?.accountID ?? 0, option?.login ?? '', option?.accountID, assigneeChatReport);
}
Navigation.dismissModal(report.reportID);
// If there's no report, we're creating a new task
} else if (option.accountID) {
- Task.setAssigneeValue(option.login, option.accountID, task.shareDestination, OptionsListUtils.isCurrentUser(option));
+ TaskActions.setAssigneeValue(
+ option?.login ?? '',
+ option.accountID,
+ task?.shareDestination ?? '',
+ OptionsListUtils.isCurrentUser({...option, accountID: option?.accountID ?? -1, login: option?.login ?? undefined}),
+ );
Navigation.goBack(ROUTES.NEW_TASK);
}
},
- [session.accountID, task.shareDestination, report],
+ [session?.accountID, task?.shareDestination, report],
);
- const handleBackButtonPress = useCallback(() => (lodashGet(route.params, 'reportID') ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]);
+ const handleBackButtonPress = useCallback(() => (route.params?.reportID ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]);
const isOpen = ReportUtils.isOpenTaskReport(report);
- const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID, lodashGet(rootParentReportPolicy, 'role', ''));
+ const canModifyTask = TaskActions.canModifyTask(report, currentUserPersonalDetails.accountID);
const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen);
return (
@@ -199,7 +212,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
includeSafeAreaPaddingBottom={false}
testID={TaskAssigneeSelectorModal.displayName}
>
- {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
+ {({didScreenTransitionEnd}) => (
@@ -225,26 +237,14 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
}
TaskAssigneeSelectorModal.displayName = 'TaskAssigneeSelectorModal';
-TaskAssigneeSelectorModal.propTypes = propTypes;
-TaskAssigneeSelectorModal.defaultProps = defaultProps;
-export default compose(
- withOnyx({
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- },
- task: {
- key: ONYXKEYS.TASK,
- },
- }),
- withOnyx({
- rootParentReportPolicy: {
- key: ({reports, route}) => {
- const report = reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID || '0'}`];
- const rootParentReport = ReportUtils.getRootParentReport(report);
- return `${ONYXKEYS.COLLECTION.POLICY}${rootParentReport ? rootParentReport.policyID : '0'}`;
- },
- selector: (policy) => lodashPick(policy, ['role']),
- },
- }),
-)(TaskAssigneeSelectorModal);
+const TaskAssigneeSelectorModalWithOnyx = withOnyx({
+ reports: {
+ key: ONYXKEYS.COLLECTION.REPORT,
+ },
+ task: {
+ key: ONYXKEYS.TASK,
+ },
+})(TaskAssigneeSelectorModal);
+
+export default withCurrentUserPersonalDetails(TaskAssigneeSelectorModalWithOnyx);
diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.tsx
similarity index 61%
rename from src/pages/tasks/TaskDescriptionPage.js
rename to src/pages/tasks/TaskDescriptionPage.tsx
index b8b48abd09ff..e08d6380bb18 100644
--- a/src/pages/tasks/TaskDescriptionPage.js
+++ b/src/pages/tasks/TaskDescriptionPage.tsx
@@ -2,53 +2,43 @@ import {useFocusEffect} from '@react-navigation/native';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import React, {useCallback, useRef} from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import withReportOrNotFound from '@pages/home/report/withReportOrNotFound';
-import reportPropTypes from '@pages/reportPropTypes';
+import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound';
import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/EditTaskForm';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
-const propTypes = {
- /** The report currently being looked at */
- report: reportPropTypes,
-
- /* Onyx Props */
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- report: {},
-};
+type TaskDescriptionPageProps = WithReportOrNotFoundProps & WithCurrentUserPersonalDetailsProps;
const parser = new ExpensiMark();
-function TaskDescriptionPage(props) {
+
+function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescriptionPageProps) {
const styles = useThemeStyles();
+ const {translate} = useLocalize();
- /**
- * @param {Object} values - form input values passed by the Form component
- * @returns {Boolean}
- */
- const validate = useCallback((values) => {
+ const validate = useCallback((values: FormOnyxValues): FormInputErrors => {
const errors = {};
- if (values.description.length > CONST.DESCRIPTION_LIMIT) {
+ if (values?.description && values.description?.length > CONST.DESCRIPTION_LIMIT) {
ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.DESCRIPTION_LIMIT}]);
}
@@ -56,30 +46,30 @@ function TaskDescriptionPage(props) {
}, []);
const submit = useCallback(
- (values) => {
- // props.report.description might contain CRLF from the server
- if (StringUtils.normalizeCRLF(values.description) !== StringUtils.normalizeCRLF(props.report.description)) {
+ (values: FormOnyxValues) => {
+ // report.description might contain CRLF from the server
+ if (StringUtils.normalizeCRLF(values.description) !== StringUtils.normalizeCRLF(report?.description) && !isEmptyObject(report)) {
// Set the description of the report in the store and then call EditTask API
// to update the description of the report on the server
- Task.editTask(props.report, {description: values.description});
+ Task.editTask(report, {description: values.description});
}
- Navigation.dismissModal(props.report.reportID);
+ Navigation.dismissModal(report?.reportID);
},
- [props],
+ [report],
);
- if (!ReportUtils.isTaskReport(props.report)) {
+ if (!ReportUtils.isTaskReport(report)) {
Navigation.isNavigationReady().then(() => {
- Navigation.dismissModal(props.report.reportID);
+ Navigation.dismissModal(report?.reportID);
});
}
- const inputRef = useRef(null);
- const focusTimeoutRef = useRef(null);
+ const inputRef = useRef(null);
+ const focusTimeoutRef = useRef(null);
- const isOpen = ReportUtils.isOpenTaskReport(props.report);
- const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID);
- const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen);
+ const isOpen = ReportUtils.isOpenTaskReport(report);
+ const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID);
+ const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen);
useFocusEffect(
useCallback(() => {
@@ -104,13 +94,13 @@ function TaskDescriptionPage(props) {
testID={TaskDescriptionPage.displayName}
>
-
+
@@ -119,14 +109,14 @@ function TaskDescriptionPage(props) {
role={CONST.ROLE.PRESENTATION}
inputID={INPUT_IDS.DESCRIPTION}
name={INPUT_IDS.DESCRIPTION}
- label={props.translate('newTaskPage.descriptionOptional')}
- accessibilityLabel={props.translate('newTaskPage.descriptionOptional')}
- defaultValue={parser.htmlToMarkdown((props.report && parser.replace(props.report.description)) || '')}
- ref={(el) => {
- if (!el) {
+ label={translate('newTaskPage.descriptionOptional')}
+ accessibilityLabel={translate('newTaskPage.descriptionOptional')}
+ defaultValue={parser.htmlToMarkdown((report && parser.replace(report?.description ?? '')) || '')}
+ ref={(element: AnimatedTextInputRef) => {
+ if (!element) {
return;
}
- inputRef.current = el;
+ inputRef.current = element;
updateMultilineInputRange(inputRef.current);
}}
autoGrowHeight
@@ -140,17 +130,8 @@ function TaskDescriptionPage(props) {
);
}
-TaskDescriptionPage.propTypes = propTypes;
-TaskDescriptionPage.defaultProps = defaultProps;
TaskDescriptionPage.displayName = 'TaskDescriptionPage';
-export default compose(
- withLocalize,
- withCurrentUserPersonalDetails,
- withReportOrNotFound(),
- withOnyx({
- report: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`,
- },
- }),
-)(TaskDescriptionPage);
+const ComponentWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(TaskDescriptionPage);
+
+export default withReportOrNotFound()(ComponentWithCurrentUserPersonalDetails);
diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx
similarity index 62%
rename from src/pages/tasks/TaskShareDestinationSelectorModal.js
rename to src/pages/tasks/TaskShareDestinationSelectorModal.tsx
index b62440b22967..5b56e58752ac 100644
--- a/src/pages/tasks/TaskShareDestinationSelectorModal.js
+++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx
@@ -1,9 +1,7 @@
-import keys from 'lodash/keys';
-import reduce from 'lodash/reduce';
-import PropTypes from 'prop-types';
import React, {useEffect, useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import {usePersonalDetails} from '@components/OnyxProvider';
import ScreenWrapper from '@components/ScreenWrapper';
@@ -13,51 +11,45 @@ import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as Report from '@libs/actions/Report';
+import * as ReportActions from '@libs/actions/Report';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
-import reportPropTypes from '@pages/reportPropTypes';
import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {Report} from '@src/types/onyx';
-const propTypes = {
- /** All reports shared with the user */
- reports: PropTypes.objectOf(reportPropTypes),
- /** Whether or not we are searching for reports on the server */
- isSearchingForReports: PropTypes.bool,
-};
+type TaskShareDestinationSelectorModalOnyxProps = {
+ reports: OnyxCollection;
-const defaultProps = {
- reports: {},
- isSearchingForReports: false,
+ isSearchingForReports: OnyxEntry;
};
-const selectReportHandler = (option) => {
- if (!option || !option.reportID) {
+type TaskShareDestinationSelectorModalProps = TaskShareDestinationSelectorModalOnyxProps;
+
+const selectReportHandler = (option: unknown) => {
+ const optionItem = option as ReportUtils.OptionData;
+
+ if (!optionItem || !optionItem?.reportID) {
return;
}
- Task.setShareDestinationValue(option.reportID);
+ Task.setShareDestinationValue(optionItem?.reportID);
Navigation.goBack(ROUTES.NEW_TASK);
};
-const reportFilter = (reports) =>
- reduce(
- keys(reports),
- (filtered, reportKey) => {
- const report = reports[reportKey];
- if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) {
- return {...filtered, [reportKey]: report};
- }
- return filtered;
- },
- {},
- );
+const reportFilter = (reports: OnyxCollection) =>
+ Object.keys(reports ?? {}).reduce((filtered, reportKey) => {
+ const report: OnyxEntry = reports?.[reportKey] ?? null;
+ if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) {
+ return {...filtered, [reportKey]: report};
+ }
+ return filtered;
+ }, {});
-function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) {
+function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: TaskShareDestinationSelectorModalProps) {
const styles = useThemeStyles();
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
const {translate} = useLocalize();
@@ -73,13 +65,29 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) {
const headerMessage = OptionsListUtils.getHeaderMessage(recentReports && recentReports.length !== 0, false, debouncedSearchValue);
- const sections = recentReports && recentReports.length > 0 ? [{data: recentReports, shouldShow: true}] : [];
+ const sections =
+ recentReports && recentReports.length > 0
+ ? [
+ {
+ data: recentReports.map((option) => ({
+ ...option,
+ text: option.text ?? '',
+ alternateText: option.alternateText ?? undefined,
+ keyForList: option.keyForList ?? '',
+ isDisabled: option.isDisabled ?? undefined,
+ login: option.login ?? undefined,
+ shouldShowSubscript: option.shouldShowSubscript ?? undefined,
+ })),
+ shouldShow: true,
+ },
+ ]
+ : [];
return {sections, headerMessage};
}, [personalDetails, reports, debouncedSearchValue]);
useEffect(() => {
- Report.searchInServer(debouncedSearchValue);
+ ReportActions.searchInServer(debouncedSearchValue);
}, [debouncedSearchValue]);
return (
@@ -87,7 +95,7 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) {
includeSafeAreaPaddingBottom={false}
testID="TaskShareDestinationSelectorModal"
>
- {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
+ {({didScreenTransitionEnd}) => (
<>
@@ -115,10 +122,8 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) {
}
TaskShareDestinationSelectorModal.displayName = 'TaskShareDestinationSelectorModal';
-TaskShareDestinationSelectorModal.propTypes = propTypes;
-TaskShareDestinationSelectorModal.defaultProps = defaultProps;
-export default withOnyx({
+export default withOnyx({
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
},
diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.tsx
similarity index 50%
rename from src/pages/tasks/TaskTitlePage.js
rename to src/pages/tasks/TaskTitlePage.tsx
index 370baab7cd89..009983beac3e 100644
--- a/src/pages/tasks/TaskTitlePage.js
+++ b/src/pages/tasks/TaskTitlePage.tsx
@@ -1,98 +1,85 @@
import React, {useCallback, useRef} from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
-import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import withReportOrNotFound from '@pages/home/report/withReportOrNotFound';
-import reportPropTypes from '@pages/reportPropTypes';
+import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound';
import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/EditTaskForm';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
-const propTypes = {
- /** The report currently being looked at */
- report: reportPropTypes,
+type TaskTitlePageProps = WithReportOrNotFoundProps & WithCurrentUserPersonalDetailsProps;
- /* Onyx Props */
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- report: {},
-};
-
-function TaskTitlePage(props) {
+function TaskTitlePage({report, currentUserPersonalDetails}: TaskTitlePageProps) {
const styles = useThemeStyles();
- /**
- * @param {Object} values
- * @param {String} values.title
- * @returns {Object} - An object containing the errors for each inputID
- */
- const validate = useCallback((values) => {
- const errors = {};
+ const {translate} = useLocalize();
+
+ const validate = useCallback(({title}: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
- if (_.isEmpty(values.title)) {
+ if (!title) {
errors.title = 'newTaskPage.pleaseEnterTaskName';
- } else if (values.title.length > CONST.TITLE_CHARACTER_LIMIT) {
- ErrorUtils.addErrorMessage(errors, 'title', ['common.error.characterLimitExceedCounter', {length: values.title.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
return errors;
}, []);
const submit = useCallback(
- (values) => {
- if (values.title !== props.report.reportName) {
+ (values: FormOnyxValues) => {
+ if (values.title !== report?.reportName && !isEmptyObject(report)) {
// Set the title of the report in the store and then call EditTask API
// to update the title of the report on the server
- Task.editTask(props.report, {title: values.title});
+ Task.editTask(report, {title: values.title});
}
- Navigation.dismissModal(props.report.reportID);
+ Navigation.dismissModal(report?.reportID);
},
- [props],
+ [report],
);
- if (!ReportUtils.isTaskReport(props.report)) {
+ if (!ReportUtils.isTaskReport(report)) {
Navigation.isNavigationReady().then(() => {
- Navigation.dismissModal(props.report.reportID);
+ Navigation.dismissModal(report?.reportID);
});
}
- const inputRef = useRef(null);
- const isOpen = ReportUtils.isOpenTaskReport(props.report);
- const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID);
- const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen);
+ const inputRef = useRef(null);
+ const isOpen = ReportUtils.isOpenTaskReport(report);
+ const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID);
+ const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen);
return (
inputRef.current && inputRef.current.focus()}
+ onEntryTransitionEnd={() => {
+ inputRef?.current?.focus();
+ }}
shouldEnableMaxHeight
testID={TaskTitlePage.displayName}
>
{({didScreenTransitionEnd}) => (
-
+
@@ -101,17 +88,17 @@ function TaskTitlePage(props) {
role={CONST.ROLE.PRESENTATION}
inputID={INPUT_IDS.TITLE}
name={INPUT_IDS.TITLE}
- label={props.translate('task.title')}
- accessibilityLabel={props.translate('task.title')}
- defaultValue={(props.report && props.report.reportName) || ''}
- ref={(el) => {
- if (!el) {
+ label={translate('task.title')}
+ accessibilityLabel={translate('task.title')}
+ defaultValue={report?.reportName ?? ''}
+ ref={(element: AnimatedTextInputRef) => {
+ if (!element) {
return;
}
if (!inputRef.current && didScreenTransitionEnd) {
- el.focus();
+ element.focus();
}
- inputRef.current = el;
+ inputRef.current = element;
}}
/>
@@ -122,17 +109,8 @@ function TaskTitlePage(props) {
);
}
-TaskTitlePage.propTypes = propTypes;
-TaskTitlePage.defaultProps = defaultProps;
TaskTitlePage.displayName = 'TaskTitlePage';
-export default compose(
- withLocalize,
- withCurrentUserPersonalDetails,
- withReportOrNotFound(),
- withOnyx({
- report: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`,
- },
- }),
-)(TaskTitlePage);
+const ComponentWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(TaskTitlePage);
+
+export default withReportOrNotFound()(ComponentWithCurrentUserPersonalDetails);
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index d2565022075a..c4f4d6399dbd 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect, useState} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
@@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useActiveRoute from '@hooks/useActiveRoute';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
diff --git a/src/pages/workspace/WorkspaceJoinUserPage.tsx b/src/pages/workspace/WorkspaceJoinUserPage.tsx
new file mode 100644
index 000000000000..8167e6fc1ebf
--- /dev/null
+++ b/src/pages/workspace/WorkspaceJoinUserPage.tsx
@@ -0,0 +1,80 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useEffect, useRef} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxCollection} from 'react-native-onyx';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useThemeStyles from '@hooks/useThemeStyles';
+import navigateAfterJoinRequest from '@libs/navigateAfterJoinRequest';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import Navigation from '@navigation/Navigation';
+import type {AuthScreensParamList} from '@navigation/types';
+import * as PolicyAction from '@userActions/Policy';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type {Policy} from '@src/types/onyx';
+
+type WorkspaceJoinUserPageOnyxProps = {
+ /** The list of this user's policies */
+ policies: OnyxCollection;
+};
+
+type WorkspaceJoinUserPageRoute = {route: StackScreenProps['route']};
+type WorkspaceJoinUserPageProps = WorkspaceJoinUserPageRoute & WorkspaceJoinUserPageOnyxProps;
+
+let isJoinLinkUsed = false;
+
+function WorkspaceJoinUserPage({route, policies}: WorkspaceJoinUserPageProps) {
+ const styles = useThemeStyles();
+ const policyID = route?.params?.policyID;
+ const inviterEmail = route?.params?.email;
+ const policy = ReportUtils.getPolicy(policyID);
+ const isUnmounted = useRef(false);
+
+ useEffect(() => {
+ if (!isJoinLinkUsed) {
+ return;
+ }
+ Navigation.goBack(undefined, false, true);
+ }, []);
+
+ useEffect(() => {
+ if (!policy || !policies || isUnmounted.current || isJoinLinkUsed) {
+ return;
+ }
+ const isPolicyMember = PolicyUtils.isPolicyMember(policyID, policies as Record);
+ if (isPolicyMember) {
+ Navigation.goBack(undefined, false, true);
+ return;
+ }
+ PolicyAction.inviteMemberToWorkspace(policyID, inviterEmail);
+ isJoinLinkUsed = true;
+ Navigation.isNavigationReady().then(() => {
+ if (isUnmounted.current) {
+ return;
+ }
+ navigateAfterJoinRequest();
+ });
+ }, [policy, policyID, policies, inviterEmail]);
+
+ useEffect(
+ () => () => {
+ isUnmounted.current = true;
+ },
+ [],
+ );
+
+ return (
+
+
+
+ );
+}
+
+WorkspaceJoinUserPage.displayName = 'WorkspaceJoinUserPage';
+export default withOnyx({
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ },
+})(WorkspaceJoinUserPage);
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index 100044344d4e..3970533870c1 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -254,6 +254,19 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
[selectedEmployees, addUser, removeUser],
);
+ /** Opens the member details page */
+ const openMemberDetails = useCallback(
+ (item: MemberOption) => {
+ if (!isPolicyAdmin || !PolicyUtils.isPaidGroupPolicy(policy)) {
+ Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID));
+ return;
+ }
+
+ Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(route.params.policyID, item.accountID, Navigation.getActiveRoute()));
+ },
+ [isPolicyAdmin, policy, route.params.policyID],
+ );
+
/**
* Dismisses the errors on one item
*/
@@ -465,7 +478,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
onPress={inviteUser}
text={translate('workspace.invite.member')}
icon={Expensicons.Plus}
- iconStyles={{transform: [{scale: 0.6}]}}
+ iconStyles={StyleUtils.getTransformScaleStyle(0.6)}
innerStyles={[isSmallScreenWidth && styles.alignItemsCenter]}
style={[isSmallScreenWidth && styles.flexGrow1]}
/>
@@ -525,13 +538,8 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
disableKeyboardShortcuts={removeMembersConfirmModalVisible}
headerMessage={getHeaderMessage()}
headerContent={getHeaderContent()}
- onSelectRow={(item) => {
- if (!isPolicyAdmin) {
- Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID));
- return;
- }
- toggleUser(item.accountID);
- }}
+ onSelectRow={openMemberDetails}
+ onCheckboxPress={(item) => toggleUser(item.accountID)}
onSelectAll={() => toggleAllUsers(data)}
onDismissError={dismissError}
showLoadingPlaceholder={!isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policyMembers))}
diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx
index 796f32c343f2..9d90557b1d37 100644
--- a/src/pages/workspace/WorkspaceProfilePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -1,15 +1,17 @@
-import React, {useCallback} from 'react';
+import React, {useCallback, useState} from 'react';
import type {ImageStyle, StyleProp} from 'react-native';
-import {Image, ScrollView, StyleSheet, View} from 'react-native';
+import {Image, StyleSheet, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Avatar from '@components/Avatar';
import AvatarWithImagePicker from '@components/AvatarWithImagePicker';
import Button from '@components/Button';
+import ConfirmModal from '@components/ConfirmModal';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
@@ -74,6 +76,19 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi
[policy?.avatar, policyName, styles.alignSelfCenter, styles.avatarXLarge],
);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+
+ const confirmDeleteAndHideModal = useCallback(() => {
+ if (!policy?.id || !policyName) {
+ return;
+ }
+
+ Policy.deleteWorkspace(policy?.id, policyName);
+
+ PolicyUtils.goBackFromInvalidPolicy();
+
+ setIsDeleteModalOpen(false);
+ }, [policy?.id, policyName]);
return (
{!readOnly && (
-
+
+ setIsDeleteModalOpen(true)}
+ medium
+ />
)}
+ setIsDeleteModalOpen(false)}
+ prompt={translate('workspace.common.deleteConfirmation')}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
+ />
)}
diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx
index dd03436042ca..56fd8b783742 100644
--- a/src/pages/workspace/WorkspaceProfileSharePage.tsx
+++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx
@@ -1,5 +1,5 @@
import React, {useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {ImageSourcePropType} from 'react-native';
import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png';
import ContextMenuItem from '@components/ContextMenuItem';
@@ -9,6 +9,7 @@ import MenuItem from '@components/MenuItem';
import QRShareWithDownload from '@components/QRShare/QRShareWithDownload';
import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx
index d1edf7f2f783..948cd2fd83c4 100755
--- a/src/pages/workspace/WorkspacesListPage.tsx
+++ b/src/pages/workspace/WorkspacesListPage.tsx
@@ -1,5 +1,5 @@
import React, {useCallback, useMemo, useState} from 'react';
-import {FlatList, ScrollView, View} from 'react-native';
+import {FlatList, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
@@ -17,6 +17,7 @@ import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import {PressableWithoutFeedback} from '@components/Pressable';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -51,6 +52,7 @@ type WorkspaceItem = Required> &
policyID?: string;
adminRoom?: string | null;
announceRoom?: string | null;
+ isJoinRequestPending?: boolean;
};
// eslint-disable-next-line react/no-unused-prop-types
@@ -115,11 +117,12 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
- const {isSmallScreenWidth} = useWindowDimensions();
+ const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [policyIDToDelete, setPolicyIDToDelete] = useState();
const [policyNameToDelete, setPolicyNameToDelete] = useState();
+ const isLessThanMediumScreen = isMediumScreenWidth || isSmallScreenWidth;
const confirmDeleteAndHideModal = () => {
if (!policyIDToDelete || !policyNameToDelete) {
@@ -191,8 +194,9 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
workspaceIcon={item.icon}
ownerAccountID={item.ownerAccountID}
workspaceType={item.type}
+ isJoinRequestPending={item?.isJoinRequestPending}
rowStyles={hovered && styles.hoveredComponentBG}
- layoutWidth={isSmallScreenWidth ? CONST.LAYOUT_WIDTH.NARROW : CONST.LAYOUT_WIDTH.WIDE}
+ layoutWidth={isLessThanMediumScreen ? CONST.LAYOUT_WIDTH.NARROW : CONST.LAYOUT_WIDTH.WIDE}
brickRoadIndicator={item.brickRoadIndicator}
shouldDisableThreeDotsMenu={item.disabled}
/>
@@ -201,11 +205,11 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
);
},
- [isSmallScreenWidth, styles.mb3, styles.mh5, styles.ph5, styles.hoveredComponentBG, translate],
+ [isLessThanMediumScreen, styles.mb3, styles.mh5, styles.ph5, styles.hoveredComponentBG, translate],
);
const listHeaderComponent = useCallback(() => {
- if (isSmallScreenWidth) {
+ if (isLessThanMediumScreen) {
return ;
}
@@ -238,7 +242,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
);
- }, [isSmallScreenWidth, styles, translate]);
+ }, [isLessThanMediumScreen, styles, translate]);
const policyRooms = useMemo(() => {
if (!reports || isEmptyObject(reports)) {
@@ -283,8 +287,28 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
return Object.values(policies)
.filter((policy): policy is PolicyType => PolicyUtils.shouldShowPolicy(policy, !!isOffline))
- .map(
- (policy): WorkspaceItem => ({
+ .map((policy): WorkspaceItem => {
+ if (policy?.isJoinRequestPending && policy?.policyDetailsForNonMembers) {
+ const policyInfo = Object.values(policy.policyDetailsForNonMembers)[0];
+ const id = Object.keys(policy.policyDetailsForNonMembers)[0];
+ return {
+ title: policyInfo.name,
+ icon: policyInfo.avatar ? policyInfo.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy.name),
+ disabled: true,
+ ownerAccountID: policyInfo.ownerAccountID,
+ type: policyInfo.type,
+ iconType: policyInfo.avatar ? CONST.ICON_TYPE_AVATAR : CONST.ICON_TYPE_ICON,
+ iconFill: theme.textLight,
+ fallbackIcon: Expensicons.FallbackWorkspaceAvatar,
+ policyID: id,
+ role: CONST.POLICY.ROLE.USER,
+ errors: null,
+ action: () => null,
+ dismissError: () => null,
+ isJoinRequestPending: true,
+ };
+ }
+ return {
title: policy.name,
icon: policy.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy.name),
action: () => Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)),
@@ -307,8 +331,8 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
ownerAccountID: policy.ownerAccountID,
role: policy.role,
type: policy.type,
- }),
- )
+ };
+ })
.sort((a, b) => localeCompare(a.title, b.title));
}, [reimbursementAccount?.errors, policies, isOffline, theme.textLight, allPolicyMembers, policyRooms]);
@@ -323,7 +347,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
>
Navigation.goBack()}
>
-
+
Navigation.goBack()}
>
-
+
{title}
- {isNarrow && (
- <>
-
-
+
+
+ >
+ ) : (
+
- >
- )}
+ ))}
{!!ownerDetails && (
@@ -183,14 +196,14 @@ function WorkspacesListRow({
>
)}
-
+
-
+
+ {isJoinRequestPending && !isNarrow && (
+
+
+
+ )}
- {isWide && (
+ {isWide && !isJoinRequestPending && (
<>
diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
new file mode 100644
index 000000000000..d44ff8baa08b
--- /dev/null
+++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
@@ -0,0 +1,152 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback} from 'react';
+import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import Avatar from '@components/Avatar';
+import Button from '@components/Button';
+import ConfirmModal from '@components/ConfirmModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItem from '@components/MenuItem';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as UserUtils from '@libs/UserUtils';
+import Navigation from '@navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Route} from '@src/ROUTES';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx';
+
+type WorkspacePolicyOnyxProps = {
+ /** Personal details of all users */
+ personalDetails: OnyxEntry;
+};
+
+type WorkspaceMemberDetailsPageProps = WithPolicyAndFullscreenLoadingProps & WorkspacePolicyOnyxProps & StackScreenProps;
+
+function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, route}: WorkspaceMemberDetailsPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const StyleUtils = useStyleUtils();
+
+ const [isRemoveMemberConfirmModalVisible, setIsRemoveMemberConfirmModalVisible] = React.useState(false);
+
+ const accountID = Number(route.params.accountID);
+ const policyID = route.params.policyID;
+ const backTo = route.params.backTo ?? ('' as Route);
+
+ const member = policyMembers?.[accountID];
+ const details = personalDetails?.[accountID] ?? ({} as PersonalDetails);
+ const avatar = details.avatar ?? UserUtils.getDefaultAvatar();
+ const fallbackIcon = details.fallbackIcon ?? '';
+ const displayName = details.displayName ?? '';
+
+ const askForConfirmationToRemove = () => {
+ setIsRemoveMemberConfirmModalVisible(true);
+ };
+
+ const removeUser = useCallback(() => {
+ Policy.removeMembers([accountID], route.params.policyID);
+ setIsRemoveMemberConfirmModalVisible(false);
+ Navigation.goBack(backTo);
+ }, [accountID, backTo, route.params.policyID]);
+
+ const navigateToProfile = useCallback(() => {
+ Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute()));
+ }, [accountID]);
+
+ const openRoleSelectionModal = useCallback(() => {
+ Navigation.navigate(ROUTES.WORKSPACE_MEMBER_ROLE_SELECTION.getRoute(route.params.policyID, accountID, Navigation.getActiveRoute()));
+ }, [accountID, route.params.policyID]);
+
+ return (
+
+
+
+ Navigation.goBack(backTo)}
+ />
+
+
+
+
+
+ {Boolean(details.displayName ?? '') && (
+
+ {displayName}
+
+ )}
+
+ setIsRemoveMemberConfirmModalVisible(false)}
+ prompt={translate('workspace.people.removeMemberPrompt', {memberName: displayName})}
+ confirmText={translate('common.remove')}
+ cancelText={translate('common.cancel')}
+ />
+
+
+
+
+
+
+
+
+
+ );
+}
+
+WorkspaceMemberDetailsPage.displayName = 'WorkspaceMemberDetailsPage';
+
+export default withPolicyAndFullscreenLoading(
+ withOnyx({
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ })(WorkspaceMemberDetailsPage),
+);
diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage.tsx
new file mode 100644
index 000000000000..ea8c6aaf144a
--- /dev/null
+++ b/src/pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage.tsx
@@ -0,0 +1,87 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React from 'react';
+import {View} from 'react-native';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
+import type {Route} from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+
+type WorkspaceMemberDetailsPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
+
+type ListItemType = {
+ value: typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.USER;
+ text: string;
+ isSelected: boolean;
+ keyForList: typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.USER;
+};
+
+function WorkspaceMemberDetailsRoleSelectionPage({policyMembers, route}: WorkspaceMemberDetailsPageProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const accountID = Number(route.params.accountID) ?? 0;
+ const policyID = route.params.policyID;
+ const backTo = route.params.backTo ?? ('' as Route);
+ const member = policyMembers?.[accountID];
+
+ const items: ListItemType[] = [
+ {
+ value: CONST.POLICY.ROLE.ADMIN,
+ text: translate('common.admin'),
+ isSelected: member?.role === CONST.POLICY.ROLE.ADMIN,
+ keyForList: CONST.POLICY.ROLE.ADMIN,
+ },
+ {
+ value: CONST.POLICY.ROLE.USER,
+ text: translate('common.member'),
+ isSelected: member?.role === CONST.POLICY.ROLE.USER,
+ keyForList: CONST.POLICY.ROLE.USER,
+ },
+ ];
+
+ const changeRole = ({value}: ListItemType) => {
+ if (!member) {
+ return;
+ }
+
+ Policy.updateWorkspaceMembersRole(route.params.policyID, [accountID], value);
+ Navigation.goBack(backTo);
+ };
+
+ return (
+
+
+
+ Navigation.goBack(backTo)}
+ />
+
+ item.isSelected)?.keyForList}
+ />
+
+
+
+
+ );
+}
+
+WorkspaceMemberDetailsRoleSelectionPage.displayName = 'WorkspaceMemberDetailsRoleSelectionPage';
+
+export default withPolicyAndFullscreenLoading(WorkspaceMemberDetailsRoleSelectionPage);
diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
index a23073bbc884..bc728992b811 100644
--- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
+++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
@@ -1,11 +1,12 @@
import React, {useEffect, useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {withNetwork} from '@components/OnyxProvider';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
@@ -110,14 +111,7 @@ function WorkspaceRateAndUnitPage(props: WorkspaceRateAndUnitPageProps) {
shouldShowBackButton
>
{() => (
-
+
;
type OriginalMessageApproved = {
@@ -219,6 +220,17 @@ type OriginalMessagePolicyChangeLog = {
originalMessage: ChangeLog;
};
+type OriginalMessageJoinPolicyChangeLog = {
+ actionName: typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLEJOINREQUEST;
+ originalMessage: {
+ choice: string;
+ email: string;
+ inviterEmail: string;
+ lastModified: string;
+ policyID: string;
+ };
+};
+
type OriginalMessageRoomChangeLog = {
actionName: ValueOf;
originalMessage: ChangeLog;
@@ -258,7 +270,9 @@ type OriginalMessageModifiedExpense = {
type OriginalMessageReimbursementQueued = {
actionName: typeof CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED;
- originalMessage: unknown;
+ originalMessage: {
+ paymentType: DeepValueOf;
+ };
};
type OriginalMessageReimbursementDequeued = {
@@ -294,6 +308,7 @@ type OriginalMessage =
| OriginalMessageRoomChangeLog
| OriginalMessagePolicyChangeLog
| OriginalMessagePolicyTask
+ | OriginalMessageJoinPolicyChangeLog
| OriginalMessageModifiedExpense
| OriginalMessageReimbursementQueued
| OriginalMessageReimbursementDequeued
@@ -315,6 +330,7 @@ export type {
OriginalMessageCreated,
OriginalMessageRenamed,
OriginalMessageAddComment,
+ OriginalMessageJoinPolicyChangeLog,
OriginalMessageActionableMentionWhisper,
OriginalMessageChronosOOOList,
OriginalMessageSource,
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index b005a9d2756f..61b63106ad8a 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -82,6 +82,20 @@ type Connection = {
type AutoReportingOffset = number | ValueOf;
+type PendingJoinRequestPolicy = {
+ isJoinRequestPending: boolean;
+ policyDetailsForNonMembers: Record<
+ string,
+ OnyxCommon.OnyxValueWithOfflineFeedback<{
+ name: string;
+ ownerAccountID: number;
+ ownerEmail: string;
+ type: ValueOf;
+ avatar?: string;
+ }>
+ >;
+};
+
type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
{
/** The ID of the policy */
@@ -244,10 +258,10 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
/** Whether the Connections feature is enabled */
areConnectionsEnabled?: boolean;
- },
+ } & Partial,
'generalSettings' | 'addWorkspaceRoom'
>;
export default Policy;
-export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault};
+export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault, PendingJoinRequestPolicy};
diff --git a/src/types/onyx/PolicyJoinMember.ts b/src/types/onyx/PolicyJoinMember.ts
new file mode 100644
index 000000000000..263be6c21dc2
--- /dev/null
+++ b/src/types/onyx/PolicyJoinMember.ts
@@ -0,0 +1,17 @@
+import type * as OnyxCommon from './OnyxCommon';
+
+type PolicyJoinMember = {
+ /** Role of the user in the policy */
+ policyID?: string;
+
+ /** Email of the user inviting the new member */
+ inviterEmail?: string;
+
+ /**
+ * Errors from api calls on the specific user
+ * {: 'error message', : 'error message 2'}
+ */
+ errors?: OnyxCommon.Errors;
+};
+
+export default PolicyJoinMember;
diff --git a/src/types/onyx/Task.ts b/src/types/onyx/Task.ts
index 2aec786d77ad..50a871b7ea07 100644
--- a/src/types/onyx/Task.ts
+++ b/src/types/onyx/Task.ts
@@ -2,7 +2,7 @@ import type Report from './Report';
type Task = {
/** Title of the Task */
- title: string;
+ title?: string;
/** Description of the Task */
description?: string;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 321a7a0903d4..d12b0a22bba2 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -27,6 +27,7 @@ import type MapboxAccessToken from './MapboxAccessToken';
import type Modal from './Modal';
import type Network from './Network';
import type {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer';
+import type {DecisionName, OriginalMessageIOU} from './OriginalMessage';
import type PersonalBankAccount from './PersonalBankAccount';
import type {PersonalDetailsList} from './PersonalDetails';
import type PersonalDetails from './PersonalDetails';
@@ -34,6 +35,7 @@ import type PlaidData from './PlaidData';
import type Policy from './Policy';
import type {TaxRate, TaxRates, TaxRatesWithDefault} from './Policy';
import type {PolicyCategories, PolicyCategory} from './PolicyCategory';
+import type PolicyJoinMember from './PolicyJoinMember';
import type {PolicyMembers} from './PolicyMember';
import type PolicyMember from './PolicyMember';
import type {PolicyReportField, PolicyReportFields} from './PolicyReportField';
@@ -45,7 +47,7 @@ import type RecentlyUsedTags from './RecentlyUsedTags';
import type RecentWaypoint from './RecentWaypoint';
import type ReimbursementAccount from './ReimbursementAccount';
import type Report from './Report';
-import type {ReportActions} from './ReportAction';
+import type {ReportActionBase, ReportActions} from './ReportAction';
import type ReportAction from './ReportAction';
import type ReportActionReactions from './ReportActionReactions';
import type ReportActionsDraft from './ReportActionsDraft';
@@ -152,8 +154,12 @@ export type {
PolicyReportField,
PolicyReportFields,
RecentlyUsedReportFields,
+ DecisionName,
+ OriginalMessageIOU,
+ ReportActionBase,
LastPaymentMethod,
LastSelectedDistanceRates,
InvitedEmailsToAccountIDs,
Log,
+ PolicyJoinMember,
};
diff --git a/tests/e2e/utils/installApp.ts b/tests/e2e/utils/installApp.ts
index b443344e6f02..dc6a9d64053f 100644
--- a/tests/e2e/utils/installApp.ts
+++ b/tests/e2e/utils/installApp.ts
@@ -17,7 +17,7 @@ export default function (packageName: string, path: string, platform = 'android'
execAsync(`adb uninstall ${packageName}`)
.catch((error: ExecException) => {
// Ignore errors
- Logger.warn('Failed to uninstall app:', error);
+ Logger.warn('Failed to uninstall app:', error.message);
})
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.finally(() => execAsync(`adb install ${path}`))
diff --git a/tests/e2e/utils/logger.js b/tests/e2e/utils/logger.ts
similarity index 66%
rename from tests/e2e/utils/logger.js
rename to tests/e2e/utils/logger.ts
index e120c90482b5..ebe8fc05e66a 100644
--- a/tests/e2e/utils/logger.js
+++ b/tests/e2e/utils/logger.ts
@@ -1,7 +1,6 @@
/* eslint-disable import/no-import-module-exports */
import fs from 'fs';
import path from 'path';
-import _ from 'underscore';
import CONFIG from '../config';
const COLOR_DIM = '\x1b[2m';
@@ -12,7 +11,7 @@ const COLOR_GREEN = '\x1b[32m';
const getDateString = () => `[${Date()}] `;
-const writeToLogFile = (...args) => {
+const writeToLogFile = (...args: string[]) => {
if (!fs.existsSync(CONFIG.LOG_FILE)) {
// Check that the directory exists
const logDir = path.dirname(CONFIG.LOG_FILE);
@@ -24,45 +23,46 @@ const writeToLogFile = (...args) => {
}
fs.appendFileSync(
CONFIG.LOG_FILE,
- `${_.map(args, (arg) => {
- if (typeof arg === 'string') {
- // Remove color codes from arg, because they are not supported in log files
- // eslint-disable-next-line no-control-regex
- return arg.replace(/\x1b\[\d+m/g, '');
- }
- return arg;
- })
+ `${args
+ .map((arg) => {
+ if (typeof arg === 'string') {
+ // Remove color codes from arg, because they are not supported in log files
+ // eslint-disable-next-line no-control-regex
+ return arg.replace(/\x1b\[\d+m/g, '');
+ }
+ return arg;
+ })
.join(' ')
.trim()}\n`,
);
};
-const log = (...args) => {
+const log = (...args: string[]) => {
const argsWithTime = [getDateString(), ...args];
console.debug(...argsWithTime);
writeToLogFile(...argsWithTime);
};
-const info = (...args) => {
+const info = (...args: string[]) => {
log('▶️', ...args);
};
-const success = (...args) => {
+const success = (...args: string[]) => {
const lines = ['✅', COLOR_GREEN, ...args, COLOR_RESET];
log(...lines);
};
-const warn = (...args) => {
+const warn = (...args: string[]) => {
const lines = ['⚠️', COLOR_YELLOW, ...args, COLOR_RESET];
log(...lines);
};
-const note = (...args) => {
+const note = (...args: string[]) => {
const lines = [COLOR_DIM, ...args, COLOR_RESET];
log(...lines);
};
-const error = (...args) => {
+const error = (...args: string[]) => {
const lines = ['🔴', COLOR_RED, ...args, COLOR_RESET];
log(...lines);
};
diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js
index 6051f04f570e..9c2ff134f21a 100644
--- a/tests/ui/UnreadIndicatorsTest.js
+++ b/tests/ui/UnreadIndicatorsTest.js
@@ -21,6 +21,7 @@ import * as Pusher from '../../src/libs/Pusher/pusher';
import PusherConnectionManager from '../../src/libs/PusherConnectionManager';
import ONYXKEYS from '../../src/ONYXKEYS';
import appSetup from '../../src/setup';
+import PusherHelper from '../utils/PusherHelper';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
@@ -276,6 +277,9 @@ describe('Unread Indicators', () => {
afterEach(() => {
jest.clearAllMocks();
Onyx.clear();
+
+ // Unsubscribe to pusher channels
+ PusherHelper.teardown();
});
it('Display bold in the LHN for unread chat and new line indicator above the chat message when we navigate to it', () =>
@@ -375,54 +379,48 @@ describe('Unread Indicators', () => {
const NEW_REPORT_FIST_MESSAGE_CREATED_DATE = addSeconds(NEW_REPORT_CREATED_DATE, 1);
const createdReportActionID = NumberUtils.rand64();
const commentReportActionID = NumberUtils.rand64();
- const channel = Pusher.getChannel(`${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${USER_A_ACCOUNT_ID}${CONFIG.PUSHER.SUFFIX}`);
- channel.emit(Pusher.TYPE.MULTIPLE_EVENTS, [
+ PusherHelper.emitOnyxUpdate([
{
- eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE,
- data: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`,
- value: {
- reportID: NEW_REPORT_ID,
- reportName: CONST.REPORT.DEFAULT_REPORT_NAME,
- lastReadTime: '',
- lastVisibleActionCreated: DateUtils.getDBTime(utcToZonedTime(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, 'UTC').valueOf()),
- lastMessageText: 'Comment 1',
- lastActorAccountID: USER_C_ACCOUNT_ID,
- participantAccountIDs: [USER_C_ACCOUNT_ID],
- type: CONST.REPORT.TYPE.CHAT,
- },
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`,
- value: {
- [createdReportActionID]: {
- actionName: CONST.REPORT.ACTIONS.TYPE.CREATED,
- automatic: false,
- created: format(NEW_REPORT_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING),
- reportActionID: createdReportActionID,
- },
- [commentReportActionID]: {
- actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT,
- actorAccountID: USER_C_ACCOUNT_ID,
- person: [{type: 'TEXT', style: 'strong', text: 'User C'}],
- created: format(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING),
- message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}],
- reportActionID: commentReportActionID,
- },
- },
- shouldNotify: true,
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`,
+ value: {
+ reportID: NEW_REPORT_ID,
+ reportName: CONST.REPORT.DEFAULT_REPORT_NAME,
+ lastReadTime: '',
+ lastVisibleActionCreated: DateUtils.getDBTime(utcToZonedTime(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, 'UTC').valueOf()),
+ lastMessageText: 'Comment 1',
+ lastActorAccountID: USER_C_ACCOUNT_ID,
+ participantAccountIDs: [USER_C_ACCOUNT_ID],
+ type: CONST.REPORT.TYPE.CHAT,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`,
+ value: {
+ [createdReportActionID]: {
+ actionName: CONST.REPORT.ACTIONS.TYPE.CREATED,
+ automatic: false,
+ created: format(NEW_REPORT_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING),
+ reportActionID: createdReportActionID,
},
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- value: {
- [USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'),
- },
+ [commentReportActionID]: {
+ actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT,
+ actorAccountID: USER_C_ACCOUNT_ID,
+ person: [{type: 'TEXT', style: 'strong', text: 'User C'}],
+ created: format(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING),
+ message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}],
+ reportActionID: commentReportActionID,
},
- ],
+ },
+ shouldNotify: true,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ [USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'),
+ },
},
]);
return waitForBatchedUpdates();
diff --git a/tests/unit/GithubUtilsTest.ts b/tests/unit/GithubUtilsTest.ts
index 794139286527..24e56402f1ea 100644
--- a/tests/unit/GithubUtilsTest.ts
+++ b/tests/unit/GithubUtilsTest.ts
@@ -376,6 +376,7 @@ describe('GithubUtils', () => {
login: 'hubot',
},
],
+ merged_by: {login: 'octocat'},
},
{
number: 7,
@@ -392,14 +393,8 @@ describe('GithubUtils', () => {
color: 'f29513',
},
],
- assignees: [
- {
- login: 'octocat',
- },
- {
- login: 'hubot',
- },
- ],
+ assignees: [],
+ merged_by: {login: 'hubot'},
},
];
const mockGithub = jest.fn(() => ({
@@ -446,7 +441,8 @@ describe('GithubUtils', () => {
const internalQAHeader = '\r\n\r\n**Internal QA:**';
const lineBreak = '\r\n';
const lineBreakDouble = '\r\n\r\n';
- const assignOctocatHubot = ' - @octocat @hubot';
+ const assignOctocat = ' - @octocat';
+ const assignHubot = ' - @hubot';
const deployerVerificationsHeader = '\r\n**Deployer verifications:**';
// eslint-disable-next-line max-len
const timingDashboardVerification =
@@ -468,8 +464,8 @@ describe('GithubUtils', () => {
`${lineBreak}`;
test('Test no verified PRs', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList).then((issue) => {
+ expect(issue.issueBody).toBe(
`${baseExpectedOutput}` +
`${openCheckbox}${basePRList[2]}` +
`${lineBreak}${openCheckbox}${basePRList[0]}` +
@@ -482,12 +478,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test some verified PRs', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList, [basePRList[0]]).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList, [basePRList[0]]).then((issue) => {
+ expect(issue.issueBody).toBe(
`${baseExpectedOutput}` +
`${openCheckbox}${basePRList[2]}` +
`${lineBreak}${closedCheckbox}${basePRList[0]}` +
@@ -500,12 +497,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test all verified PRs', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList).then((issue) => {
+ expect(issue.issueBody).toBe(
`${allVerifiedExpectedOutput}` +
`${lineBreak}${deployerVerificationsHeader}` +
`${lineBreak}${openCheckbox}${timingDashboardVerification}` +
@@ -513,12 +511,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test no resolved deploy blockers', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList).then((issue) => {
+ expect(issue.issueBody).toBe(
`${allVerifiedExpectedOutput}` +
`${lineBreak}${deployBlockerHeader}` +
`${lineBreak}${openCheckbox}${baseDeployBlockerList[0]}` +
@@ -529,12 +528,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}${lineBreak}` +
`${lineBreak}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test some resolved deploy blockers', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList, [baseDeployBlockerList[0]]).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList, [baseDeployBlockerList[0]]).then((issue) => {
+ expect(issue.issueBody).toBe(
`${allVerifiedExpectedOutput}` +
`${lineBreak}${deployBlockerHeader}` +
`${lineBreak}${closedCheckbox}${baseDeployBlockerList[0]}` +
@@ -545,12 +545,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test all resolved deploy blockers', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList, baseDeployBlockerList).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList, baseDeployBlockerList).then((issue) => {
+ expect(issue.issueBody).toBe(
`${baseExpectedOutput}` +
`${closedCheckbox}${basePRList[2]}` +
`${lineBreak}${closedCheckbox}${basePRList[0]}` +
@@ -566,12 +567,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test internalQA PRs', () => {
- githubUtils.generateStagingDeployCashBody(tag, [...basePRList, ...internalQAPRList]).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, [...basePRList, ...internalQAPRList]).then((issue) => {
+ expect(issue.issueBody).toBe(
`${baseExpectedOutput}` +
`${openCheckbox}${basePRList[2]}` +
`${lineBreak}${openCheckbox}${basePRList[0]}` +
@@ -579,20 +581,21 @@ describe('GithubUtils', () => {
`${lineBreak}${closedCheckbox}${basePRList[4]}` +
`${lineBreak}${closedCheckbox}${basePRList[5]}` +
`${lineBreak}${internalQAHeader}` +
- `${lineBreak}${openCheckbox}${internalQAPRList[0]}${assignOctocatHubot}` +
- `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignOctocatHubot}` +
+ `${lineBreak}${openCheckbox}${internalQAPRList[0]}${assignOctocat}` +
+ `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignHubot}` +
`${lineBreakDouble}${deployerVerificationsHeader}` +
`${lineBreak}${openCheckbox}${timingDashboardVerification}` +
`${lineBreak}${openCheckbox}${firebaseVerification}` +
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual(['octocat', 'hubot']);
});
});
test('Test some verified internalQA PRs', () => {
- githubUtils.generateStagingDeployCashBody(tag, [...basePRList, ...internalQAPRList], [], [], [], [internalQAPRList[0]]).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, [...basePRList, ...internalQAPRList], [], [], [], [internalQAPRList[0]]).then((issue) => {
+ expect(issue.issueBody).toBe(
`${baseExpectedOutput}` +
`${openCheckbox}${basePRList[2]}` +
`${lineBreak}${openCheckbox}${basePRList[0]}` +
@@ -600,14 +603,15 @@ describe('GithubUtils', () => {
`${lineBreak}${closedCheckbox}${basePRList[4]}` +
`${lineBreak}${closedCheckbox}${basePRList[5]}` +
`${lineBreak}${internalQAHeader}` +
- `${lineBreak}${closedCheckbox}${internalQAPRList[0]}${assignOctocatHubot}` +
- `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignOctocatHubot}` +
+ `${lineBreak}${closedCheckbox}${internalQAPRList[0]}${assignOctocat}` +
+ `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignHubot}` +
`${lineBreakDouble}${deployerVerificationsHeader}` +
`${lineBreak}${openCheckbox}${timingDashboardVerification}` +
`${lineBreak}${openCheckbox}${firebaseVerification}` +
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual(['octocat', 'hubot']);
});
});
});
diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts
index f28e2f2c3f14..14c749fc92de 100644
--- a/tests/unit/ReportActionsUtilsTest.ts
+++ b/tests/unit/ReportActionsUtilsTest.ts
@@ -285,8 +285,7 @@ describe('ReportActionsUtils', () => {
reportActionID: '1661970171066216',
actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED,
originalMessage: {
- html: 'Hello world',
- whisperedTo: [],
+ paymentType: 'ACH',
},
message: [{html: 'Waiting for the bank account', type: 'Action type', text: 'Action text'}],
},
@@ -303,7 +302,6 @@ describe('ReportActionsUtils', () => {
];
const result = ReportActionsUtils.getSortedReportActionsForDisplay(input);
- input.pop();
expect(result).toStrictEqual(input);
});
@@ -389,8 +387,7 @@ describe('ReportActionsUtils', () => {
reportActionID: '1661970171066216',
actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED,
originalMessage: {
- html: 'Hello world',
- whisperedTo: [],
+ paymentType: 'ACH',
},
message: [{html: 'Waiting for the bank account', type: 'Action type', text: 'Action text'}],
},
diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js
index a5b0a5d3c151..9fbea1df862e 100644
--- a/tests/unit/ReportUtilsTest.js
+++ b/tests/unit/ReportUtilsTest.js
@@ -410,21 +410,26 @@ describe('ReportUtils', () => {
});
});
- it("it is a non-open expense report tied to user's own paid policy expense chat", () => {
- Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}101`, {
- reportID: '101',
- chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
- isOwnPolicyExpenseChat: true,
- }).then(() => {
+ it("it is a submitted report tied to user's own policy expense chat and the policy does not have Instant Submit frequency", () => {
+ const paidPolicy = {
+ id: '3f54cca8',
+ type: CONST.POLICY.TYPE.TEAM,
+ };
+ Promise.all([
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${paidPolicy.id}`, paidPolicy),
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}101`, {
+ reportID: '101',
+ chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
+ isOwnPolicyExpenseChat: true,
+ }),
+ ]).then(() => {
const report = {
...LHNTestUtils.getFakeReport(),
type: CONST.REPORT.TYPE.EXPENSE,
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
parentReportID: '101',
- };
- const paidPolicy = {
- type: CONST.POLICY.TYPE.TEAM,
+ policyID: paidPolicy.id,
};
const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]]);
expect(moneyRequestOptions.length).toBe(0);
@@ -498,7 +503,7 @@ describe('ReportUtils', () => {
});
});
- it("it is an open expense report tied to user's own paid policy expense chat", () => {
+ it("it is an open expense report tied to user's own policy expense chat", () => {
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}103`, {
reportID: '103',
chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
@@ -542,6 +547,33 @@ describe('ReportUtils', () => {
expect(moneyRequestOptions.length).toBe(1);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true);
});
+
+ it("it is a submitted expense report in user's own policyExpenseChat and the policy has Instant Submit frequency", () => {
+ const paidPolicy = {
+ id: 'ef72dfeb',
+ type: CONST.POLICY.TYPE.TEAM,
+ autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT,
+ };
+ Promise.all([
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${paidPolicy.id}`, paidPolicy),
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}101`, {
+ reportID: '101',
+ chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
+ isOwnPolicyExpenseChat: true,
+ }),
+ ]).then(() => {
+ const report = {
+ ...LHNTestUtils.getFakeReport(),
+ type: CONST.REPORT.TYPE.EXPENSE,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
+ parentReportID: '101',
+ policyID: paidPolicy.id,
+ };
+ const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]]);
+ expect(moneyRequestOptions.length).toBe(1);
+ });
+ });
});
describe('return multiple money request option if', () => {
diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts
index 4ee3e63b13e7..dcd144e77596 100644
--- a/tests/utils/PusherHelper.ts
+++ b/tests/utils/PusherHelper.ts
@@ -26,12 +26,17 @@ function setup() {
function emitOnyxUpdate(args: OnyxUpdate[]) {
const channel = Pusher.getChannel(CHANNEL_NAME);
- channel?.emit(Pusher.TYPE.MULTIPLE_EVENTS, [
- {
- eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE,
- data: args,
- },
- ]);
+ channel?.emit(Pusher.TYPE.MULTIPLE_EVENTS, {
+ type: 'pusher',
+ lastUpdateID: null,
+ previousUpdateID: null,
+ updates: [
+ {
+ eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE,
+ data: args,
+ },
+ ],
+ });
}
function teardown() {