Skip to content

Commit

Permalink
Merge pull request Expensify#53749 from Krishna2323/krishna2323/issue…
Browse files Browse the repository at this point in the history
…/52979_pr2

fix: Make all task titles/descriptions read-only to all but the author/assignee.
  • Loading branch information
thienlnam authored Dec 23, 2024
2 parents eebf550 + 56a7c3a commit c0db276
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 53 deletions.
9 changes: 5 additions & 4 deletions src/components/ReportActionItem/TaskPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che
? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED
: action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED;
const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitleFromReport(taskReport, action?.childReportName ?? ''));
const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? -1;
const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID;
const taskOwnerAccountID = taskReport?.ownerAccountID ?? action?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID;
const hasAssignee = taskAssigneeAccountID > 0;
const personalDetails = usePersonalDetails();
const avatar = personalDetails?.[taskAssigneeAccountID]?.avatar ?? Expensicons.FallbackAvatar;
Expand Down Expand Up @@ -106,12 +107,12 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che
<Checkbox
style={[styles.mr2]}
isChecked={isTaskCompleted}
disabled={!Task.canModifyTask(taskReport, currentUserPersonalDetails.accountID) || !Task.canActionTask(taskReport, currentUserPersonalDetails.accountID)}
disabled={!Task.canActionTask(taskReport, currentUserPersonalDetails.accountID, taskOwnerAccountID, taskAssigneeAccountID)}
onPress={Session.checkIfActionIsAllowed(() => {
if (isTaskCompleted) {
Task.reopenTask(taskReport);
Task.reopenTask(taskReport, taskReportID);
} else {
Task.completeTask(taskReport);
Task.completeTask(taskReport, taskReportID);
}
})}
accessibilityLabel={translate('task.task')}
Expand Down
35 changes: 19 additions & 16 deletions src/components/ReportActionItem/TaskView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {usePersonalDetails} from '@components/OnyxProvider';
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
import Text from '@components/Text';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -27,27 +26,28 @@ import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Report} from '@src/types/onyx';

type TaskViewProps = WithCurrentUserPersonalDetailsProps & {
type TaskViewProps = {
/** The report currently being looked at */
report: Report;
};

function TaskView({report, ...props}: TaskViewProps) {
function TaskView({report}: TaskViewProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const personalDetails = usePersonalDetails();
useEffect(() => {
Task.setTaskReport(report);
}, [report]);
const personalDetails = usePersonalDetails();
const taskTitle = convertToLTR(report.reportName ?? '');
const assigneeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips(
OptionsListUtils.getPersonalDetailsForAccountIDs(report.managerID ? [report.managerID] : [], personalDetails),
false,
);
const isCompleted = ReportUtils.isCompletedTaskReport(report);
const isOpen = ReportUtils.isOpenTaskReport(report);
const canModifyTask = Task.canModifyTask(report, props.currentUserPersonalDetails.accountID);
const canActionTask = Task.canActionTask(report, props.currentUserPersonalDetails.accountID);
const isCompleted = ReportUtils.isCompletedTaskReport(report);
const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID);
const canActionTask = Task.canActionTask(report, currentUserPersonalDetails.accountID);
const disableState = !canModifyTask;
const isDisableInteractive = !canModifyTask || !isOpen;
const {translate} = useLocalize();
Expand Down Expand Up @@ -77,10 +77,10 @@ function TaskView({report, ...props}: TaskViewProps) {
styles.ph5,
styles.pv2,
StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, false, disableState, !isDisableInteractive), true),
isDisableInteractive && !disableState && styles.cursorDefault,
isDisableInteractive && styles.cursorDefault,
]}
disabled={disableState}
accessibilityLabel={taskTitle || translate('task.task')}
disabled={isDisableInteractive}
>
{({pressed}) => (
<OfflineWithFeedback pendingAction={report.pendingFields?.reportName}>
Expand All @@ -104,7 +104,7 @@ function TaskView({report, ...props}: TaskViewProps) {
containerBorderRadius={8}
caretSize={16}
accessibilityLabel={taskTitle || translate('task.task')}
disabled={!canModifyTask || !canActionTask}
disabled={!canActionTask}
/>
<View style={[styles.flexRow, styles.flex1]}>
<Text
Expand All @@ -114,7 +114,7 @@ function TaskView({report, ...props}: TaskViewProps) {
{taskTitle}
</Text>
</View>
{isOpen && (
{!isDisableInteractive && (
<View style={styles.taskRightIconContainer}>
<Icon
additionalStyles={[styles.alignItemsCenter]}
Expand All @@ -135,12 +135,13 @@ function TaskView({report, ...props}: TaskViewProps) {
description={translate('task.description')}
title={report.description ?? ''}
onPress={() => Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))}
shouldShowRightIcon={isOpen}
shouldShowRightIcon={!isDisableInteractive}
disabled={disableState}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
shouldGreyOutWhenDisabled={false}
numberOfLinesTitle={0}
interactive={!isDisableInteractive}
shouldUseDefaultCursorWhenDisabled
/>
</OfflineWithFeedback>
<OfflineWithFeedback pendingAction={report.pendingFields?.managerID}>
Expand All @@ -153,23 +154,25 @@ function TaskView({report, ...props}: TaskViewProps) {
avatarSize={CONST.AVATAR_SIZE.SMALLER}
titleStyle={styles.assigneeTextStyle}
onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))}
shouldShowRightIcon={isOpen}
shouldShowRightIcon={!isDisableInteractive}
disabled={disableState}
wrapperStyle={[styles.pv2]}
isSmallAvatarSubscriptMenu
shouldGreyOutWhenDisabled={false}
interactive={!isDisableInteractive}
titleWithTooltips={assigneeTooltipDetails}
shouldUseDefaultCursorWhenDisabled
/>
) : (
<MenuItemWithTopDescription
description={translate('task.assignee')}
onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))}
shouldShowRightIcon={isOpen}
shouldShowRightIcon={!isDisableInteractive}
disabled={disableState}
wrapperStyle={[styles.pv2]}
shouldGreyOutWhenDisabled={false}
interactive={!isDisableInteractive}
shouldUseDefaultCursorWhenDisabled
/>
)}
</OfflineWithFeedback>
Expand All @@ -180,4 +183,4 @@ function TaskView({report, ...props}: TaskViewProps) {

TaskView.displayName = 'TaskView';

export default withCurrentUserPersonalDetails(TaskView);
export default TaskView;
23 changes: 7 additions & 16 deletions src/components/TaskHeaderActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
import React from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ReportUtils from '@libs/ReportUtils';
import * as TaskUtils from '@libs/TaskUtils';
import * as Session from '@userActions/Session';
import * as Task from '@userActions/Task';
import ONYXKEYS from '@src/ONYXKEYS';
import CONST from '@src/CONST';
import type * as OnyxTypes from '@src/types/onyx';
import Button from './Button';
import {useSession} from './OnyxProvider';

type TaskHeaderActionButtonOnyxProps = {
/** Current user session */
session: OnyxEntry<OnyxTypes.Session>;
};

type TaskHeaderActionButtonProps = TaskHeaderActionButtonOnyxProps & {
type TaskHeaderActionButtonProps = {
/** The report currently being looked at */
report: OnyxTypes.Report;
};

function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) {
function TaskHeaderActionButton({report}: TaskHeaderActionButtonProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const session = useSession();

if (!ReportUtils.canWriteInReport(report)) {
return null;
Expand All @@ -34,7 +29,7 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps)
<View style={[styles.flexRow, styles.alignItemsCenter, styles.justifyContentEnd]}>
<Button
success
isDisabled={!Task.canModifyTask(report, session?.accountID ?? -1) || !Task.canActionTask(report, session?.accountID ?? -1)}
isDisabled={!Task.canActionTask(report, session?.accountID ?? CONST.DEFAULT_NUMBER_ID)}
text={translate(ReportUtils.isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')}
onPress={Session.checkIfActionIsAllowed(() => {
// If we're already navigating to these task editing pages, early return not to mark as completed, otherwise we would have not found page.
Expand All @@ -55,8 +50,4 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps)

TaskHeaderActionButton.displayName = 'TaskHeaderActionButton';

export default withOnyx<TaskHeaderActionButtonProps, TaskHeaderActionButtonOnyxProps>({
session: {
key: ONYXKEYS.SESSION,
},
})(TaskHeaderActionButton);
export default TaskHeaderActionButton;
55 changes: 38 additions & 17 deletions src/libs/actions/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Onyx.connect({
key: ONYXKEYS.SESSION,
callback: (value) => {
currentUserEmail = value?.email ?? '';
currentUserAccountID = value?.accountID ?? -1;
currentUserAccountID = value?.accountID ?? CONST.DEFAULT_NUMBER_ID;
},
});

Expand Down Expand Up @@ -365,8 +365,13 @@ function getOutstandingChildTask(taskReport: OnyxEntry<OnyxTypes.Report>) {
/**
* Complete a task
*/
function completeTask(taskReport: OnyxEntry<OnyxTypes.Report>) {
const taskReportID = taskReport?.reportID ?? '-1';
function completeTask(taskReport: OnyxEntry<OnyxTypes.Report>, reportIDFromAction?: string) {
const taskReportID = taskReport?.reportID ?? reportIDFromAction;

if (!taskReportID) {
return;
}

const message = `marked as complete`;
const completedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED, message);
const parentReport = getParentReport(taskReport);
Expand Down Expand Up @@ -450,8 +455,11 @@ function completeTask(taskReport: OnyxEntry<OnyxTypes.Report>) {
/**
* Reopen a closed task
*/
function reopenTask(taskReport: OnyxEntry<OnyxTypes.Report>) {
const taskReportID = taskReport?.reportID ?? '-1';
function reopenTask(taskReport: OnyxEntry<OnyxTypes.Report>, reportIDFromAction?: string) {
const taskReportID = taskReport?.reportID ?? reportIDFromAction;
if (!taskReportID) {
return;
}
const message = `marked as incomplete`;
const reopenedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASK_REOPENED, message);
const parentReport = getParentReport(taskReport);
Expand Down Expand Up @@ -608,7 +616,7 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task

function editTaskAssignee(report: OnyxTypes.Report, sessionAccountID: number, assigneeEmail: string, assigneeAccountID: number | null = 0, assigneeChatReport?: OnyxEntry<OnyxTypes.Report>) {
// Create the EditedReportAction on the task
const editTaskReportAction = ReportUtils.buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID ?? 0);
const editTaskReportAction = ReportUtils.buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID ?? CONST.DEFAULT_NUMBER_ID);
const reportName = report.reportName?.trim();

let assigneeChatReportOnyxData;
Expand Down Expand Up @@ -1214,9 +1222,14 @@ function getTaskOwnerAccountID(taskReport: OnyxEntry<OnyxTypes.Report>): number
}

/**
* Check if you're allowed to modify the task - anyone that has write access to the report can modify the task, except auditor
* Check if you're allowed to modify the task - only the author can modify the task
*/
function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID: number): boolean {
function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID: number, taskOwnerAccountID?: number): boolean {
const ownerAccountID = getTaskOwnerAccountID(taskReport) ?? taskOwnerAccountID;
if (ownerAccountID !== sessionAccountID) {
return false;
}

if (ReportUtils.isCanceledTaskReport(taskReport)) {
return false;
}
Expand All @@ -1227,22 +1240,30 @@ function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID
return false;
}

if (sessionAccountID === getTaskOwnerAccountID(taskReport) || sessionAccountID === getTaskAssigneeAccountID(taskReport)) {
if (sessionAccountID === ownerAccountID) {
return true;
}

if (!ReportUtils.canWriteInReport(taskReport) || ReportUtils.isAuditor(taskReport)) {
return false;
}

return !isEmptyObject(taskReport) && ReportUtils.isAllowedToComment(taskReport);
return false;
}

/**
* Check if you can change the status of the task (mark complete or incomplete). Only the task owner and task assignee can do this.
*/
function canActionTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID: number): boolean {
return sessionAccountID === getTaskOwnerAccountID(taskReport) || sessionAccountID === getTaskAssigneeAccountID(taskReport);
function canActionTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID: number, taskOwnerAccountID?: number, taskAssigneeAccountID?: number): boolean {
if (ReportUtils.isCanceledTaskReport(taskReport)) {
return false;
}

const parentReport = getParentReport(taskReport);
const reportNameValuePairs = ReportUtils.getReportNameValuePairs(parentReport?.reportID);
if (ReportUtils.isArchivedRoom(parentReport, reportNameValuePairs)) {
return false;
}

const ownerAccountID = getTaskOwnerAccountID(taskReport) ?? taskOwnerAccountID;
const assigneeAccountID = getTaskAssigneeAccountID(taskReport) ?? taskAssigneeAccountID;
return sessionAccountID === ownerAccountID || sessionAccountID === assigneeAccountID;
}

function clearTaskErrors(reportID: string) {
Expand Down Expand Up @@ -1286,9 +1307,9 @@ export {
getTaskAssigneeAccountID,
clearTaskErrors,
canModifyTask,
canActionTask,
setNewOptimisticAssignee,
getNavigationUrlOnTaskDelete,
canActionTask,
};

export type {PolicyValue, Assignee, ShareDestination};

0 comments on commit c0db276

Please sign in to comment.