import React, { useState, useRef } from 'react'; import { useCurrentUser } from 'App/context'; import { Plus, User, Trash, Paperclip, Clone, Share, Tags, Checkmark, CheckSquareOutline, At, Smile, } from 'shared/icons'; import { toArray } from 'react-emoji-render'; import DOMPurify from 'dompurify'; import TaskAssignee from 'shared/components/TaskAssignee'; import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import { usePopup } from 'shared/components/PopupMenu'; import CommentCreator from 'shared/components/TaskDetails/CommentCreator'; import { AngleDown } from 'shared/icons/AngleDown'; import Editor from 'rich-markdown-editor'; import dark from 'shared/utils/editorTheme'; import styled from 'styled-components'; import ReactMarkdown from 'react-markdown'; import { Picker, Emoji } from 'emoji-mart'; import 'emoji-mart/css/emoji-mart.css'; import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import dayjs from 'dayjs'; import Task from 'shared/icons/Task'; import { ActivityItemHeader, ActivityItemTimestamp, ActivityItem, ActivityItemCommentAction, ActivityItemCommentActions, TaskDetailLabel, CommentContainer, ActivityItemCommentContainer, MetaDetailContent, TaskDetailsAddLabelIcon, ActionButton, AssignUserIcon, AssignUserLabel, AssignUsersButton, AssignedUsersSection, ViewRawButton, DueDateTitle, Container, LeftSidebar, ContentContainer, LeftSidebarContent, LeftSidebarSection, SidebarTitle, SidebarButton, SidebarButtonText, MarkCompleteButton, HeaderContainer, HeaderLeft, HeaderInnerContainer, TaskDetailsTitleWrapper, TaskDetailsTitle, ExtraActionsSection, HeaderRight, HeaderActionIcon, EditorContainer, InnerContentContainer, DescriptionContainer, Labels, ChecklistSection, MemberList, TaskMember, TabBarSection, TabBarItem, ActivitySection, TaskDetailsEditor, ActivityItemHeaderUser, ActivityItemHeaderTitle, ActivityItemHeaderTitleName, ActivityItemComment, } from './Styles'; import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist'; import onDragEnd from './onDragEnd'; import plugin from './remark'; import ActivityMessage from './ActivityMessage'; const parseEmojis = (value: string) => { const emojisArray = toArray(value); // toArray outputs React elements for emojis and strings for other const newValue = emojisArray.reduce((previous: any, current: any) => { if (typeof current === 'string') { return previous + current; } return previous + current.props.children; }, ''); return newValue; }; type StreamCommentProps = { comment?: TaskComment | null; onUpdateComment: (message: string) => void; onExtraActions: (commentID: string, $target: React.RefObject) => void; onCancelCommentEdit: () => void; editable: boolean; }; const StreamComment: React.FC = ({ comment, onExtraActions, editable, onUpdateComment, onCancelCommentEdit, }) => { const $actions = useRef(null); if (comment) { return ( {comment.createdBy.fullName} {dayjs(comment.createdAt).format('MMM D [at] h:mm A')} {comment.updatedAt && ' (edited)'} {editable ? ( ) : ( {DOMPurify.sanitize(comment.message, { FORBID_TAGS: ['style', 'img'] })} )} { onExtraActions(comment.id, $actions); }} > ); } return null; }; type StreamActivityProps = { activity?: TaskActivity | null; }; const StreamActivity: React.FC = ({ activity }) => { if (activity) { return ( {activity.causedBy.fullName} {dayjs(activity.createdAt).format('MMM D [at] h:mm A')} ); } return null; }; const ChecklistContainer = styled.div``; type TaskLabelProps = { label: TaskLabel; onClick: ($target: React.RefObject) => void; }; const TaskLabelItem: React.FC = ({ label, onClick }) => { const $label = useRef(null); return ( { onClick($label); }} ref={$label} color={label.projectLabel.labelColor.colorHex} > {label.projectLabel.name} ); }; type DetailsEditorProps = { description: string; onTaskDescriptionChange: (newDescription: string) => void; onCancel: () => void; }; type TaskDetailsProps = { task: Task; me?: TaskUser | null; onTaskNameChange: (task: Task, newName: string) => void; onTaskDescriptionChange: (task: Task, newDescription: string) => void; onDeleteTask: (task: Task) => void; onAddItem: (checklistID: string, name: string, position: number) => void; onDeleteItem: (checklistID: string, itemID: string) => void; onChangeItemName: (itemID: string, itemName: string) => void; onToggleTaskComplete: (task: Task) => void; onToggleChecklistItem: (itemID: string, complete: boolean) => void; onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject) => void; onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject) => void; onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject) => void; onOpenAddChecklistPopup: (task: Task, $targetRef: React.RefObject) => void; onCreateComment: (task: Task, message: string) => void; onCommentShowActions: (commentID: string, $targetRef: React.RefObject) => void; onMemberProfile: ($targetRef: React.RefObject, memberID: string) => void; onCancelCommentEdit: () => void; onUpdateComment: (commentID: string, message: string) => void; onChangeChecklistName: (checklistID: string, name: string) => void; editableComment?: string | null; onDeleteChecklist: ($target: React.RefObject, checklistID: string) => void; onCloseModal: () => void; onChecklistDrop: (checklist: TaskChecklist) => void; onChecklistItemDrop: (prevChecklistID: string, checklistID: string, checklistItem: TaskChecklistItem) => void; }; const TaskDetails: React.FC = ({ me, onCancelCommentEdit, task, editableComment = null, onDeleteChecklist, onTaskNameChange, onCommentShowActions, onOpenAddChecklistPopup, onChangeChecklistName, onCreateComment, onChecklistDrop, onChecklistItemDrop, onToggleTaskComplete, onTaskDescriptionChange, onChangeItemName, onDeleteItem, onDeleteTask, onCloseModal, onUpdateComment, onOpenAddMemberPopup, onOpenAddLabelPopup, onOpenDueDatePopop, onAddItem, onToggleChecklistItem, onMemberProfile, }) => { const { user } = useCurrentUser(); const [taskName, setTaskName] = useState(task.name); const [editTaskDescription, setEditTaskDescription] = useState(() => { if (task.description) { if (task.description.trim() === '' || task.description.trim() === '\\') { return true; } return false; } return true; }); const [saveTimeout, setSaveTimeout] = useState(null); const [showRaw, setShowRaw] = useState(false); const taskDescriptionRef = useRef(task.description ?? ''); const $noMemberBtn = useRef(null); const $addMemberBtn = useRef(null); const $dueDateBtn = useRef(null); const $detailsTitle = useRef(null); const activityStream: Array<{ id: string; data: { time: string; type: 'comment' | 'activity' } }> = []; if (task.activity) { task.activity.forEach((activity) => { activityStream.push({ id: activity.id, data: { time: activity.createdAt, type: 'activity', }, }); }); } if (task.comments) { task.comments.forEach((comment) => { activityStream.push({ id: comment.id, data: { time: comment.createdAt, type: 'comment', }, }); }); } activityStream.sort((a, b) => (dayjs(a.data.time).isAfter(dayjs(b.data.time)) ? 1 : -1)); const saveDescription = () => { onTaskDescriptionChange(task, taskDescriptionRef.current); }; return ( TASK GROUP {task.taskGroup.name} DUE DATE { if (user) { onOpenDueDatePopop(task, $dueDateBtn); } }} > {task.dueDate ? ( {dayjs(task.dueDate).format(task.hasTime ? 'MMM D [at] h:mm A' : 'MMMM D')} ) : ( No due date )} MEMBERS {task.assigned && task.assigned.length !== 0 ? ( {task.assigned.map((m) => ( { if (user) { onMemberProfile($target, m.id); } }} /> ))} { if (user) { onOpenAddMemberPopup(task, $addMemberBtn); } }} > ) : ( { if (user) { onOpenAddMemberPopup(task, $noMemberBtn); } }} > No members )} {user && ( ACTIONS { onOpenAddLabelPopup(task, $target); }} icon={} > Labels { onOpenAddChecklistPopup(task, $target); }} icon={} > Checklist Cover )} { if (user) { onToggleTaskComplete(task); } }} > {task.complete ? 'Completed' : 'Mark complete'} {user && ( onDeleteTask(task)}> )} { if (e.keyCode === 13) { e.preventDefault(); if ($detailsTitle && $detailsTitle.current) { $detailsTitle.current.blur(); } } }} onChange={(e) => { setTaskName(e.currentTarget.value); }} onBlur={() => { if (taskName !== task.name) { onTaskNameChange(task, taskName); } }} /> {task.labels.length !== 0 && ( {task.labels.map((label) => { return ( { onOpenAddLabelPopup(task, $target); }} /> ); })} )} {showRaw ? ( ) : ( { if (!editTaskDescription) { setEditTaskDescription(true); } }} > { setSaveTimeout(() => { clearTimeout(saveTimeout); return setTimeout(saveDescription, 2000); }); const text = value(); taskDescriptionRef.current = text; }} /> )} setShowRaw(!showRaw)}>{showRaw ? 'Show editor' : 'Show raw'} onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}> {(dropProvided) => ( {task.checklists && task.checklists .slice() .sort((a, b) => a.position - b.position) .map((checklist, idx) => ( {(provided) => ( onChangeChecklistName(checklist.id, newName)} onToggleItem={onToggleChecklistItem} onDeleteItem={onDeleteItem} onAddItem={(n) => { if (task.checklists) { let position = 65535; const [lastItem] = checklist.items .sort((a, b) => a.position - b.position) .slice(-1); if (lastItem) { position = lastItem.position * 2 + 1; } onAddItem(checklist.id, n, position); } }} onChangeItemName={onChangeItemName} > {(checklistDrop) => ( <> {checklist.items .slice() .sort((a, b) => a.position - b.position) .map((item, itemIdx) => ( {(itemDrop) => ( { onToggleChecklistItem(item.id, complete); }} /> )} ))} {checklistDrop.placeholder} )} )} ))} {dropProvided.placeholder} )} Activity {activityStream.map((stream) => stream.data.type === 'comment' ? ( onUpdateComment(stream.id, message)} editable={stream.id === editableComment} comment={task.comments && task.comments.find((comment) => comment.id === stream.id)} /> ) : ( activity.id === stream.id)} /> ), )} {me && ( onCreateComment(task, message)} onMemberProfile={onMemberProfile} /> )} ); }; export default TaskDetails;