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 {
useFindTaskQuery,
useUpdateTaskDueDateMutation,
useSetTaskCompleteMutation,
useAssignTaskMutation,
useUnassignTaskMutation,
useSetTaskChecklistItemCompleteMutation,
@ -108,6 +109,7 @@ const Details: React.FC<DetailsProps> = ({
},
});
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const [setTaskComplete] = useSetTaskCompleteMutation();
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => {
refetch();
@ -136,7 +138,7 @@ const Details: React.FC<DetailsProps> = ({
return (
<>
<Modal
width={1040}
width={768}
onClose={() => {
history.push(projectURL);
}}
@ -146,6 +148,9 @@ const Details: React.FC<DetailsProps> = ({
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 } });

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`
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;

View File

@ -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';

View File

@ -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<HTMLButtonElement>) => void;
};
const Button: React.FC<ButtonProps> = ({
@ -122,46 +122,68 @@ const Button: React.FC<ButtonProps> = ({
className,
children,
}) => {
const $button = useRef<HTMLButtonElement>(null);
const handleClick = () => {
if (onClick) {
onClick();
onClick($button);
}
};
switch (variant) {
case 'filled':
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>
</Filled>
);
case 'outline':
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>
</Outline>
);
case 'flat':
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>
</Flat>
);
case 'lineDown':
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>
<LineX color={color} />
</LineDown>
);
case 'gradient':
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>
</Gradient>
);
case 'relief':
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>
</Relief>
);

View File

@ -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 }>`

View File

@ -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``;

View File

@ -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')}

View File

@ -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<HTMLElement>) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
@ -138,6 +147,7 @@ type TaskDetailsProps = {
const TaskDetails: React.FC<TaskDetailsProps> = ({
task,
onTaskNameChange,
onToggleTaskComplete,
onTaskDescriptionChange,
onChangeItemName,
onDeleteItem,
@ -170,13 +180,14 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const onUnassignedClick = () => {
onOpenAddMemberPopup(task, $unassignedRef);
};
const onAddMember = () => {
onOpenAddMemberPopup(task, $addMemberRef);
const onAddMember = ($target: React.RefObject<HTMLElement>) => {
onOpenAddMemberPopup(task, $target);
};
const $dueDateLabel = useRef<HTMLDivElement>(null);
const $addLabelRef = useRef<HTMLDivElement>(null);
const onAddLabel = () => {
onOpenAddLabelPopup(task, $addLabelRef);
const onAddLabel = ($target: React.RefObject<HTMLElement>) => {
onOpenAddLabelPopup(task, $target);
};
return (
<>
@ -206,100 +217,111 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
</TaskHeader>
<TaskDetailsWrapper>
<TaskDetailsContent>
<TaskDetailsLabel>Description</TaskDetailsLabel>
{editorOpen ? (
<DetailsEditor
description={description}
onTaskDescriptionChange={newDescription => {
setEditorOpen(false);
setDescription(newDescription);
onTaskDescriptionChange(task, newDescription);
}}
onCancel={() => {
setEditorOpen(false);
}}
/>
) : (
<TaskContent description={description} onEditContent={handleClick} />
)}
{task.checklists &&
task.checklists
.slice()
.sort((a, b) => a.position - b.position)
.map(checklist => (
<Checklist
key={checklist.id}
name={checklist.name}
checklistID={checklist.id}
items={checklist.items}
onDeleteChecklist={() => {}}
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}
/>
))}
</TaskDetailsContent>
<TaskDetailsSidebar>
<TaskDetailSectionTitle>Assignees</TaskDetailSectionTitle>
<TaskDetailAssignees>
{task.assigned && task.assigned.length === 0 ? (
<UnassignedLabel ref={$unassignedRef} onClick={onUnassignedClick}>
Unassigned
</UnassignedLabel>
) : (
<>
{task.assigned &&
task.assigned.map(member => (
<TaskAssignee key={member.id} size={32} member={member} onMemberProfile={onMemberProfile} />
))}
<TaskDetailsAddMember ref={$addMemberRef} onClick={onAddMember}>
<TaskDetailsAddMemberIcon>
<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>
</TaskDetailsAddMember>
</>
</MetaDetailContent>
</MetaDetail>
)}
</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>
)}
{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>
{editorOpen ? (
<DetailsEditor
description={description}
onTaskDescriptionChange={newDescription => {
setEditorOpen(false);
setDescription(newDescription);
onTaskDescriptionChange(task, newDescription);
}}
onCancel={() => {
setEditorOpen(false);
}}
/>
) : (
<TaskContent description={description} onEditContent={handleClick} />
)}
{task.checklists &&
task.checklists
.slice()
.sort((a, b) => a.position - b.position)
.map(checklist => (
<Checklist
key={checklist.id}
name={checklist.name}
checklistID={checklist.id}
items={checklist.items}
onDeleteChecklist={() => {}}
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}
/>
))}
</TaskDetailsSection>
</TaskDetailsContent>
<TaskDetailsSidebar>
<ActionButtons>
<ActionButtonsTitle>ADD TO CARD</ActionButtonsTitle>
<ActionButton onClick={() => onToggleTaskComplete(task)}>
{task.complete ? 'Mark Incomplete' : 'Mark Complete'}
</ActionButton>
<ActionButton onClick={$target => onAddMember($target)}>Members</ActionButton>
<ActionButton onClick={$target => onAddLabel($target)}>Labels</ActionButton>
<ActionButton>Checklist</ActionButton>
<ActionButton onClick={$target => onOpenDueDatePopop(task, $target)}>Due Date</ActionButton>
<ActionButton>Attachment</ActionButton>
<ActionButton>Cover</ActionButton>
</ActionButtons>
</TaskDetailsSidebar>
</TaskDetailsWrapper>
</>

View File

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

View File

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