feature: update task details design

This commit is contained in:
Jordan Knott
2020-04-12 17:45:51 -05:00
parent c250ce574b
commit 16eb9e165f
24 changed files with 903 additions and 89 deletions

View File

@ -0,0 +1,32 @@
import React, { useRef } from 'react';
import { action } from '@storybook/addon-actions';
import DueDateManager from '.';
export default {
component: DueDateManager,
title: 'DueDateManager',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
export const Default = () => {
return (
<DueDateManager
task={{
taskID: '1',
taskGroup: { name: 'General', taskGroupID: '1' },
name: 'Hello, world',
position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
description: 'hello!',
members: [{ userID: '1', displayName: 'Jordan Knott' }],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}
/>
);
};

View File

@ -0,0 +1,45 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
display: flex
flex-direction: column;
`;
export const DueDatePickerWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;
export const ConfirmAddDueDate = styled.div`
background-color: #5aac44;
box-shadow: none;
border: none;
color: #fff;
float: left;
margin: 0 4px 0 0;
cursor: pointer;
display: inline-block;
font-weight: 400;
line-height: 20px;
padding: 6px 12px;
text-align: center;
border-radius: 3px;
font-size: 14px;
`;
export const CancelDueDate = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 32px;
width: 32px;
cursor: pointer;
`;
export const ActionWrapper = styled.div`
padding-top: 8px;
width: 100%;
display: flex;
`;

View File

@ -0,0 +1,32 @@
import React, { useState } from 'react';
import DatePicker from 'react-datepicker';
import { Cross } from 'shared/icons';
import { Wrapper, ActionWrapper, DueDatePickerWrapper, ConfirmAddDueDate, CancelDueDate } from './Styles';
import 'react-datepicker/dist/react-datepicker.css';
type DueDateManagerProps = {
task: Task;
onDueDateChange: (task: Task, newDueDate: Date) => void;
onCancel: () => void;
};
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onCancel }) => {
const [startDate, setStartDate] = useState(new Date());
return (
<Wrapper>
<DueDatePickerWrapper>
<DatePicker inline selected={startDate} onChange={date => setStartDate(date ?? new Date())} />
</DueDatePickerWrapper>
<ActionWrapper>
<ConfirmAddDueDate onClick={() => onDueDateChange(task, startDate)}>Save</ConfirmAddDueDate>
<CancelDueDate onClick={onCancel}>
<Cross size={16} color="#c2c6dc" />
</CancelDueDate>
</ActionWrapper>
</Wrapper>
);
};
export default DueDateManager;

View File

@ -113,7 +113,7 @@ export const ListCards = styled.div`
margin: 0 4px;
padding: 0 4px;
flex: 1 1 auto;
min-height: 30px;
min-height: 45px;
overflow-y: auto;
overflow-x: hidden;
`;

View File

@ -64,13 +64,13 @@ const initialListsData = {
export const Default = () => {
const [listsData, setListsData] = useState(initialListsData);
const onCardDrop = (droppedTask: any) => {
const onCardDrop = (droppedTask: Task) => {
console.log(droppedTask);
const newState = {
...listsData,
tasks: {
...listsData.tasks,
[droppedTask.taskGroupID]: droppedTask,
[droppedTask.taskID]: droppedTask,
},
};
console.log(newState);
@ -91,6 +91,7 @@ export const Default = () => {
return (
<Lists
{...listsData}
onCardClick={action('card click')}
onExtraMenuOpen={action('extra menu open')}
onQuickEditorOpen={action('card composer open')}
onCardDrop={onCardDrop}
@ -201,6 +202,7 @@ export const ListsWithManyList = () => {
return (
<Lists
{...listsData}
onCardClick={action('card click')}
onQuickEditorOpen={action('card composer open')}
onCardCreate={action('card create')}
onCardDrop={onCardDrop}

View File

@ -23,6 +23,7 @@ interface Tasks {
type Props = {
columns: Columns;
tasks: Tasks;
onCardClick: (task: Task) => void;
onCardDrop: (task: Task) => void;
onListDrop: (taskGroup: TaskGroup) => void;
onCardCreate: (taskGroupID: string, name: string) => void;
@ -34,6 +35,7 @@ type Props = {
const Lists: React.FC<Props> = ({
columns,
tasks,
onCardClick,
onCardDrop,
onListDrop,
onCardCreate,
@ -72,7 +74,6 @@ const Lists: React.FC<Props> = ({
console.log(beforeDropDraggables);
console.log(destination);
console.log(droppedDraggable);
const afterDropDraggables = getAfterDropDraggableList(
beforeDropDraggables,
droppedDraggable,
@ -85,16 +86,20 @@ const Lists: React.FC<Props> = ({
if (isList) {
const droppedList = columns[droppedDraggable.id];
console.log(`is list ${droppedList}`);
onListDrop({
...droppedList,
position: newPosition,
});
} else {
const droppedCard = tasks[droppedDraggable.id];
console.log(`is card ${droppedCard}`);
const newCard = {
...droppedCard,
position: newPosition,
taskGroupID: destination.droppableId,
taskGroup: {
taskGroupID: destination.droppableId,
},
};
onCardDrop(newCard);
}
@ -121,22 +126,22 @@ const Lists: React.FC<Props> = ({
return (
<Draggable draggableId={column.taskGroupID} key={column.taskGroupID} index={index}>
{columnDragProvided => (
<List
id={column.taskGroupID}
name={column.name}
key={column.taskGroupID}
onOpenComposer={id => setCurrentComposer(id)}
isComposerOpen={currentComposer === column.taskGroupID}
onSaveName={name => console.log(name)}
index={index}
tasks={columnCards}
ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps}
headerProps={columnDragProvided.dragHandleProps}
onExtraMenuOpen={onExtraMenuOpen}
>
<Droppable type="tasks" droppableId={column.taskGroupID}>
{columnDropProvided => (
<Droppable type="tasks" droppableId={column.taskGroupID}>
{(columnDropProvided, snapshot) => (
<List
name={column.name}
onOpenComposer={id => setCurrentComposer(id)}
isComposerOpen={currentComposer === column.taskGroupID}
onSaveName={name => console.log(name)}
tasks={columnCards}
ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps}
headerProps={columnDragProvided.dragHandleProps}
onExtraMenuOpen={onExtraMenuOpen}
id={column.taskGroupID}
key={column.taskGroupID}
index={index}
>
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
{columnCards.map((task: Task, taskIndex: any) => {
return (
@ -154,7 +159,7 @@ const Lists: React.FC<Props> = ({
description=""
title={task.name}
labels={task.labels}
onClick={e => console.log(e)}
onClick={() => onCardClick(task)}
onContextMenu={onQuickEditorOpen}
/>
);
@ -163,7 +168,6 @@ const Lists: React.FC<Props> = ({
);
})}
{columnDropProvided.placeholder}
{currentComposer === column.taskGroupID && (
<CardComposer
onClose={() => {
@ -176,9 +180,9 @@ const Lists: React.FC<Props> = ({
/>
)}
</ListCards>
)}
</Droppable>
</List>
</List>
)}
</Droppable>
)}
</Draggable>
);

View File

@ -0,0 +1,18 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import MemberManager from '.';
export default {
component: MemberManager,
title: 'MemberManager',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
export const Default = () => {
return <MemberManager availableMembers={[]} activeMembers={[]} onMemberChange={action('member change')} />;
};

View File

@ -0,0 +1,81 @@
import styled from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
export const MemberManagerWrapper = styled.div``;
export const MemberManagerSearchWrapper = styled.div`
width: 100%;
display: flex;
`;
export const MemberManagerSearch = styled(TextareaAutosize)`
margin: 4px 0 12px;
width: 100%;
background-color: #ebecf0;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
line-height: 20px;
padding: 8px 12px;
font-size: 14px;
color: #172b4d;
`;
export const BoardMembersLabel = styled.h4`
color: #5e6c84;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
line-height: 16px;
text-transform: uppercase;
`;
export const BoardMembersList = styled.ul`
margin: 0;
padding: 0;
list-style-type: none;
`;
export const BoardMembersListItem = styled.li``;
export const BoardMemberListItemContent = styled.div`
background-color: rgba(9, 30, 66, 0.04);
padding-right: 28px;
border-radius: 3px;
display: flex;
height: 40px;
overflow: hidden;
cursor: pointer;
align-items: center;
position: relative;
text-overflow: ellipsis;
text-decoration: none;
white-space: nowrap;
padding: 4px;
margin-bottom: 2px;
color: #172b4d;
`;
export const ProfileIcon = styled.div`
width: 32px;
height: 32px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
background: rgb(115, 103, 240);
cursor: pointer;
margin-right: 6px;
`;
export const MemberName = styled.span`
font-size: 14px;
`;
export const ActiveIconWrapper = styled.div`
position: absolute;
top: 0;
right: 0;
padding: 11px;
`;

View File

@ -0,0 +1,73 @@
import React, { useState } from 'react';
import {
MemberName,
ProfileIcon,
MemberManagerWrapper,
MemberManagerSearchWrapper,
MemberManagerSearch,
BoardMembersLabel,
BoardMembersList,
BoardMembersListItem,
BoardMemberListItemContent,
ActiveIconWrapper,
} from './Styles';
import { Checkmark } from 'shared/icons';
type MemberManagerProps = {
availableMembers: Array<TaskUser>;
activeMembers: Array<TaskUser>;
onMemberChange: (member: TaskUser, isActive: boolean) => void;
};
const MemberManager: React.FC<MemberManagerProps> = ({
availableMembers,
activeMembers: initialActiveMembers,
onMemberChange,
}) => {
const [activeMembers, setActiveMembers] = useState(initialActiveMembers);
const [currentSearch, setCurrentSearch] = useState('');
return (
<MemberManagerWrapper>
<MemberManagerSearchWrapper>
<MemberManagerSearch
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setCurrentSearch(e.currentTarget.value);
}}
/>
</MemberManagerSearchWrapper>
<BoardMembersLabel>Board Members</BoardMembersLabel>
<BoardMembersList>
{availableMembers
.filter(
member => currentSearch === '' || member.displayName.toLowerCase().startsWith(currentSearch.toLowerCase()),
)
.map(member => {
return (
<BoardMembersListItem>
<BoardMemberListItemContent
onClick={() => {
const isActive = activeMembers.findIndex(m => m.userID === member.userID) !== -1;
if (isActive) {
setActiveMembers(activeMembers.filter(m => m.userID !== member.userID));
} else {
setActiveMembers([...activeMembers, member]);
}
onMemberChange(member, !isActive);
}}
>
<ProfileIcon>JK</ProfileIcon>
<MemberName>{member.displayName}</MemberName>
{activeMembers.findIndex(m => m.userID === member.userID) !== -1 && (
<ActiveIconWrapper>
<Checkmark size={16} color="#42526e" />
</ActiveIconWrapper>
)}
</BoardMemberListItemContent>
</BoardMembersListItem>
);
})}
</BoardMembersList>
</MemberManagerWrapper>
);
};
export default MemberManager;

View File

@ -14,7 +14,7 @@ export const ScrollOverlay = styled.div`
export const ClickableOverlay = styled.div`
min-height: 100%;
background: rgba(9, 30, 66, 0.54);
background: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
@ -25,7 +25,7 @@ export const StyledModal = styled.div<{ width: number }>`
display: inline-block;
position: relative;
width: 100%;
background: #fff;
background: #262c49;
max-width: ${props => props.width}px;
vertical-align: middle;
border-radius: 3px;

View File

@ -4,8 +4,12 @@ import LabelColors from 'shared/constants/labelColors';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
import ListActions from 'shared/components/ListActions';
import MemberManager from 'shared/components/MemberManager';
import DueDateManager from 'shared/components/DueDateManager';
import PopupMenu from '.';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
export default {
component: PopupMenu,
@ -99,3 +103,93 @@ export const ListActionsPopup = () => {
</>
);
};
export const MemberManagerPopup = () => {
const $buttonRef = useRef<HTMLButtonElement>(null);
const [popupData, setPopupData] = useState(initalState);
return (
<>
<NormalizeStyles />
<BaseStyles />
{popupData.isOpen && (
<PopupMenu title="Members" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<MemberManager
availableMembers={[{ userID: '1', displayName: 'Jordan Knott' }]}
activeMembers={[]}
onMemberChange={action('member change')}
/>
</PopupMenu>
)}
<span
style={{
width: '60px',
textAlign: 'center',
margin: '25px auto',
cursor: 'pointer',
}}
ref={$buttonRef}
onClick={() => {
if ($buttonRef && $buttonRef.current) {
const pos = $buttonRef.current.getBoundingClientRect();
setPopupData({
isOpen: true,
left: pos.left,
top: pos.top + pos.height + 10,
});
}
}}
>
Open
</span>
</>
);
};
export const DueDateManagerPopup = () => {
const $buttonRef = useRef<HTMLButtonElement>(null);
const [popupData, setPopupData] = useState(initalState);
return (
<>
<NormalizeStyles />
<BaseStyles />
{popupData.isOpen && (
<PopupMenu title="Due Date" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<DueDateManager
task={{
taskID: '1',
taskGroup: { name: 'General', taskGroupID: '1' },
name: 'Hello, world',
position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
description: 'hello!',
members: [{ userID: '1', displayName: 'Jordan Knott' }],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}
/>
</PopupMenu>
)}
<span
style={{
width: '60px',
textAlign: 'center',
margin: '25px auto',
cursor: 'pointer',
}}
ref={$buttonRef}
onClick={() => {
if ($buttonRef && $buttonRef.current) {
const pos = $buttonRef.current.getBoundingClientRect();
setPopupData({
isOpen: true,
left: pos.left,
top: pos.top + pos.height + 10,
});
}
}}
>
Open
</span>
</>
);
};

View File

@ -8,10 +8,9 @@ export const Container = styled.div<{ top: number; left: number; ref: any }>`
border-radius: 3px;
box-shadow: 0 8px 16px -4px rgba(9, 30, 66, 0.25), 0 0 0 1px rgba(9, 30, 66, 0.08);
display: block;
overflow: hidden;
position: absolute;
width: 304px;
z-index: 70;
z-index: 100000000000;
&:focus {
outline: none;
border: none;

View File

@ -1,22 +1,38 @@
import styled from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
import { mixin } from 'shared/utils/styles';
export const TaskHeader = styled.div`
padding: 21px 30px 0px;
margin-right: 70px;
display: flex;
-webkit-box-pack: justify;
justify-content: space-between;
padding: 21px 18px 0px;
flex-direction: column;
`;
export const TaskMeta = styled.div`
position: relative;
cursor: pointer;
font-size: 14px;
display: inline-block;
display: flex;
align-items: center;
border-radius: 4px;
`;
export const TaskGroupLabel = styled.span`
color: #c2c6dc;
font-size: 14px;
`;
export const TaskGroupLabelName = styled.span`
color: #c2c6dc;
text-decoration: underline;
font-size: 14px;
`;
export const TaskActions = styled.div`
position: absolute;
top: 0;
right: 0;
padding: 21px 18px 0px;
display: flex;
align-items: center;
`;
@ -26,19 +42,8 @@ export const TaskAction = styled.button`
align-items: center;
justify-content: center;
height: 32px;
vertical-align: middle;
line-height: 1;
white-space: nowrap;
cursor: pointer;
user-select: none;
font-size: 14.5px;
color: rgb(66, 82, 110);
font-family: CircularStdBook;
font-weight: normal;
padding: 0px 9px;
border-radius: 3px;
transition: all 0.1s ease 0s;
background: rgb(255, 255, 255);
`;
export const TaskDetailsWrapper = styled.div`
@ -53,13 +58,12 @@ export const TaskDetailsContent = styled.div`
export const TaskDetailsSidebar = styled.div`
width: 35%;
padding-top: 5px;
`;
export const TaskDetailsTitleWrapper = styled.div`
height: 44px;
width: 100%;
margin: 18px 0px 0px -8px;
margin: 0 0 0 -8px;
display: inline-block;
`;
@ -70,8 +74,8 @@ export const TaskDetailsTitle = styled(TextareaAutosize)`
font-size: 24px;
font-family: 'Droid Sans';
font-weight: 700;
padding: 7px 7px 8px;
background: rgb(255, 255, 255);
padding: 4px;
background: #262c49;
border-width: 1px;
border-style: solid;
border-color: transparent;
@ -79,28 +83,22 @@ export const TaskDetailsTitle = styled(TextareaAutosize)`
transition: background 0.1s ease 0s;
overflow-y: hidden;
width: 100%;
color: rgb(23, 43, 77);
&:hover {
background: rgb(235, 236, 240);
}
color: #c2c6dc;
&:focus {
box-shadow: rgb(76, 154, 255) 0px 0px 0px 1px;
background: rgb(255, 255, 255);
border-width: 1px;
border-style: solid;
border-color: rgb(76, 154, 255);
border-image: initial;
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const TaskDetailsLabel = styled.div`
padding: 20px 0px 12px;
padding: 24px 0px 12px;
font-size: 15px;
font-weight: 600;
color: #c2c6dc;
`;
export const TaskDetailsAddDetailsButton = styled.div`
background-color: rgba(9, 30, 66, 0.04);
background: ${mixin.darken('#262c49', 0.15)};
box-shadow: none;
border: none;
border-radius: 3px;
@ -110,8 +108,9 @@ export const TaskDetailsAddDetailsButton = styled.div`
text-decoration: none;
font-size: 14px;
cursor: pointer;
color: #c2c6dc;
&:hover {
background-color: rgba(9, 30, 66, 0.08);
background: ${mixin.darken('#262c49', 0.25)};
box-shadow: none;
border: none;
}
@ -128,9 +127,9 @@ export const TaskDetailsEditorWrapper = styled.div`
export const TaskDetailsEditor = styled(TextareaAutosize)`
width: 100%;
min-height: 108px;
background: #fff;
box-shadow: none;
border-color: rgba(9, 30, 66, 0.13);
color: #c2c6dc;
background: #262c49;
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
border-radius: 3px;
line-height: 20px;
padding: 8px 12px;
@ -138,15 +137,15 @@ export const TaskDetailsEditor = styled(TextareaAutosize)`
border: none;
&:focus {
background: #fff;
border: none;
box-shadow: inset 0 0 0 2px #0079bf;
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.05)};
}
`;
export const TaskDetailsMarkdown = styled.div`
width: 100%;
cursor: pointer;
color: #c2c6dc;
`;
export const TaskDetailsControls = styled.div`
@ -179,3 +178,106 @@ export const CancelEdit = styled.div`
width: 32px;
cursor: pointer;
`;
export const TaskDetailSectionTitle = styled.div`
text-transform: uppercase;
color: #c2c6dc;
font-size: 12.5px;
font-weight: 600;
margin: 24px 0px 5px;
`;
export const TaskDetailAssignees = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
`;
export const TaskDetailAssignee = styled.div`
&:hover {
opacity: 0.8;
}
margin-right: 4px;
`;
export const ProfileIcon = styled.div`
width: 32px;
height: 32px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
background: rgb(115, 103, 240);
cursor: pointer;
`;
export const TaskDetailsAddMember = styled.div`
border-radius: 100%;
background: ${mixin.darken('#262c49', 0.15)};
cursor: pointer;
&:hover {
opacity: 0.8;
}
`;
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;
flex-wrap: wrap;
`;
export const TaskDetailLabel = styled.div`
&:hover {
opacity: 0.8;
}
background-color: #00c2e0;
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
border-radius: 3px;
box-sizing: border-box;
display: block;
float: left;
font-weight: 600;
height: 32px;
line-height: 32px;
margin: 0 4px 4px 0;
min-width: 40px;
padding: 0 12px;
width: auto;
`;
export const TaskDetailsAddLabel = styled.div`
border-radius: 3px;
background: ${mixin.darken('#262c49', 0.15)};
cursor: pointer;
&:hover {
opacity: 0.8;
}
`;
export const TaskDetailsAddLabelIcon = styled.div`
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
`;
export const NoDueDateLabel = styled.span`
color: rgb(137, 147, 164);
font-size: 14px;
cursor: pointer;
`;

View File

@ -30,15 +30,19 @@ export const Default = () => {
<TaskDetails
task={{
taskID: '1',
taskGroup: { taskGroupID: '1' },
taskGroup: { name: 'General', taskGroupID: '1' },
name: 'Hello, world',
position: 1,
labels: [],
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
description,
members: [{ userID: '1', displayName: 'Jordan Knott' }],
}}
onTaskNameChange={action('task name change')}
onTaskDescriptionChange={(_task, desc) => setDescription(desc)}
onDeleteTask={action('delete task')}
onCloseModal={action('close modal')}
onOpenAddMemberPopup={action('open add member popup')}
onOpenAddLabelPopup={action('open add label popup')}
/>
);
}}

View File

@ -1,12 +1,19 @@
import React, { useState, useRef, useEffect } from 'react';
import { Bin, Cross } from 'shared/icons';
import { Bin, Cross, Plus } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import {
NoDueDateLabel,
TaskDetailsAddMember,
TaskGroupLabel,
TaskGroupLabelName,
TaskActions,
TaskDetailsAddLabel,
TaskDetailsAddLabelIcon,
TaskAction,
TaskMeta,
TaskHeader,
ProfileIcon,
TaskDetailsContent,
TaskDetailsWrapper,
TaskDetailsSidebar,
@ -20,8 +27,16 @@ import {
TaskDetailsControls,
ConfirmSave,
CancelEdit,
TaskDetailSectionTitle,
TaskDetailLabel,
TaskDetailLabels,
TaskDetailAssignee,
TaskDetailAssignees,
TaskDetailsAddMemberIcon,
} from './Styles';
import convertDivElementRefToBounds from 'shared/utils/boundingRect';
type TaskContentProps = {
onEditContent: () => void;
description: string;
@ -70,7 +85,7 @@ const DetailsEditor: React.FC<DetailsEditorProps> = ({
<TaskDetailsControls>
<ConfirmSave onClick={handleOutsideClick}>Save</ConfirmSave>
<CancelEdit onClick={onCancel}>
<Cross size={16} />
<Plus size={16} color="#c2c6dc" />
</CancelEdit>
</TaskDetailsControls>
</TaskDetailsEditorWrapper>
@ -79,37 +94,81 @@ const DetailsEditor: React.FC<DetailsEditorProps> = ({
type TaskDetailsProps = {
task: Task;
onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
onCloseModal: () => void;
onOpenAddMemberPopup: (task: Task, bounds: ElementBounds) => void;
onOpenAddLabelPopup: (task: Task, bounds: ElementBounds) => void;
};
const TaskDetails: React.FC<TaskDetailsProps> = ({ task, onTaskDescriptionChange, onDeleteTask, onCloseModal }) => {
const TaskDetails: React.FC<TaskDetailsProps> = ({
task,
onTaskNameChange,
onTaskDescriptionChange,
onDeleteTask,
onCloseModal,
onOpenAddMemberPopup,
onOpenAddLabelPopup,
}) => {
const [editorOpen, setEditorOpen] = useState(false);
const [taskName, setTaskName] = useState(task.name);
const handleClick = () => {
setEditorOpen(!editorOpen);
};
const handleDeleteTask = () => {
onDeleteTask(task);
};
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
onTaskNameChange(task, taskName);
}
};
const $addMemberRef = useRef<HTMLDivElement>(null);
const onAddMember = () => {
console.log('beep!');
const bounds = convertDivElementRefToBounds($addMemberRef);
console.log(bounds);
if (bounds) {
onOpenAddMemberPopup(task, bounds);
}
};
const $addLabelRef = useRef<HTMLDivElement>(null);
const onAddLabel = () => {
const bounds = convertDivElementRefToBounds($addLabelRef);
if (bounds) {
onOpenAddLabelPopup(task, bounds);
}
};
return (
<>
<TaskActions>
<TaskAction onClick={handleDeleteTask}>
<Bin size={20} color="#c2c6dc" />
</TaskAction>
<TaskAction onClick={onCloseModal}>
<Cross size={20} color="#c2c6dc" />
</TaskAction>
</TaskActions>
<TaskHeader>
<TaskMeta />
<TaskActions>
<TaskAction onClick={handleDeleteTask}>
<Bin size={20} />
</TaskAction>
<TaskAction onClick={onCloseModal}>
<Cross size={20} />
</TaskAction>
</TaskActions>
<TaskDetailsTitleWrapper>
<TaskDetailsTitle
value={taskName}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setTaskName(e.currentTarget.value)}
onKeyDown={onKeyDown}
/>
</TaskDetailsTitleWrapper>
<TaskMeta>
{task.taskGroup.name && (
<TaskGroupLabel>
in list <TaskGroupLabelName>{task.taskGroup.name}</TaskGroupLabelName>
</TaskGroupLabel>
)}
</TaskMeta>
</TaskHeader>
<TaskDetailsWrapper>
<TaskDetailsContent>
<TaskDetailsTitleWrapper>
<TaskDetailsTitle value="Hello darkness my old friend" />
</TaskDetailsTitleWrapper>
<TaskDetailsLabel>Description</TaskDetailsLabel>
{editorOpen ? (
<DetailsEditor
@ -126,7 +185,38 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({ task, onTaskDescriptionChange
<TaskContent description={task.description ?? ''} onEditContent={handleClick} />
)}
</TaskDetailsContent>
<TaskDetailsSidebar />
<TaskDetailsSidebar>
<TaskDetailSectionTitle>Assignees</TaskDetailSectionTitle>
<TaskDetailAssignees>
{task.members &&
task.members.map(member => {
const initials = 'JK';
return (
<TaskDetailAssignee key={member.userID}>
<ProfileIcon>{initials}</ProfileIcon>
</TaskDetailAssignee>
);
})}
<TaskDetailsAddMember ref={$addMemberRef} onClick={onAddMember}>
<TaskDetailsAddMemberIcon>
<Plus size={16} color="#c2c6dc" />
</TaskDetailsAddMemberIcon>
</TaskDetailsAddMember>
</TaskDetailAssignees>
<TaskDetailSectionTitle>Labels</TaskDetailSectionTitle>
<TaskDetailLabels>
{task.labels.map(label => {
return <TaskDetailLabel>{label.name}</TaskDetailLabel>;
})}
<TaskDetailsAddLabel ref={$addLabelRef} onClick={onAddLabel}>
<TaskDetailsAddLabelIcon>
<Plus size={16} color="#c2c6dc" />
</TaskDetailsAddLabelIcon>
</TaskDetailsAddLabel>
</TaskDetailLabels>
<TaskDetailSectionTitle>Due Date</TaskDetailSectionTitle>
<NoDueDateLabel>No due date</NoDueDateLabel>
</TaskDetailsSidebar>
</TaskDetailsWrapper>
</>
);