box-ui-elements-mlh
Version:
435 lines (397 loc) • 17.9 kB
JavaScript
// @flow
import * as React from 'react';
import noop from 'lodash/noop';
import flow from 'lodash/flow';
import get from 'lodash/get';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import TetherComponent from 'react-tether';
import { withFeatureConsumer, getFeatureConfig } from '../../../common/feature-checking';
import { withAPIContext } from '../../../common/api-context';
import Avatar from '../Avatar';
import Media from '../../../../components/media';
import { MenuItem } from '../../../../components/menu';
import ActivityCard from '../ActivityCard';
import ActivityError from '../common/activity-error';
import ActivityMessage from '../common/activity-message';
import ActivityTimestamp from '../common/activity-timestamp';
import DeleteConfirmation from '../common/delete-confirmation';
import IconTaskApproval from '../../../../icons/two-toned/IconTaskApproval';
import IconTaskGeneral from '../../../../icons/two-toned/IconTaskGeneral';
import IconTrash from '../../../../icons/general/IconTrash';
import IconPencil from '../../../../icons/general/IconPencil';
import UserLink from '../common/user-link';
import API from '../../../../api/APIFactory';
import {
TASK_COMPLETION_RULE_ALL,
TASK_NEW_APPROVED,
TASK_NEW_REJECTED,
TASK_NEW_NOT_STARTED,
TASK_NEW_IN_PROGRESS,
TASK_NEW_COMPLETED,
TASK_TYPE_APPROVAL,
PLACEHOLDER_USER,
TASK_EDIT_MODE_EDIT,
} from '../../../../constants';
import type { TaskAssigneeCollection, TaskNew } from '../../../../common/types/tasks';
import { ACTIVITY_TARGETS } from '../../../common/interactionTargets';
import { bdlGray80 } from '../../../../styles/variables';
import TaskActions from './TaskActions';
import TaskCompletionRuleIcon from './TaskCompletionRuleIcon';
import TaskDueDate from './TaskDueDate';
import TaskStatus from './TaskStatus';
import AssigneeList from './AssigneeList';
import TaskModal from '../../TaskModal';
import TaskMultiFileIcon from './TaskMultiFileIcon';
import commonMessages from '../../../common/messages';
import messages from './messages';
import type { GetAvatarUrlCallback, GetProfileUrlCallback } from '../../../common/flowTypes';
import type { ElementsXhrError } from '../../../../common/types/api';
import type { SelectorItems, User } from '../../../../common/types/core';
import type { ActionItemError } from '../../../../common/types/feed';
import type { Translations } from '../../flowTypes';
import type { FeatureConfig } from '../../../common/feature-checking';
import './Task.scss';
type Props = {|
...TaskNew,
api: API,
approverSelectorContacts: SelectorItems<>,
currentUser: User,
error?: ActionItemError,
features?: FeatureConfig,
getApproverWithQuery?: Function,
getAvatarUrl: GetAvatarUrlCallback,
getMentionWithQuery?: Function,
getUserProfileUrl?: GetProfileUrlCallback,
isPending?: boolean,
onAssignmentUpdate: Function,
onDelete?: Function,
onEdit?: Function,
onModalClose?: Function,
onView?: Function,
translatedTaggedMessage?: string,
translations?: Translations,
|};
type State = {
// the complete list of assignees (when task.assigned_to is truncated)
assignedToFull: TaskAssigneeCollection,
isAssigneeListOpen: boolean,
isConfirmingDelete: boolean,
isEditing: boolean,
isLoading: boolean,
loadCollabError: ?ActionItemError,
modalError: ?ElementsXhrError,
};
class Task extends React.Component<Props, State> {
static defaultProps = {
completion_rule: TASK_COMPLETION_RULE_ALL,
};
state = {
loadCollabError: undefined,
assignedToFull: this.props.assigned_to,
modalError: undefined,
isEditing: false,
isLoading: false,
isAssigneeListOpen: false,
isConfirmingDelete: false,
};
handleAssigneeListExpand = () => {
this.getAllTaskCollaborators(() => {
this.setState({ isAssigneeListOpen: true });
});
};
handleAssigneeListCollapse = () => {
this.setState({ isAssigneeListOpen: false });
};
handleEditClick = () => {
this.getAllTaskCollaborators(() => {
this.setState({ isEditing: true });
});
};
handleDeleteClick = () => {
this.setState({ isConfirmingDelete: true });
};
handleDeleteConfirm = (): void => {
const { id, onDelete, permissions } = this.props;
if (onDelete) {
onDelete({ id, permissions });
}
};
handleDeleteCancel = (): void => {
this.setState({ isConfirmingDelete: false });
};
handleEditModalClose = () => {
const { onModalClose } = this.props;
this.setState({ isEditing: false, modalError: undefined });
if (onModalClose) {
onModalClose();
}
};
handleEditSubmitError = (error: ElementsXhrError) => {
this.setState({ modalError: error });
};
getAllTaskCollaborators = (onSuccess: () => any) => {
const { id, api, task_links, assigned_to } = this.props;
const { errorOccured } = commonMessages;
const { taskCollaboratorLoadErrorMessage } = messages;
// skip fetch when there are no additional collaborators
if (!assigned_to.next_marker) {
this.setState({ assignedToFull: assigned_to });
onSuccess();
return;
}
// fileid is required for api calls, check for presence
const fileId = get(task_links, 'entries[0].target.id');
if (!fileId) {
return;
}
this.setState({ isLoading: true });
api.getTaskCollaboratorsAPI(false).getTaskCollaborators({
task: { id },
file: { id: fileId },
errorCallback: () => {
this.setState({
isLoading: false,
loadCollabError: {
message: taskCollaboratorLoadErrorMessage,
title: errorOccured,
},
});
},
successCallback: assignedToFull => {
this.setState({ assignedToFull, isLoading: false });
onSuccess();
},
});
};
handleTaskAction = (taskId: string, assignmentId: string, taskStatus: string) => {
const { onAssignmentUpdate } = this.props;
this.setState({ isAssigneeListOpen: false });
onAssignmentUpdate(taskId, assignmentId, taskStatus);
};
render() {
const {
approverSelectorContacts,
assigned_to,
completion_rule,
created_at,
created_by,
currentUser,
due_at,
error,
features,
getApproverWithQuery,
getAvatarUrl,
getUserProfileUrl,
id,
isPending,
description,
onEdit,
onView,
permissions,
status,
task_links,
task_type,
translatedTaggedMessage,
translations,
} = this.props;
const {
assignedToFull,
modalError,
isEditing,
isLoading,
loadCollabError,
isAssigneeListOpen,
isConfirmingDelete,
} = this.state;
const inlineError = loadCollabError || error;
const assignments = assigned_to && assigned_to.entries;
const currentUserAssignment = assignments && assignments.find(({ target }) => target.id === currentUser.id);
const createdByUser = created_by.target || PLACEHOLDER_USER;
const createdAtTimestamp = new Date(created_at).getTime();
const isTaskCompleted = !(status === TASK_NEW_NOT_STARTED || status === TASK_NEW_IN_PROGRESS);
const isCreator = created_by.target.id === currentUser.id;
const isMultiFile = task_links.entries.length > 1;
let shouldShowActions;
if (isTaskCompleted) {
shouldShowActions = false;
} else if (isMultiFile && isCreator) {
shouldShowActions = true;
} else {
shouldShowActions =
currentUserAssignment &&
currentUserAssignment.permissions &&
currentUserAssignment.permissions.can_update &&
currentUserAssignment.status === TASK_NEW_NOT_STARTED;
}
const TaskTypeIcon = task_type === TASK_TYPE_APPROVAL ? IconTaskApproval : IconTaskGeneral;
const isMenuVisible = (permissions.can_delete || permissions.can_update) && !isPending;
return (
<ActivityCard
className="bcs-Task"
data-resin-feature="tasks"
data-resin-taskid={id}
data-resin-tasktype={task_type}
data-resin-numassignees={assignments && assignments.length}
>
{/* $FlowFixMe */}
{inlineError ? <ActivityError {...inlineError} /> : null}
<Media
className={classNames('bcs-Task-media', {
'bcs-is-pending': isPending || isLoading,
})}
data-testid="task-card"
>
<Media.Figure className="bcs-Task-avatar">
<Avatar getAvatarUrl={getAvatarUrl} user={createdByUser} />
<TaskTypeIcon width={20} height={20} className="bcs-Task-avatarBadge" />
</Media.Figure>
<Media.Body>
{isMenuVisible && (
<TetherComponent
attachment="top right"
className="bcs-Task-deleteConfirmationModal"
constraints={[{ to: 'scrollParent', attachment: 'together' }]}
targetAttachment="bottom right"
>
<Media.Menu
isDisabled={isConfirmingDelete}
data-testid="task-actions-menu"
menuProps={{
'data-resin-component': ACTIVITY_TARGETS.TASK_OPTIONS,
}}
>
{permissions.can_update && (
<MenuItem
data-resin-target={ACTIVITY_TARGETS.TASK_OPTIONS_EDIT}
data-testid="edit-task"
onClick={this.handleEditClick}
>
<IconPencil color={bdlGray80} />
<FormattedMessage {...messages.taskEditMenuItem} />
</MenuItem>
)}
{permissions.can_delete && (
<MenuItem
data-resin-target={ACTIVITY_TARGETS.TASK_OPTIONS_DELETE}
data-testid="delete-task"
onClick={this.handleDeleteClick}
>
<IconTrash color={bdlGray80} />
<FormattedMessage {...messages.taskDeleteMenuItem} />
</MenuItem>
)}
</Media.Menu>
{isConfirmingDelete && (
<DeleteConfirmation
data-resin-component={ACTIVITY_TARGETS.TASK_OPTIONS}
isOpen={isConfirmingDelete}
message={messages.taskDeletePrompt}
onDeleteCancel={this.handleDeleteCancel}
onDeleteConfirm={this.handleDeleteConfirm}
/>
)}
</TetherComponent>
)}
<div className="bcs-Task-headline">
<UserLink
{...createdByUser}
data-resin-target={ACTIVITY_TARGETS.PROFILE}
getUserProfileUrl={getUserProfileUrl}
/>
</div>
<div>
<ActivityTimestamp date={createdAtTimestamp} />
</div>
<div className="bcs-Task-status">
<TaskStatus status={status} />
<TaskMultiFileIcon isMultiFile={isMultiFile} />
<TaskCompletionRuleIcon completionRule={completion_rule} />
</div>
<div className="bcs-Task-dueDate">
{!!due_at && <TaskDueDate dueDate={due_at} status={status} />}
</div>
<div>
<ActivityMessage
id={id}
tagged_message={description}
translatedTaggedMessage={translatedTaggedMessage}
{...translations}
translationFailed={error ? true : null}
getUserProfileUrl={getUserProfileUrl}
/>
</div>
<div className="bcs-Task-assigneeListContainer">
<AssigneeList
isOpen={isAssigneeListOpen}
onCollapse={this.handleAssigneeListCollapse}
onExpand={this.handleAssigneeListExpand}
getAvatarUrl={getAvatarUrl}
initialAssigneeCount={3}
users={isAssigneeListOpen ? assignedToFull : assigned_to}
/>
</div>
{shouldShowActions && (
<div className="bcs-Task-actionsContainer" data-testid="action-container">
<TaskActions
isMultiFile={isMultiFile}
taskType={task_type}
onTaskApproval={
isPending
? noop
: () =>
// $FlowFixMe checked by shouldShowActions
this.handleTaskAction(id, currentUserAssignment.id, TASK_NEW_APPROVED)
}
onTaskReject={
isPending
? noop
: () =>
// $FlowFixMe checked by shouldShowActions
this.handleTaskAction(id, currentUserAssignment.id, TASK_NEW_REJECTED)
}
onTaskComplete={
isPending
? noop
: () =>
this.handleTaskAction(
id,
// $FlowFixMe checked by shouldShowActions
currentUserAssignment.id,
TASK_NEW_COMPLETED,
)
}
onTaskView={onView && (() => onView(id, isCreator))}
/>
</div>
)}
</Media.Body>
</Media>
<TaskModal
editMode={TASK_EDIT_MODE_EDIT}
error={modalError}
feedbackUrl={getFeatureConfig(features, 'activityFeed.tasks').feedbackUrl || ''}
onSubmitError={this.handleEditSubmitError}
onSubmitSuccess={this.handleEditModalClose}
onModalClose={this.handleEditModalClose}
isTaskFormOpen={isEditing}
taskFormProps={{
id,
approvers: assignedToFull.entries,
approverSelectorContacts,
completionRule: completion_rule,
getApproverWithQuery,
getAvatarUrl,
createTask: () => {},
editTask: onEdit,
dueDate: due_at,
message: description,
}}
taskType={task_type}
/>
</ActivityCard>
);
}
}
export { Task as TaskComponent };
export default flow([withFeatureConsumer, withAPIContext])(Task);