From 5c3afaba7c9711b4556b28e9d415521ab7a8bb5d Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Fri, 19 Jun 2020 16:33:02 -0500 Subject: [PATCH] change: redesign task details modal --- web/src/Projects/Project/Details/index.tsx | 7 +- web/src/shared/components/AddList/Styles.ts | 10 +- web/src/shared/components/AddList/index.tsx | 3 +- web/src/shared/components/Button/index.tsx | 40 +++- .../shared/components/TaskAssignee/index.tsx | 1 + .../shared/components/TaskDetails/Styles.ts | 98 ++++++-- .../TaskDetails/TaskDetails.stories.tsx | 1 + .../shared/components/TaskDetails/index.tsx | 214 ++++++++++-------- web/src/shared/generated/graphql.tsx | 15 +- web/src/shared/graphql/findTask.graphqls | 1 + 10 files changed, 263 insertions(+), 127 deletions(-) diff --git a/web/src/Projects/Project/Details/index.tsx b/web/src/Projects/Project/Details/index.tsx index 2d025e2..90f0b44 100644 --- a/web/src/Projects/Project/Details/index.tsx +++ b/web/src/Projects/Project/Details/index.tsx @@ -7,6 +7,7 @@ import { useRouteMatch, useHistory } from 'react-router'; import { useFindTaskQuery, useUpdateTaskDueDateMutation, + useSetTaskCompleteMutation, useAssignTaskMutation, useUnassignTaskMutation, useSetTaskChecklistItemCompleteMutation, @@ -108,6 +109,7 @@ const Details: React.FC = ({ }, }); const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } }); + const [setTaskComplete] = useSetTaskCompleteMutation(); const [updateTaskDueDate] = useUpdateTaskDueDateMutation({ onCompleted: () => { refetch(); @@ -136,7 +138,7 @@ const Details: React.FC = ({ return ( <> { history.push(projectURL); }} @@ -146,6 +148,9 @@ const Details: React.FC = ({ task={data.findTask} onTaskNameChange={onTaskNameChange} onTaskDescriptionChange={onTaskDescriptionChange} + onToggleTaskComplete={task => { + setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } }); + }} onDeleteTask={onDeleteTask} onChangeItemName={(itemID, itemName) => { updateTaskChecklistItemName({ variables: { taskChecklistItemID: itemID, name: itemName } }); diff --git a/web/src/shared/components/AddList/Styles.ts b/web/src/shared/components/AddList/Styles.ts index e29da6e..708de3f 100644 --- a/web/src/shared/components/AddList/Styles.ts +++ b/web/src/shared/components/AddList/Styles.ts @@ -46,6 +46,10 @@ export const Wrapper = styled.div<{ editorOpen: boolean }>` `} `; +export const AddListButton = styled(Button)` + padding: 6px 12px; +`; + export const Placeholder = styled.span` color: #c2c6dc; display: flex; @@ -99,12 +103,6 @@ export const ListAddControls = styled.div` margin: 4px 0 0; `; -export const AddListButton = styled(Button)` - float: left; - padding: 6px 12px; - border-radius: 3px; -`; - export const CancelAdd = styled.div` display: flex; align-items: center; diff --git a/web/src/shared/components/AddList/index.tsx b/web/src/shared/components/AddList/index.tsx index 999991a..0c822b2 100644 --- a/web/src/shared/components/AddList/index.tsx +++ b/web/src/shared/components/AddList/index.tsx @@ -1,16 +1,17 @@ import React, { useState, useRef, useEffect } from 'react'; import { Plus, Cross } from 'shared/icons'; import useOnOutsideClick from 'shared/hooks/onOutsideClick'; +import Button from 'shared/components/Button'; import { Container, Wrapper, Placeholder, AddIconWrapper, + AddListButton, ListNameEditor, ListAddControls, CancelAdd, - AddListButton, ListNameEditorWrapper, } from './Styles'; diff --git a/web/src/shared/components/Button/index.tsx b/web/src/shared/components/Button/index.tsx index 62a528d..ba500a7 100644 --- a/web/src/shared/components/Button/index.tsx +++ b/web/src/shared/components/Button/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import styled, { css } from 'styled-components/macro'; const Text = styled.span<{ fontSize: string }>` @@ -109,7 +109,7 @@ type ButtonProps = { disabled?: boolean; type?: 'button' | 'submit'; className?: string; - onClick?: () => void; + onClick?: ($target: React.RefObject) => void; }; const Button: React.FC = ({ @@ -122,46 +122,68 @@ const Button: React.FC = ({ className, children, }) => { + const $button = useRef(null); const handleClick = () => { if (onClick) { - onClick(); + onClick($button); } }; switch (variant) { case 'filled': return ( - + {children} ); case 'outline': return ( - + {children} ); case 'flat': return ( - + {children} ); case 'lineDown': return ( - + {children} ); case 'gradient': return ( - + {children} ); case 'relief': return ( - + {children} ); diff --git a/web/src/shared/components/TaskAssignee/index.tsx b/web/src/shared/components/TaskAssignee/index.tsx index 2c82d77..9dc2742 100644 --- a/web/src/shared/components/TaskAssignee/index.tsx +++ b/web/src/shared/components/TaskAssignee/index.tsx @@ -6,6 +6,7 @@ const TaskDetailAssignee = styled.div` opacity: 0.8; } margin-right: 4px; + float: left; `; export const Wrapper = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>` diff --git a/web/src/shared/components/TaskDetails/Styles.ts b/web/src/shared/components/TaskDetails/Styles.ts index d127367..e17acc1 100644 --- a/web/src/shared/components/TaskDetails/Styles.ts +++ b/web/src/shared/components/TaskDetails/Styles.ts @@ -49,16 +49,17 @@ export const TaskAction = styled.button` export const TaskDetailsWrapper = styled.div` display: flex; - padding: 0px 30px 60px; + padding: 0px 16px 60px; `; export const TaskDetailsContent = styled.div` - width: 65%; - padding-right: 50px; + flex: 1; + padding-right: 8px; `; export const TaskDetailsSidebar = styled.div` - width: 35%; + width: 168px; + padding-left: 8px; `; export const TaskDetailsTitleWrapper = styled.div` @@ -235,7 +236,13 @@ export const ProfileIcon = styled.div` cursor: pointer; `; -export const TaskDetailsAddMember = styled.div` +export const TaskDetailsAddMemberIcon = styled.div` + float: left; + height: 32px; + width: 32px; + display: flex; + align-items: center; + justify-content: center; border-radius: 100%; background: ${mixin.darken('#262c49', 0.15)}; cursor: pointer; @@ -244,14 +251,6 @@ export const TaskDetailsAddMember = styled.div` } `; -export const TaskDetailsAddMemberIcon = styled.div` - height: 32px; - width: 32px; - display: flex; - align-items: center; - justify-content: center; -`; - export const TaskDetailLabels = styled.div` display: flex; align-items: center; @@ -292,11 +291,18 @@ export const TaskDetailsAddLabel = styled.div` `; export const TaskDetailsAddLabelIcon = styled.div` + float: left; height: 32px; width: 32px; display: flex; align-items: center; justify-content: center; + border-radius: 3px; + background: ${mixin.darken('#262c49', 0.15)}; + cursor: pointer; + &:hover { + opacity: 0.8; + } `; export const NoDueDateLabel = styled.span` @@ -313,3 +319,69 @@ export const UnassignedLabel = styled.div` align-items: center; height: 32px; `; + +export const ActionButtons = styled.div` + display: flex; + flex-direction: column; +`; + +export const ActionButtonsTitle = styled.h3` + color: rgba(${props => props.theme.colors.text.primary}); + font-size: 12px; + font-weight: 500; + letter-spacing: 0.04em; +`; + +export const ActionButton = styled(Button)` + margin-top: 8px; + padding: 6px 12px; + background: rgba(${props => props.theme.colors.bg.primary}, 0.4); + text-align: left; + &:hover { + box-shadow: none; + background: rgba(${props => props.theme.colors.bg.primary}, 0.6); + } +`; + +export const MetaDetails = styled.div` + margin-top: 8px; + display: flex; +`; + +export const TaskDueDateButton = styled(Button)` + height: 32px; + padding: 6px 12px; + background: rgba(${props => props.theme.colors.bg.primary}, 0.4); + &:hover { + box-shadow: none; + background: rgba(${props => props.theme.colors.bg.primary}, 0.6); + } +`; + +export const MetaDetail = styled.div` + display: block; + float: left; + margin: 0 16px 8px 0; + max-width: 100%; +`; + +export const TaskDetailsSection = styled.div` + display: block; +`; + +export const MetaDetailTitle = styled.h3` + color: rgba(${props => props.theme.colors.text.primary}); + font-size: 12px; + font-weight: 500; + letter-spacing: 0.04em; + margin-top: 16px; + text-transform: uppercase; + display: block; + line-height: 20px; + margin: 0 8px 4px 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +`; + +export const MetaDetailContent = styled.div``; diff --git a/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx index 034ad5e..ddccaff 100644 --- a/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx +++ b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx @@ -68,6 +68,7 @@ export const Default = () => { onMemberProfile={action('profile')} onOpenAddMemberPopup={action('open add member popup')} onAddItem={action('add item')} + onToggleTaskComplete={action('toggle task complete')} onToggleChecklistItem={action('toggle checklist item')} onOpenAddLabelPopup={action('open add label popup')} onOpenDueDatePopop={action('open due date popup')} diff --git a/web/src/shared/components/TaskDetails/index.tsx b/web/src/shared/components/TaskDetails/index.tsx index 81844db..0be4433 100644 --- a/web/src/shared/components/TaskDetails/index.tsx +++ b/web/src/shared/components/TaskDetails/index.tsx @@ -7,15 +7,19 @@ import moment from 'moment'; import { NoDueDateLabel, + TaskDueDateButton, UnassignedLabel, - TaskDetailsAddMember, TaskGroupLabel, TaskGroupLabelName, + TaskDetailsSection, TaskActions, TaskDetailsAddLabel, TaskDetailsAddLabelIcon, TaskAction, TaskMeta, + ActionButtons, + ActionButton, + ActionButtonsTitle, TaskHeader, ProfileIcon, TaskDetailsContent, @@ -37,6 +41,10 @@ import { TaskDetailAssignee, TaskDetailAssignees, TaskDetailsAddMemberIcon, + MetaDetails, + MetaDetail, + MetaDetailTitle, + MetaDetailContent, } from './Styles'; import Checklist from '../Checklist'; @@ -127,6 +135,7 @@ type TaskDetailsProps = { onAddItem: (checklistID: string, name: string, position: number) => void; onDeleteItem: (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; @@ -138,6 +147,7 @@ type TaskDetailsProps = { const TaskDetails: React.FC = ({ task, onTaskNameChange, + onToggleTaskComplete, onTaskDescriptionChange, onChangeItemName, onDeleteItem, @@ -170,13 +180,14 @@ const TaskDetails: React.FC = ({ const onUnassignedClick = () => { onOpenAddMemberPopup(task, $unassignedRef); }; - const onAddMember = () => { - onOpenAddMemberPopup(task, $addMemberRef); + const onAddMember = ($target: React.RefObject) => { + onOpenAddMemberPopup(task, $target); }; const $dueDateLabel = useRef(null); const $addLabelRef = useRef(null); - const onAddLabel = () => { - onOpenAddLabelPopup(task, $addLabelRef); + + const onAddLabel = ($target: React.RefObject) => { + onOpenAddLabelPopup(task, $target); }; return ( <> @@ -206,100 +217,111 @@ const TaskDetails: React.FC = ({ - Description - {editorOpen ? ( - { - setEditorOpen(false); - setDescription(newDescription); - onTaskDescriptionChange(task, newDescription); - }} - onCancel={() => { - setEditorOpen(false); - }} - /> - ) : ( - - )} - {task.checklists && - task.checklists - .slice() - .sort((a, b) => a.position - b.position) - .map(checklist => ( - {}} - onChangeName={() => {}} - onToggleItem={onToggleChecklistItem} - onDeleteItem={onDeleteItem} - onAddItem={n => { - if (task.checklists) { - let position = 1; - const lastChecklist = task.checklists.sort((a, b) => a.position - b.position)[-1]; - if (lastChecklist) { - position = lastChecklist.position * 2 + 1; - } - onAddItem(checklist.id, n, position); - } - }} - onChangeItemName={onChangeItemName} - /> - ))} - - - Assignees - - {task.assigned && task.assigned.length === 0 ? ( - - Unassigned - - ) : ( - <> - {task.assigned && - task.assigned.map(member => ( - - ))} - - + + {task.assigned && task.assigned.length !== 0 && ( + + MEMBERS + + {task.assigned && + task.assigned.map(member => ( + + ))} + onAddMember($addMemberRef)}> - - + + )} - - Labels - - {task.labels.map(label => { - return ( - { - onOpenAddLabelPopup(task, $target); - }} - /> - ); - })} - - - - - - - Due Date - {task.dueDate ? ( - onOpenDueDatePopop(task, $dueDateLabel)}> - {moment(task.dueDate).format('MMM D [at] h:mm A')} - - ) : ( - onOpenDueDatePopop(task, $dueDateLabel)}> - No due date - - )} + {task.labels.length !== 0 && ( + + LABELS + + {task.labels.map(label => { + return ( + { + onOpenAddLabelPopup(task, $target); + }} + /> + ); + })} + onAddLabel($addLabelRef)}> + + + + + )} + {task.dueDate && ( + + DUE DATE + + {moment(task.dueDate).format('MMM D [at] h:mm A')} + + + )} + + + + Description + {editorOpen ? ( + { + setEditorOpen(false); + setDescription(newDescription); + onTaskDescriptionChange(task, newDescription); + }} + onCancel={() => { + setEditorOpen(false); + }} + /> + ) : ( + + )} + {task.checklists && + task.checklists + .slice() + .sort((a, b) => a.position - b.position) + .map(checklist => ( + {}} + onChangeName={() => {}} + onToggleItem={onToggleChecklistItem} + onDeleteItem={onDeleteItem} + onAddItem={n => { + if (task.checklists) { + let position = 1; + const lastChecklist = task.checklists.sort((a, b) => a.position - b.position)[-1]; + if (lastChecklist) { + position = lastChecklist.position * 2 + 1; + } + onAddItem(checklist.id, n, position); + } + }} + onChangeItemName={onChangeItemName} + /> + ))} + + + + + ADD TO CARD + onToggleTaskComplete(task)}> + {task.complete ? 'Mark Incomplete' : 'Mark Complete'} + + onAddMember($target)}>Members + onAddLabel($target)}>Labels + Checklist + onOpenDueDatePopop(task, $target)}>Due Date + Attachment + Cover + diff --git a/web/src/shared/generated/graphql.tsx b/web/src/shared/generated/graphql.tsx index a8b043f..21d35b4 100644 --- a/web/src/shared/generated/graphql.tsx +++ b/web/src/shared/generated/graphql.tsx @@ -102,6 +102,17 @@ export type TaskGroup = { tasks: Array; }; +export type ChecklistBadge = { + __typename?: 'ChecklistBadge'; + complete: Scalars['Int']; + total: Scalars['Int']; +}; + +export type TaskBadges = { + __typename?: 'TaskBadges'; + checklist?: Maybe; +}; + export type Task = { __typename?: 'Task'; id: Scalars['ID']; @@ -115,6 +126,7 @@ export type Task = { assigned: Array; labels: Array; checklists: Array; + badges: TaskBadges; }; export type ProjectsFilter = { @@ -792,7 +804,7 @@ export type FindTaskQuery = ( { __typename?: 'Query' } & { findTask: ( { __typename?: 'Task' } - & Pick + & Pick & { taskGroup: ( { __typename?: 'TaskGroup' } & Pick @@ -1607,6 +1619,7 @@ export const FindTaskDocument = gql` description dueDate position + complete taskGroup { id } diff --git a/web/src/shared/graphql/findTask.graphqls b/web/src/shared/graphql/findTask.graphqls index ba056ec..577ab0c 100644 --- a/web/src/shared/graphql/findTask.graphqls +++ b/web/src/shared/graphql/findTask.graphqls @@ -5,6 +5,7 @@ query findTask($taskID: UUID!) { description dueDate position + complete taskGroup { id }