Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: task view with offline with feedback #23705

Merged
merged 10 commits into from
Aug 3, 2023
182 changes: 97 additions & 85 deletions src/components/ReportActionItem/TaskView.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {useEffect} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import lodashGet from 'lodash/get';
import reportPropTypes from '../../pages/reportPropTypes';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import withWindowDimensions from '../withWindowDimensions';
Expand All @@ -11,6 +12,7 @@ import ROUTES from '../../ROUTES';
import MenuItemWithTopDescription from '../MenuItemWithTopDescription';
import Hoverable from '../Hoverable';
import MenuItem from '../MenuItem';
import OfflineWithFeedback from '../OfflineWithFeedback';
import styles from '../../styles/styles';
import * as ReportUtils from '../../libs/ReportUtils';
import * as OptionsListUtils from '../../libs/OptionsListUtils';
Expand Down Expand Up @@ -51,94 +53,104 @@ function TaskView(props) {
const disableState = !canModifyTask || !isOpen;
return (
<View>
<Hoverable>
{(hovered) => (
<PressableWithSecondaryInteraction
onPress={Session.checkIfActionIsAllowed((e) => {
if (e && e.type === 'click') {
e.currentTarget.blur();
}
<OfflineWithFeedback
shouldShowErrorMessages
errors={lodashGet(props, 'report.errorFields.editTask')}
onClose={() => Task.clearEditTaskErrors(props.report.reportID)}
errorRowStyles={styles.ph5}
>
<Hoverable>
{(hovered) => (
<PressableWithSecondaryInteraction
onPress={Session.checkIfActionIsAllowed((e) => {
if (e && e.type === 'click') {
e.currentTarget.blur();
}

Navigation.navigate(ROUTES.getTaskReportTitleRoute(props.report.reportID));
})}
style={({pressed}) => [styles.ph5, styles.pv2, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, false, disableState), true)]}
ref={props.forwardedRef}
disabled={disableState}
accessibilityLabel={taskTitle || props.translate('task.task')}
>
{({pressed}) => (
<>
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
<Text style={styles.taskTitleDescription}>{props.translate('task.title')}</Text>
<View style={[styles.flexRow, styles.alignItemsTop, styles.flex1]}>
<Checkbox
onPress={() => (isCompleted ? Task.reopenTask(props.report.reportID, taskTitle) : Task.completeTask(props.report.reportID, taskTitle))}
isChecked={isCompleted}
style={styles.taskMenuItemCheckbox}
containerSize={24}
containerBorderRadius={8}
caretSize={16}
accessibilityLabel={taskTitle || props.translate('task.task')}
disabled={isCanceled || !canModifyTask}
/>
<View style={[styles.flexRow, styles.flex1]}>
<Text
numberOfLines={3}
style={styles.taskTitleMenuItem}
>
{taskTitle}
</Text>
</View>
{isOpen && (
<View style={styles.taskRightIconContainer}>
<Icon
additionalStyles={[styles.alignItemsCenter]}
src={Expensicons.ArrowRight}
fill={StyleUtils.getIconFillColor(getButtonState(hovered, pressed, false, disableState))}
/>
Navigation.navigate(ROUTES.getTaskReportTitleRoute(props.report.reportID));
})}
style={({pressed}) => [styles.ph5, styles.pv2, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, false, disableState), true)]}
ref={props.forwardedRef}
disabled={disableState}
accessibilityLabel={taskTitle || props.translate('task.task')}
>
{({pressed}) => (
<OfflineWithFeedback pendingAction={lodashGet(props, 'report.pendingFields.reportName')}>
<Text style={styles.taskTitleDescription}>{props.translate('task.title')}</Text>
<View style={[styles.flexRow, styles.alignItemsTop, styles.flex1]}>
<Checkbox
onPress={() => (isCompleted ? Task.reopenTask(props.report.reportID, taskTitle) : Task.completeTask(props.report.reportID, taskTitle))}
isChecked={isCompleted}
style={styles.taskMenuItemCheckbox}
containerSize={24}
containerBorderRadius={8}
caretSize={16}
accessibilityLabel={taskTitle || props.translate('task.task')}
disabled={isCanceled || !canModifyTask}
/>
<View style={[styles.flexRow, styles.flex1]}>
<Text
numberOfLines={3}
style={styles.taskTitleMenuItem}
>
{taskTitle}
</Text>
</View>
)}
</View>
</>
)}
</PressableWithSecondaryInteraction>
{isOpen && (
<View style={styles.taskRightIconContainer}>
<Icon
additionalStyles={[styles.alignItemsCenter]}
src={Expensicons.ArrowRight}
fill={StyleUtils.getIconFillColor(getButtonState(hovered, pressed, false, disableState))}
/>
</View>
)}
</View>
</OfflineWithFeedback>
)}
</PressableWithSecondaryInteraction>
)}
</Hoverable>
<OfflineWithFeedback pendingAction={lodashGet(props, 'report.pendingFields.description')}>
<MenuItemWithTopDescription
description={props.translate('task.description')}
title={props.report.description || ''}
onPress={() => Navigation.navigate(ROUTES.getTaskReportDescriptionRoute(props.report.reportID))}
shouldShowRightIcon={isOpen}
disabled={disableState}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
shouldGreyOutWhenDisabled={false}
numberOfLinesTitle={0}
/>
</OfflineWithFeedback>
{props.report.managerID ? (
<OfflineWithFeedback pendingAction={lodashGet(props, 'report.pendingFields.managerID')}>
<MenuItem
label={props.translate('task.assignee')}
title={ReportUtils.getDisplayNameForParticipant(props.report.managerID)}
icon={OptionsListUtils.getAvatarsForAccountIDs([props.report.managerID], props.personalDetails)}
iconType={CONST.ICON_TYPE_AVATAR}
avatarSize={CONST.AVATAR_SIZE.SMALLER}
titleStyle={styles.assigneeTextStyle}
onPress={() => Navigation.navigate(ROUTES.getTaskReportAssigneeRoute(props.report.reportID))}
shouldShowRightIcon={isOpen}
disabled={disableState}
wrapperStyle={[styles.pv2]}
isSmallAvatarSubscriptMenu
shouldGreyOutWhenDisabled={false}
/>
</OfflineWithFeedback>
) : (
<MenuItemWithTopDescription
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #35196, we should have wrapped MenuItemWithTopDescription with the OfflineWithFeedback to support the offline mode. In offline mode, tasks created without an assignee will not have offline feedback.

description={props.translate('task.assignee')}
onPress={() => Navigation.navigate(ROUTES.getTaskReportAssigneeRoute(props.report.reportID))}
shouldShowRightIcon={isOpen}
disabled={disableState}
wrapperStyle={[styles.pv2]}
shouldGreyOutWhenDisabled={false}
/>
)}
</Hoverable>
<MenuItemWithTopDescription
description={props.translate('task.description')}
title={props.report.description || ''}
onPress={() => Navigation.navigate(ROUTES.getTaskReportDescriptionRoute(props.report.reportID))}
shouldShowRightIcon={isOpen}
disabled={disableState}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
shouldGreyOutWhenDisabled={false}
numberOfLinesTitle={0}
/>
{props.report.managerID ? (
<MenuItem
label={props.translate('task.assignee')}
title={ReportUtils.getDisplayNameForParticipant(props.report.managerID)}
icon={OptionsListUtils.getAvatarsForAccountIDs([props.report.managerID], props.personalDetails)}
iconType={CONST.ICON_TYPE_AVATAR}
avatarSize={CONST.AVATAR_SIZE.SMALLER}
titleStyle={styles.assigneeTextStyle}
onPress={() => Navigation.navigate(ROUTES.getTaskReportAssigneeRoute(props.report.reportID))}
shouldShowRightIcon={isOpen}
disabled={disableState}
wrapperStyle={[styles.pv2]}
isSmallAvatarSubscriptMenu
shouldGreyOutWhenDisabled={false}
/>
) : (
<MenuItemWithTopDescription
description={props.translate('task.assignee')}
onPress={() => Navigation.navigate(ROUTES.getTaskReportAssigneeRoute(props.report.reportID))}
shouldShowRightIcon={isOpen}
disabled={disableState}
wrapperStyle={[styles.pv2]}
shouldGreyOutWhenDisabled={false}
/>
)}

</OfflineWithFeedback>
{props.shouldShowHorizontalRule && <View style={styles.reportHorizontalRule} />}
</View>
);
Expand Down
37 changes: 35 additions & 2 deletions src/libs/actions/Task.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,10 +401,27 @@ function editTaskAndNavigate(report, ownerAccountID, {title, description, assign
description: reportDescription,
managerID: assigneeAccountID || report.managerID,
managerEmail: assignee || report.managerEmail,
pendingFields: {
...(title && {reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(description && {description: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(assigneeAccountID && {managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
},
},
},
];
const successData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`,
value: {
pendingFields: {
...(title && {reportName: null}),
...(description && {description: null}),
...(assigneeAccountID && {managerID: null}),
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
},
},
},
];
const successData = [];
const failureData = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand All @@ -414,7 +431,12 @@ function editTaskAndNavigate(report, ownerAccountID, {title, description, assign
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`,
value: {reportName: report.reportName, description: report.description, assignee: report.managerEmail, assigneeAccountID: report.managerID},
value: {
reportName: report.reportName,
description: report.description,
assignee: report.managerEmail,
assigneeAccountID: report.managerID,
},
},
];

Expand Down Expand Up @@ -737,6 +759,16 @@ function canModifyTask(taskReport, sessionAccountID) {
return ReportUtils.isAllowedToComment(parentReport);
}

/**
* @param {String} reportID
*/
function clearEditTaskErrors(reportID) {
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {
pendingFields: null,
errorFields: null,
});
}

export {
createTaskAndNavigate,
editTaskAndNavigate,
Expand All @@ -755,5 +787,6 @@ export {
cancelTask,
dismissModalAndClearOutTaskInfo,
getTaskAssigneeAccountID,
clearEditTaskErrors,
canModifyTask,
};
2 changes: 2 additions & 0 deletions src/pages/home/report/ReportActionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@ export default compose(
prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker &&
_.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) &&
_.isEqual(prevProps.action, nextProps.action) &&
_.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) &&
_.isEqual(prevProps.report.errorFields, nextProps.report.errorFields) &&
lodashGet(prevProps.report, 'statusNum') === lodashGet(nextProps.report, 'statusNum') &&
lodashGet(prevProps.report, 'stateNum') === lodashGet(nextProps.report, 'stateNum') &&
prevProps.translate === nextProps.translate &&
Expand Down
8 changes: 8 additions & 0 deletions src/pages/home/report/ReportActionsView.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,14 @@ function arePropsEqual(oldProps, newProps) {
return false;
}

if (!_.isEqual(oldProps.report.pendingFields, newProps.report.pendingFields)) {
return false;
}

if (!_.isEqual(oldProps.report.errorFields, newProps.report.errorFields)) {
return false;
}

if (lodashGet(oldProps.network, 'isOffline') !== lodashGet(newProps.network, 'isOffline')) {
return false;
}
Expand Down
3 changes: 3 additions & 0 deletions src/pages/reportPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,7 @@ export default PropTypes.shape({

/** Which user role is capable of posting messages on the report */
writeCapability: PropTypes.oneOf(_.values(CONST.REPORT.WRITE_CAPABILITIES)),

/** Field-specific pending states for offline UI status */
pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
});
Loading