change: redesign task details modal

This commit is contained in:
Jordan Knott 2020-06-19 16:33:02 -05:00
parent 9d6c67f791
commit 5c3afaba7c
10 changed files with 263 additions and 127 deletions

View File

@ -7,6 +7,7 @@ import { useRouteMatch, useHistory } from 'react-router';
import { import {
useFindTaskQuery, useFindTaskQuery,
useUpdateTaskDueDateMutation, useUpdateTaskDueDateMutation,
useSetTaskCompleteMutation,
useAssignTaskMutation, useAssignTaskMutation,
useUnassignTaskMutation, useUnassignTaskMutation,
useSetTaskChecklistItemCompleteMutation, useSetTaskChecklistItemCompleteMutation,
@ -108,6 +109,7 @@ const Details: React.FC<DetailsProps> = ({
}, },
}); });
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } }); const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const [setTaskComplete] = useSetTaskCompleteMutation();
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({ const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => { onCompleted: () => {
refetch(); refetch();
@ -136,7 +138,7 @@ const Details: React.FC<DetailsProps> = ({
return ( return (
<> <>
<Modal <Modal
width={1040} width={768}
onClose={() => { onClose={() => {
history.push(projectURL); history.push(projectURL);
}} }}
@ -146,6 +148,9 @@ const Details: React.FC<DetailsProps> = ({
task={data.findTask} task={data.findTask}
onTaskNameChange={onTaskNameChange} onTaskNameChange={onTaskNameChange}
onTaskDescriptionChange={onTaskDescriptionChange} onTaskDescriptionChange={onTaskDescriptionChange}
onToggleTaskComplete={task => {
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
}}
onDeleteTask={onDeleteTask} onDeleteTask={onDeleteTask}
onChangeItemName={(itemID, itemName) => { onChangeItemName={(itemID, itemName) => {
updateTaskChecklistItemName({ variables: { taskChecklistItemID: itemID, name: itemName } }); updateTaskChecklistItemName({ variables: { taskChecklistItemID: itemID, name: itemName } });

View File

@ -46,6 +46,10 @@ export const Wrapper = styled.div<{ editorOpen: boolean }>`
`} `}
`; `;
export const AddListButton = styled(Button)`
padding: 6px 12px;
`;
export const Placeholder = styled.span` export const Placeholder = styled.span`
color: #c2c6dc; color: #c2c6dc;
display: flex; display: flex;
@ -99,12 +103,6 @@ export const ListAddControls = styled.div`
margin: 4px 0 0; margin: 4px 0 0;
`; `;
export const AddListButton = styled(Button)`
float: left;
padding: 6px 12px;
border-radius: 3px;
`;
export const CancelAdd = styled.div` export const CancelAdd = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -1,16 +1,17 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { Plus, Cross } from 'shared/icons'; import { Plus, Cross } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import Button from 'shared/components/Button';
import { import {
Container, Container,
Wrapper, Wrapper,
Placeholder, Placeholder,
AddIconWrapper, AddIconWrapper,
AddListButton,
ListNameEditor, ListNameEditor,
ListAddControls, ListAddControls,
CancelAdd, CancelAdd,
AddListButton,
ListNameEditorWrapper, ListNameEditorWrapper,
} from './Styles'; } from './Styles';

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useRef } from 'react';
import styled, { css } from 'styled-components/macro'; import styled, { css } from 'styled-components/macro';
const Text = styled.span<{ fontSize: string }>` const Text = styled.span<{ fontSize: string }>`
@ -109,7 +109,7 @@ type ButtonProps = {
disabled?: boolean; disabled?: boolean;
type?: 'button' | 'submit'; type?: 'button' | 'submit';
className?: string; className?: string;
onClick?: () => void; onClick?: ($target: React.RefObject<HTMLButtonElement>) => void;
}; };
const Button: React.FC<ButtonProps> = ({ const Button: React.FC<ButtonProps> = ({
@ -122,46 +122,68 @@ const Button: React.FC<ButtonProps> = ({
className, className,
children, children,
}) => { }) => {
const $button = useRef<HTMLButtonElement>(null);
const handleClick = () => { const handleClick = () => {
if (onClick) { if (onClick) {
onClick(); onClick($button);
} }
}; };
switch (variant) { switch (variant) {
case 'filled': case 'filled':
return ( return (
<Filled type={type} onClick={handleClick} className={className} disabled={disabled} color={color}> <Filled ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
</Filled> </Filled>
); );
case 'outline': case 'outline':
return ( return (
<Outline type={type} onClick={handleClick} className={className} disabled={disabled} color={color}> <Outline
ref={$button}
type={type}
onClick={handleClick}
className={className}
disabled={disabled}
color={color}
>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
</Outline> </Outline>
); );
case 'flat': case 'flat':
return ( return (
<Flat type={type} onClick={handleClick} className={className} disabled={disabled} color={color}> <Flat ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
</Flat> </Flat>
); );
case 'lineDown': case 'lineDown':
return ( return (
<LineDown type={type} onClick={handleClick} className={className} disabled={disabled} color={color}> <LineDown
ref={$button}
type={type}
onClick={handleClick}
className={className}
disabled={disabled}
color={color}
>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
<LineX color={color} /> <LineX color={color} />
</LineDown> </LineDown>
); );
case 'gradient': case 'gradient':
return ( return (
<Gradient type={type} onClick={handleClick} className={className} disabled={disabled} color={color}> <Gradient
ref={$button}
type={type}
onClick={handleClick}
className={className}
disabled={disabled}
color={color}
>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
</Gradient> </Gradient>
); );
case 'relief': case 'relief':
return ( return (
<Relief type={type} onClick={handleClick} className={className} disabled={disabled} color={color}> <Relief ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
</Relief> </Relief>
); );

View File

@ -6,6 +6,7 @@ const TaskDetailAssignee = styled.div`
opacity: 0.8; opacity: 0.8;
} }
margin-right: 4px; margin-right: 4px;
float: left;
`; `;
export const Wrapper = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>` export const Wrapper = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`

View File

@ -49,16 +49,17 @@ export const TaskAction = styled.button`
export const TaskDetailsWrapper = styled.div` export const TaskDetailsWrapper = styled.div`
display: flex; display: flex;
padding: 0px 30px 60px; padding: 0px 16px 60px;
`; `;
export const TaskDetailsContent = styled.div` export const TaskDetailsContent = styled.div`
width: 65%; flex: 1;
padding-right: 50px; padding-right: 8px;
`; `;
export const TaskDetailsSidebar = styled.div` export const TaskDetailsSidebar = styled.div`
width: 35%; width: 168px;
padding-left: 8px;
`; `;
export const TaskDetailsTitleWrapper = styled.div` export const TaskDetailsTitleWrapper = styled.div`
@ -235,7 +236,13 @@ export const ProfileIcon = styled.div`
cursor: pointer; 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%; border-radius: 100%;
background: ${mixin.darken('#262c49', 0.15)}; background: ${mixin.darken('#262c49', 0.15)};
cursor: pointer; 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` export const TaskDetailLabels = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
@ -292,11 +291,18 @@ export const TaskDetailsAddLabel = styled.div`
`; `;
export const TaskDetailsAddLabelIcon = styled.div` export const TaskDetailsAddLabelIcon = styled.div`
float: left;
height: 32px; height: 32px;
width: 32px; width: 32px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 3px;
background: ${mixin.darken('#262c49', 0.15)};
cursor: pointer;
&:hover {
opacity: 0.8;
}
`; `;
export const NoDueDateLabel = styled.span` export const NoDueDateLabel = styled.span`
@ -313,3 +319,69 @@ export const UnassignedLabel = styled.div`
align-items: center; align-items: center;
height: 32px; 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``;

View File

@ -68,6 +68,7 @@ export const Default = () => {
onMemberProfile={action('profile')} onMemberProfile={action('profile')}
onOpenAddMemberPopup={action('open add member popup')} onOpenAddMemberPopup={action('open add member popup')}
onAddItem={action('add item')} onAddItem={action('add item')}
onToggleTaskComplete={action('toggle task complete')}
onToggleChecklistItem={action('toggle checklist item')} onToggleChecklistItem={action('toggle checklist item')}
onOpenAddLabelPopup={action('open add label popup')} onOpenAddLabelPopup={action('open add label popup')}
onOpenDueDatePopop={action('open due date popup')} onOpenDueDatePopop={action('open due date popup')}

View File

@ -7,15 +7,19 @@ import moment from 'moment';
import { import {
NoDueDateLabel, NoDueDateLabel,
TaskDueDateButton,
UnassignedLabel, UnassignedLabel,
TaskDetailsAddMember,
TaskGroupLabel, TaskGroupLabel,
TaskGroupLabelName, TaskGroupLabelName,
TaskDetailsSection,
TaskActions, TaskActions,
TaskDetailsAddLabel, TaskDetailsAddLabel,
TaskDetailsAddLabelIcon, TaskDetailsAddLabelIcon,
TaskAction, TaskAction,
TaskMeta, TaskMeta,
ActionButtons,
ActionButton,
ActionButtonsTitle,
TaskHeader, TaskHeader,
ProfileIcon, ProfileIcon,
TaskDetailsContent, TaskDetailsContent,
@ -37,6 +41,10 @@ import {
TaskDetailAssignee, TaskDetailAssignee,
TaskDetailAssignees, TaskDetailAssignees,
TaskDetailsAddMemberIcon, TaskDetailsAddMemberIcon,
MetaDetails,
MetaDetail,
MetaDetailTitle,
MetaDetailContent,
} from './Styles'; } from './Styles';
import Checklist from '../Checklist'; import Checklist from '../Checklist';
@ -127,6 +135,7 @@ type TaskDetailsProps = {
onAddItem: (checklistID: string, name: string, position: number) => void; onAddItem: (checklistID: string, name: string, position: number) => void;
onDeleteItem: (itemID: string) => void; onDeleteItem: (itemID: string) => void;
onChangeItemName: (itemID: string, itemName: string) => void; onChangeItemName: (itemID: string, itemName: string) => void;
onToggleTaskComplete: (task: Task) => void;
onToggleChecklistItem: (itemID: string, complete: boolean) => void; onToggleChecklistItem: (itemID: string, complete: boolean) => void;
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void; onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void; onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
@ -138,6 +147,7 @@ type TaskDetailsProps = {
const TaskDetails: React.FC<TaskDetailsProps> = ({ const TaskDetails: React.FC<TaskDetailsProps> = ({
task, task,
onTaskNameChange, onTaskNameChange,
onToggleTaskComplete,
onTaskDescriptionChange, onTaskDescriptionChange,
onChangeItemName, onChangeItemName,
onDeleteItem, onDeleteItem,
@ -170,13 +180,14 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const onUnassignedClick = () => { const onUnassignedClick = () => {
onOpenAddMemberPopup(task, $unassignedRef); onOpenAddMemberPopup(task, $unassignedRef);
}; };
const onAddMember = () => { const onAddMember = ($target: React.RefObject<HTMLElement>) => {
onOpenAddMemberPopup(task, $addMemberRef); onOpenAddMemberPopup(task, $target);
}; };
const $dueDateLabel = useRef<HTMLDivElement>(null); const $dueDateLabel = useRef<HTMLDivElement>(null);
const $addLabelRef = useRef<HTMLDivElement>(null); const $addLabelRef = useRef<HTMLDivElement>(null);
const onAddLabel = () => {
onOpenAddLabelPopup(task, $addLabelRef); const onAddLabel = ($target: React.RefObject<HTMLElement>) => {
onOpenAddLabelPopup(task, $target);
}; };
return ( return (
<> <>
@ -206,6 +217,53 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
</TaskHeader> </TaskHeader>
<TaskDetailsWrapper> <TaskDetailsWrapper>
<TaskDetailsContent> <TaskDetailsContent>
<MetaDetails>
{task.assigned && task.assigned.length !== 0 && (
<MetaDetail>
<MetaDetailTitle>MEMBERS</MetaDetailTitle>
<MetaDetailContent>
{task.assigned &&
task.assigned.map(member => (
<TaskAssignee key={member.id} size={32} member={member} onMemberProfile={onMemberProfile} />
))}
<TaskDetailsAddMemberIcon ref={$addMemberRef} onClick={() => onAddMember($addMemberRef)}>
<Plus size={16} color="#c2c6dc" />
</TaskDetailsAddMemberIcon>
</MetaDetailContent>
</MetaDetail>
)}
{task.labels.length !== 0 && (
<MetaDetail>
<MetaDetailTitle>LABELS</MetaDetailTitle>
<MetaDetailContent>
{task.labels.map(label => {
return (
<TaskLabelItem
key={label.projectLabel.id}
label={label}
onClick={$target => {
onOpenAddLabelPopup(task, $target);
}}
/>
);
})}
<TaskDetailsAddLabelIcon ref={$addLabelRef} onClick={() => onAddLabel($addLabelRef)}>
<Plus size={16} color="#c2c6dc" />
</TaskDetailsAddLabelIcon>
</MetaDetailContent>
</MetaDetail>
)}
{task.dueDate && (
<MetaDetail>
<MetaDetailTitle>DUE DATE</MetaDetailTitle>
<MetaDetailContent>
<TaskDueDateButton>{moment(task.dueDate).format('MMM D [at] h:mm A')}</TaskDueDateButton>
</MetaDetailContent>
</MetaDetail>
)}
</MetaDetails>
<TaskDetailsSection>
<TaskDetailsLabel>Description</TaskDetailsLabel> <TaskDetailsLabel>Description</TaskDetailsLabel>
{editorOpen ? ( {editorOpen ? (
<DetailsEditor <DetailsEditor
@ -249,57 +307,21 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onChangeItemName={onChangeItemName} onChangeItemName={onChangeItemName}
/> />
))} ))}
</TaskDetailsSection>
</TaskDetailsContent> </TaskDetailsContent>
<TaskDetailsSidebar> <TaskDetailsSidebar>
<TaskDetailSectionTitle>Assignees</TaskDetailSectionTitle> <ActionButtons>
<TaskDetailAssignees> <ActionButtonsTitle>ADD TO CARD</ActionButtonsTitle>
{task.assigned && task.assigned.length === 0 ? ( <ActionButton onClick={() => onToggleTaskComplete(task)}>
<UnassignedLabel ref={$unassignedRef} onClick={onUnassignedClick}> {task.complete ? 'Mark Incomplete' : 'Mark Complete'}
Unassigned </ActionButton>
</UnassignedLabel> <ActionButton onClick={$target => onAddMember($target)}>Members</ActionButton>
) : ( <ActionButton onClick={$target => onAddLabel($target)}>Labels</ActionButton>
<> <ActionButton>Checklist</ActionButton>
{task.assigned && <ActionButton onClick={$target => onOpenDueDatePopop(task, $target)}>Due Date</ActionButton>
task.assigned.map(member => ( <ActionButton>Attachment</ActionButton>
<TaskAssignee key={member.id} size={32} member={member} onMemberProfile={onMemberProfile} /> <ActionButton>Cover</ActionButton>
))} </ActionButtons>
<TaskDetailsAddMember ref={$addMemberRef} onClick={onAddMember}>
<TaskDetailsAddMemberIcon>
<Plus size={16} color="#c2c6dc" />
</TaskDetailsAddMemberIcon>
</TaskDetailsAddMember>
</>
)}
</TaskDetailAssignees>
<TaskDetailSectionTitle>Labels</TaskDetailSectionTitle>
<TaskDetailLabels>
{task.labels.map(label => {
return (
<TaskLabelItem
key={label.projectLabel.id}
label={label}
onClick={$target => {
onOpenAddLabelPopup(task, $target);
}}
/>
);
})}
<TaskDetailsAddLabel ref={$addLabelRef} onClick={onAddLabel}>
<TaskDetailsAddLabelIcon>
<Plus size={16} color="#c2c6dc" />
</TaskDetailsAddLabelIcon>
</TaskDetailsAddLabel>
</TaskDetailLabels>
<TaskDetailSectionTitle>Due Date</TaskDetailSectionTitle>
{task.dueDate ? (
<NoDueDateLabel ref={$dueDateLabel} onClick={() => onOpenDueDatePopop(task, $dueDateLabel)}>
{moment(task.dueDate).format('MMM D [at] h:mm A')}
</NoDueDateLabel>
) : (
<NoDueDateLabel ref={$dueDateLabel} onClick={() => onOpenDueDatePopop(task, $dueDateLabel)}>
No due date
</NoDueDateLabel>
)}
</TaskDetailsSidebar> </TaskDetailsSidebar>
</TaskDetailsWrapper> </TaskDetailsWrapper>
</> </>

View File

@ -102,6 +102,17 @@ export type TaskGroup = {
tasks: Array<Task>; tasks: Array<Task>;
}; };
export type ChecklistBadge = {
__typename?: 'ChecklistBadge';
complete: Scalars['Int'];
total: Scalars['Int'];
};
export type TaskBadges = {
__typename?: 'TaskBadges';
checklist?: Maybe<ChecklistBadge>;
};
export type Task = { export type Task = {
__typename?: 'Task'; __typename?: 'Task';
id: Scalars['ID']; id: Scalars['ID'];
@ -115,6 +126,7 @@ export type Task = {
assigned: Array<ProjectMember>; assigned: Array<ProjectMember>;
labels: Array<TaskLabel>; labels: Array<TaskLabel>;
checklists: Array<TaskChecklist>; checklists: Array<TaskChecklist>;
badges: TaskBadges;
}; };
export type ProjectsFilter = { export type ProjectsFilter = {
@ -792,7 +804,7 @@ export type FindTaskQuery = (
{ __typename?: 'Query' } { __typename?: 'Query' }
& { findTask: ( & { findTask: (
{ __typename?: 'Task' } { __typename?: 'Task' }
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position'> & Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position' | 'complete'>
& { taskGroup: ( & { taskGroup: (
{ __typename?: 'TaskGroup' } { __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id'> & Pick<TaskGroup, 'id'>
@ -1607,6 +1619,7 @@ export const FindTaskDocument = gql`
description description
dueDate dueDate
position position
complete
taskGroup { taskGroup {
id id
} }

View File

@ -5,6 +5,7 @@ query findTask($taskID: UUID!) {
description description
dueDate dueDate
position position
complete
taskGroup { taskGroup {
id id
} }