diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 740ab345fa74..d05ebd4bfd09 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -88,9 +88,9 @@ function TaskPreview(props) { disabled={ReportUtils.isCanceledTaskReport(props.taskReport)} onPress={() => { if (isTaskCompleted) { - Task.reopenTask(props.taskReportID, taskTitle); + Task.reopenTask(props.taskReport, taskTitle); } else { - Task.completeTask(props.taskReportID, taskTitle); + Task.completeTask(props.taskReport, taskTitle); } }} accessibilityLabel={props.translate('task.task')} diff --git a/src/components/ReportActionItem/TaskView.js b/src/components/ReportActionItem/TaskView.js index 7fd04d8544c7..83c15acee79f 100644 --- a/src/components/ReportActionItem/TaskView.js +++ b/src/components/ReportActionItem/TaskView.js @@ -79,7 +79,7 @@ function TaskView(props) { {props.translate('task.title')} (isCompleted ? Task.reopenTask(props.report.reportID, taskTitle) : Task.completeTask(props.report.reportID, taskTitle))} + onPress={() => (isCompleted ? Task.reopenTask(props.report, taskTitle) : Task.completeTask(props.report, taskTitle))} isChecked={isCompleted} style={styles.taskMenuItemCheckbox} containerSize={24} diff --git a/src/components/TaskHeaderActionButton.js b/src/components/TaskHeaderActionButton.js index e0a2549b2f0c..148cf81ce672 100644 --- a/src/components/TaskHeaderActionButton.js +++ b/src/components/TaskHeaderActionButton.js @@ -50,9 +50,7 @@ function TaskHeaderActionButton(props) { medium text={props.translate(ReportUtils.isCompletedTaskReport(props.report) ? 'task.markAsIncomplete' : 'task.markAsDone')} onPress={() => - ReportUtils.isCompletedTaskReport(props.report) - ? Task.reopenTask(props.report.reportID, props.report.reportName) - : Task.completeTask(props.report.reportID, props.report.reportName) + ReportUtils.isCompletedTaskReport(props.report) ? Task.reopenTask(props.report, props.report.reportName) : Task.completeTask(props.report, props.report.reportName) } style={[styles.flex1]} /> diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 3cd5621e5e32..6d777533360d 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -119,6 +119,17 @@ function getParentReportAction(report, allReportActionsParam = undefined) { return lodashGet(allReportActionsParam || allReportActions, [report.parentReportID, report.parentReportActionID], {}); } +/** + * Find the reportAction having the given childReportID in parent report actions + * + * @param {String} childReportID + * @param {String} parentReportID + * @returns {Object} + */ +function getParentReportActionInReport(childReportID, parentReportID) { + return _.find(allReportActions[parentReportID], (reportAction) => reportAction && `${reportAction.childReportID}` === `${childReportID}`); +} + /** * Determines if the given report action is sent money report action by checking for 'pay' type and presence of IOUDetails object. * @@ -583,6 +594,7 @@ export { getReportPreviewAction, isCreatedTaskReportAction, getParentReportAction, + getParentReportActionInReport, isTransactionThread, getFormattedAmount, isSentMoneyReportAction, diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 3fd1236e26ae..e3b6cb2ccc52 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1592,24 +1592,26 @@ function updateOptimisticParentReportAction(parentReportAction, lastVisibleActio * @param {String} reportID The reportID of the report that is updated * @param {String} lastVisibleActionCreated Last visible action created of the child report * @param {String} type The type of action in the child report + * @param {String} parentReportID Custom reportID to be updated + * @param {String} parentReportActionID Custom reportActionID to be updated * @returns {Object} */ -const getOptimisticDataForParentReportAction = (reportID, lastVisibleActionCreated, type) => { +function getOptimisticDataForParentReportAction(reportID, lastVisibleActionCreated, type, parentReportID = '', parentReportActionID = '') { const report = getReport(reportID); - if (report && report.parentReportActionID) { - const parentReportAction = ReportActionsUtils.getParentReportAction(report); - if (parentReportAction && parentReportAction.reportActionID) { - return { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, - value: { - [parentReportAction.reportActionID]: updateOptimisticParentReportAction(parentReportAction, lastVisibleActionCreated, type), - }, - }; - } + const parentReportAction = ReportActionsUtils.getParentReportAction(report); + if (_.isEmpty(parentReportAction)) { + return {}; } - return {}; -}; + + const optimisticParentReportAction = updateOptimisticParentReportAction(parentReportAction, lastVisibleActionCreated, type); + return { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID || report.parentReportID}`, + value: { + [parentReportActionID || report.parentReportActionID]: optimisticParentReportAction, + }, + }; +} /** * Builds an optimistic reportAction for the parent report when a task is created @@ -2788,6 +2790,30 @@ function getParentReport(report) { return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, {}); } +/** + * Find the parent report action in assignee report for a task report + * Returns an empty object if assignee report is the same as the share destination report + * + * @param {Object} taskReport + * @returns {Object} + */ +function getTaskParentReportActionIDInAssigneeReport(taskReport) { + const assigneeChatReportID = lodashGet(getChatByParticipants(isTaskAssignee(taskReport) ? [taskReport.ownerAccountID] : [taskReport.managerID]), 'reportID'); + if (!assigneeChatReportID || assigneeChatReportID === taskReport.parentReportID) { + return {}; + } + + const clonedParentReportActionID = lodashGet(ReportActionsUtils.getParentReportActionInReport(taskReport.reportID, assigneeChatReportID), 'reportActionID'); + if (!clonedParentReportActionID) { + return {}; + } + + return { + reportID: assigneeChatReportID, + reportActionID: clonedParentReportActionID, + }; +} + /** * Return true if the composer should be hidden * @param {Object} report @@ -2969,6 +2995,7 @@ export { getMoneyRequestAction, getBankAccountRoute, getParentReport, + getTaskParentReportActionIDInAssigneeReport, getReportPreviewMessage, shouldHideComposer, getOriginalReportID, diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 36eabd05cece..5cb186415406 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -213,7 +213,13 @@ function createTaskAndNavigate(parentReportID, title, description, assignee, ass Navigation.dismissModal(optimisticTaskReport.reportID); } -function completeTask(taskReportID, taskTitle) { +/** + * Complete a task + * @param {Object} taskReport task report + * @param {String} taskTitle Title of the task + */ +function completeTask(taskReport, taskTitle) { + const taskReportID = taskReport.reportID; const message = `completed task: ${taskTitle}`; const completedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED, message); @@ -266,9 +272,25 @@ function completeTask(taskReportID, taskTitle) { ]; // Update optimistic data for parent report action - const optimisticParentReportData = ReportUtils.getOptimisticDataForParentReportAction(taskReportID, completedTaskReportAction.created, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - if (!_.isEmpty(optimisticParentReportData)) { - optimisticData.push(optimisticParentReportData); + const optimisticDataForParentReportAction = ReportUtils.getOptimisticDataForParentReportAction(taskReportID, completedTaskReportAction.created, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + if (!_.isEmpty(optimisticDataForParentReportAction)) { + optimisticData.push(optimisticDataForParentReportAction); + } + + // Multiple report actions can link to the same child. Both share destination (task parent) and assignee report link to the same report action. + // We need to find and update the other parent report action (in assignee report). More info https://github.com/Expensify/App/issues/23920#issuecomment-1663092717 + const assigneeReportAction = ReportUtils.getTaskParentReportActionIDInAssigneeReport(taskReport); + if (!_.isEmpty(assigneeReportAction)) { + const optimisticDataForClonedParentReportAction = ReportUtils.getOptimisticDataForParentReportAction( + taskReportID, + completedTaskReportAction.created, + CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + assigneeReportAction.reportID, + assigneeReportAction.reportActionID, + ); + if (!_.isEmpty(optimisticDataForClonedParentReportAction)) { + optimisticData.push(optimisticDataForClonedParentReportAction); + } } API.write( @@ -282,11 +304,12 @@ function completeTask(taskReportID, taskTitle) { } /** - * Reopens a closed task - * @param {string} taskReportID ReportID of the task - * @param {string} taskTitle Title of the task + * Reopen a closed task + * @param {Object} taskReport task report + * @param {String} taskTitle Title of the task */ -function reopenTask(taskReportID, taskTitle) { +function reopenTask(taskReport, taskTitle) { + const taskReportID = taskReport.reportID; const message = `reopened task: ${taskTitle}`; const reopenedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASKREOPENED, message); @@ -342,9 +365,25 @@ function reopenTask(taskReportID, taskTitle) { ]; // Update optimistic data for parent report action - const optimisticParentReportData = ReportUtils.getOptimisticDataForParentReportAction(taskReportID, reopenedTaskReportAction.created, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - if (!_.isEmpty(optimisticParentReportData)) { - optimisticData.push(optimisticParentReportData); + const optimisticDataForParentReportAction = ReportUtils.getOptimisticDataForParentReportAction(taskReportID, reopenedTaskReportAction.created, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + if (!_.isEmpty(optimisticDataForParentReportAction)) { + optimisticData.push(optimisticDataForParentReportAction); + } + + // Multiple report actions can link to the same child. Both share destination (task parent) and assignee report link to the same report action. + // We need to find and update the other parent report action (in assignee report). More info https://github.com/Expensify/App/issues/23920#issuecomment-1663092717 + const assigneeReportAction = ReportUtils.getTaskParentReportActionIDInAssigneeReport(taskReport); + if (!_.isEmpty(assigneeReportAction)) { + const optimisticDataForClonedParentReportAction = ReportUtils.getOptimisticDataForParentReportAction( + taskReportID, + reopenedTaskReportAction.created, + CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + assigneeReportAction.reportID, + assigneeReportAction.reportActionID, + ); + if (!_.isEmpty(optimisticDataForClonedParentReportAction)) { + optimisticData.push(optimisticDataForClonedParentReportAction); + } } API.write( diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 85cdd239dae7..fafd6caf4f1f 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -104,7 +104,7 @@ function HeaderView(props) { threeDotMenuItems.push({ icon: Expensicons.Checkmark, text: props.translate('task.markAsDone'), - onSelected: () => Task.completeTask(props.report.reportID, title), + onSelected: () => Task.completeTask(props.report, title), }); } @@ -113,7 +113,7 @@ function HeaderView(props) { threeDotMenuItems.push({ icon: Expensicons.Checkmark, text: props.translate('task.markAsIncomplete'), - onSelected: () => Task.reopenTask(props.report.reportID, title), + onSelected: () => Task.reopenTask(props.report, title), }); }