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

@ -24,6 +24,7 @@
"@types/node": "^12.0.0",
"@types/react": "^16.9.21",
"@types/react-beautiful-dnd": "^12.1.1",
"@types/react-datepicker": "^2.11.0",
"@types/react-dom": "^16.9.5",
"@types/react-router": "^5.1.4",
"@types/react-router-dom": "^5.1.3",
@ -41,10 +42,12 @@
"history": "^4.10.1",
"immer": "^6.0.3",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-autosize-textarea": "^7.0.0",
"react-beautiful-dnd": "^13.0.0",
"react-datepicker": "^2.14.1",
"react-dom": "^16.12.0",
"react-hook-form": "^5.2.0",
"react-router": "^5.1.2",

View File

@ -106,5 +106,9 @@ export default createGlobalStyle`
touch-action: manipulation;
}
textarea {
resize: none;
}
${mixin.placeholderColor(color.textLight)}
`;

View File

View File

@ -19,6 +19,10 @@ import Lists from 'shared/components/Lists';
import QuickCardEditor from 'shared/components/QuickCardEditor';
import PopupMenu from 'shared/components/PopupMenu';
import ListActions from 'shared/components/ListActions';
import Modal from 'shared/components/Modal';
import TaskDetails from 'shared/components/TaskDetails';
import MemberManager from 'shared/components/MemberManager';
import { LabelsPopup } from 'shared/components/PopupMenu/PopupMenu.stories';
interface ColumnState {
[key: string]: TaskGroup;
@ -73,11 +77,16 @@ interface ProjectParams {
const initialState: State = { tasks: {}, columns: {} };
const initialPopupState = { left: 0, top: 0, isOpen: false, taskGroupID: '' };
const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top: 0, left: 0 };
const initialMemberPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const initialLabelsPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const initialTaskDetailsState = { isOpen: false, taskID: '' };
const Project = () => {
const { projectId } = useParams<ProjectParams>();
const [listsData, setListsData] = useState(initialState);
const [popupData, setPopupData] = useState(initialPopupState);
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
const [taskDetails, setTaskDetails] = useState(initialTaskDetailsState);
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
const [updateTaskLocation] = useUpdateTaskLocationMutation();
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation();
@ -256,6 +265,9 @@ const Project = () => {
</TitleWrapper>
<Board>
<Lists
onCardClick={task => {
setTaskDetails({ isOpen: true, taskID: task.taskID });
}}
onExtraMenuOpen={(taskGroupID, pos, size) => {
setPopupData({
isOpen: true,
@ -315,6 +327,57 @@ const Project = () => {
/>
</PopupMenu>
)}
{memberPopupData.isOpen && (
<PopupMenu
title="Members"
onClose={() => setMemberPopupData(initialMemberPopupState)}
top={memberPopupData.top}
left={memberPopupData.left}
>
<MemberManager
availableMembers={[{ displayName: 'Jordan Knott', userID: '21345076-6423-4a00-a6bd-cd9f830e2764' }]}
activeMembers={[]}
onMemberChange={(member, isActive) => console.log(member, isActive)}
/>
</PopupMenu>
)}
{taskDetails.isOpen && (
<Modal
width={1040}
onClose={() => {
setTaskDetails(initialTaskDetailsState);
}}
renderContent={() => {
const task = listsData.tasks[taskDetails.taskID];
return (
<TaskDetails
task={task}
onTaskNameChange={(updatedTask, newName) => {
updateTaskName({ variables: { taskID: updatedTask.taskID, name: newName } });
}}
onTaskDescriptionChange={(updatedTask, newDescription) => {
console.log(updatedTask, newDescription);
}}
onDeleteTask={deletedTask => {
setTaskDetails(initialTaskDetailsState);
deleteTask({ variables: { taskID: deletedTask.taskID } });
}}
onCloseModal={() => setTaskDetails(initialTaskDetailsState)}
onOpenAddMemberPopup={(task, bounds) => {
console.log(task, bounds);
setMemberPopupData({
isOpen: true,
taskID: task.taskID,
top: bounds.position.top + bounds.size.height + 10,
left: bounds.position.left,
});
}}
onOpenAddLabelPopup={(task, bounds) => {}}
/>
);
}}
/>
)}
</>
);
}

11
web/src/citadel.d.ts vendored
View File

@ -16,6 +16,11 @@ type InnerTaskGroup = {
position?: number;
};
type TaskUser = {
userID: string;
displayName: string;
};
type Task = {
taskID: string;
taskGroup: InnerTaskGroup;
@ -23,6 +28,7 @@ type Task = {
position: number;
labels: Label[];
description?: string;
members?: Array<TaskUser>;
};
type TaskGroup = {
@ -85,3 +91,8 @@ type ElementSize = {
width: number;
height: number;
};
type ElementBounds = {
size: ElementSize;
position: ElementPosition;
};

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,
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 => (
<Droppable type="tasks" droppableId={column.taskGroupID}>
{(columnDropProvided, snapshot) => (
<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}
id={column.taskGroupID}
key={column.taskGroupID}
index={index}
>
<Droppable type="tasks" droppableId={column.taskGroupID}>
{columnDropProvided => (
<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>
</List>
)}
</Droppable>
</List>
)}
</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 (
<>
<TaskHeader>
<TaskMeta />
<TaskActions>
<TaskAction onClick={handleDeleteTask}>
<Bin size={20} />
<Bin size={20} color="#c2c6dc" />
</TaskAction>
<TaskAction onClick={onCloseModal}>
<Cross size={20} />
<Cross size={20} color="#c2c6dc" />
</TaskAction>
</TaskActions>
<TaskHeader>
<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>
</>
);

View File

@ -0,0 +1,104 @@
import React from 'react';
type Props = {
width: number;
height: number;
};
const AccessAccount = ({ width, height }: Props) => {
return (
<svg data-name="Layer 1" width={width} height={height} viewBox="0 0 820.16 780.81">
<defs>
<linearGradient
id="a"
x1="539.63"
y1="734.6"
x2="539.63"
y2="151.19"
gradientTransform="translate(-3.62 1.57)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="gray" stop-opacity=".25" />
<stop offset=".54" stop-color="gray" stop-opacity=".12" />
<stop offset="1" stop-color="gray" stop-opacity=".1" />
</linearGradient>
<linearGradient
id="b"
x1="540.17"
y1="180.2"
x2="540.17"
y2="130.75"
gradientTransform="translate(-63.92 7.85)"
/>
<linearGradient
id="c"
x1="540.17"
y1="140.86"
x2="540.17"
y2="82.43"
gradientTransform="rotate(-12.11 545.066 460.65)"
/>
<linearGradient id="d" x1="476.4" y1="710.53" x2="476.4" y2="127.12" />
<linearGradient id="e" x1="476.94" y1="156.13" x2="476.94" y2="106.68" />
<linearGradient id="f" x1="666.86" y1="176.39" x2="666.86" y2="117.95" />
</defs>
<path fill="#e0e0e0" d="M69.12 135.49l427.295-91.682L623.09 634.19l-427.295 91.682z" />
<path
transform="rotate(-12.11 160.03 1309.797)"
fill="url(#a)"
d="M324.89 152.76h422.25v583.41H324.89z"
opacity=".5"
/>
<path fill="#fafafa" d="M84.639 146.993L486.98 60.665l119.69 557.824-402.344 86.328z" />
<path transform="rotate(-12.11 100.28 1028.707)" fill="url(#b)" d="M374.18 138.6h204.14v49.45H374.18z" />
<path
d="M460.93 91.9c-15.41 3.31-25.16 18.78-21.77 34.55s18.62 25.89 34 22.58 25.16-18.78 21.77-34.55-18.59-25.89-34-22.58zm9.67 45.1a16.86 16.86 0 1112.56-20 16.66 16.66 0 01-12.56 20z"
transform="translate(-189.92 -59.59)"
fill="url(#c)"
/>
<path fill="#6c63ff" d="M183.007 98.422L378.4 56.498l9.917 46.218-195.393 41.924z" />
<path
d="M271.01 32.31a27.93 27.93 0 1033.17 21.45 27.93 27.93 0 00-33.17-21.45zm9.24 43.1a16.12 16.12 0 1112.38-19.14 16.12 16.12 0 01-12.38 19.14z"
fill="#6c63ff"
/>
<path fill="#e0e0e0" d="M257.89 116.91h437.02v603.82H257.89z" />
<path fill="url(#d)" d="M265.28 127.12h422.25v583.41H265.28z" opacity=".5" />
<path fill="#fff" d="M270.65 131.42h411.5v570.52h-411.5z" />
<path fill="url(#e)" d="M374.87 106.68h204.14v49.45H374.87z" />
<path
d="M666.86 118c-15.76 0-28.54 13.08-28.54 29.22s12.78 29.22 28.54 29.22 28.54-13.08 28.54-29.22S682.62 118 666.86 118zm0 46.08a16.86 16.86 0 1116.46-16.86A16.66 16.66 0 01666.86 164z"
transform="translate(-189.92 -59.59)"
fill="url(#f)"
/>
<path fill="#6c63ff" d="M377.02 104.56h199.84v47.27H377.02z" />
<path
d="M476.94 58.41a27.93 27.93 0 1027.93 27.93 27.93 27.93 0 00-27.93-27.93zm0 44.05a16.12 16.12 0 1116.14-16.16 16.12 16.12 0 01-16.14 16.11z"
fill="#6c63ff"
/>
<g opacity=".5" fill="#47e6b1">
<path d="M15.27 737.05h3.76v21.33h-3.76z" />
<path d="M27.82 745.84v3.76H6.49v-3.76z" />
</g>
<g opacity=".5" fill="#47e6b1">
<path d="M451.49 0h3.76v21.33h-3.76z" />
<path d="M464.04 8.78v3.76h-21.33V8.78z" />
</g>
<path
d="M771.08 772.56a4.61 4.61 0 01-2.57-5.57 2.22 2.22 0 00.1-.51 2.31 2.31 0 00-4.15-1.53 2.22 2.22 0 00-.26.45 4.61 4.61 0 01-5.57 2.57 2.22 2.22 0 00-.51-.1 2.31 2.31 0 00-1.53 4.15 2.22 2.22 0 00.45.26 4.61 4.61 0 012.57 5.57 2.22 2.22 0 00-.1.51 2.31 2.31 0 004.15 1.53 2.22 2.22 0 00.26-.45 4.61 4.61 0 015.57-2.57 2.22 2.22 0 00.51.1 2.31 2.31 0 001.53-4.15 2.22 2.22 0 00-.45-.26z"
fill="#4d8af0"
opacity=".5"
/>
<path
d="M136.67 567.5a4.61 4.61 0 01-2.57-5.57 2.22 2.22 0 00.1-.51 2.31 2.31 0 00-4.15-1.53 2.22 2.22 0 00-.26.45 4.61 4.61 0 01-5.57 2.57 2.22 2.22 0 00-.51-.1 2.31 2.31 0 00-1.53 4.15 2.22 2.22 0 00.45.26 4.61 4.61 0 012.57 5.57 2.22 2.22 0 00-.1.51 2.31 2.31 0 004.15 1.53 2.22 2.22 0 00.26-.45 4.61 4.61 0 015.57-2.57 2.22 2.22 0 00.51.1 2.31 2.31 0 001.53-4.15 2.22 2.22 0 00-.45-.26zM665.08 68.18a4.61 4.61 0 01-2.57-5.57 2.22 2.22 0 00.1-.51 2.31 2.31 0 00-4.15-1.53 2.22 2.22 0 00-.26.45 4.61 4.61 0 01-5.57 2.57 2.22 2.22 0 00-.51-.1 2.31 2.31 0 00-1.53 4.15 2.22 2.22 0 00.45.26 4.61 4.61 0 012.57 5.57 2.22 2.22 0 00-.1.51 2.31 2.31 0 004.15 1.53 2.22 2.22 0 00.26-.45 4.61 4.61 0 015.57-2.57 2.22 2.22 0 00.51.1 2.31 2.31 0 001.53-4.15 2.22 2.22 0 00-.45-.26z"
fill="#fdd835"
opacity=".5"
/>
<circle cx="812.64" cy="314.47" r="7.53" fill="#f55f44" opacity=".5" />
<circle cx="230.73" cy="746.65" r="7.53" fill="#f55f44" opacity=".5" />
<circle cx="735.31" cy="477.23" r="7.53" fill="#f55f44" opacity=".5" />
<circle cx="87.14" cy="96.35" r="7.53" fill="#4d8af0" opacity=".5" />
<circle cx="7.53" cy="301.76" r="7.53" fill="#47e6b1" opacity=".5" />
</svg>
);
};
export default AccessAccount;

View File

@ -0,0 +1,20 @@
export const convertDivElementRefToBounds = ($ref: React.RefObject<HTMLDivElement>) => {
if ($ref && $ref.current) {
const bounds = $ref.current.getBoundingClientRect();
return {
size: {
width: bounds.width,
height: bounds.height,
},
position: {
left: bounds.left,
right: bounds.right,
top: bounds.top,
bottom: bounds.bottom,
},
};
}
return null;
};
export default convertDivElementRefToBounds;

View File

@ -9,7 +9,9 @@ export const moveItemWithinArray = (arr: Array<DraggableElement>, item: Draggabl
export const insertItemIntoArray = (arr: Array<DraggableElement>, item: DraggableElement, index: number) => {
const arrClone = [...arr];
console.log(arrClone, index, item);
arrClone.splice(index, 0, item);
console.log(arrClone);
return arrClone;
};
@ -56,6 +58,7 @@ export const isPositionChanged = (source: DraggableLocation, destination: Dragga
if (!destination) return false;
const isSameList = destination.droppableId === source.droppableId;
const isSamePosition = destination.index === source.index;
console.log(`isSameList: ${isSameList} : isSamePosition: ${isSamePosition}`);
return !isSameList || !isSamePosition;
};

View File

@ -3179,6 +3179,15 @@
dependencies:
"@types/react" "*"
"@types/react-datepicker@^2.11.0":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-2.11.0.tgz#aa6faa66de17b0ff96bc0af9d5d2506d5dd99703"
integrity sha512-eagG8BE3TFgPYyZb2/hG4+2delLH9z/4OWzT7wuTCKLHDDXIXgvkb2O2cW8q4/wuqnTnMxo+vl3vgPTENkofzw==
dependencies:
"@types/react" "*"
date-fns "^2.0.1"
popper.js "^1.14.1"
"@types/react-dom@*", "@types/react-dom@^16.9.5":
version "16.9.5"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.5.tgz#5de610b04a35d07ffd8f44edad93a71032d9aaa7"
@ -5480,7 +5489,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.5:
classnames@^2.2.5, classnames@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
@ -6319,6 +6328,11 @@ date-fns@^1.27.2:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-fns@^2.0.1:
version "2.12.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.12.0.tgz#01754c8a2f3368fc1119cf4625c3dad8c1845ee6"
integrity sha512-qJgn99xxKnFgB1qL4jpxU7Q2t0LOn1p8KMIveef3UZD7kqjT3tpFNNdXJelEHhE+rUgffriXriw/sOSU+cS1Hw==
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
@ -12032,7 +12046,7 @@ polished@^3.3.1:
dependencies:
"@babel/runtime" "^7.6.3"
popper.js@^1.14.4, popper.js@^1.14.7:
popper.js@^1.14.1, popper.js@^1.14.4, popper.js@^1.14.7:
version "1.16.1"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
@ -13250,6 +13264,17 @@ react-color@^2.17.0:
reactcss "^1.2.0"
tinycolor2 "^1.4.1"
react-datepicker@^2.14.1:
version "2.14.1"
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-2.14.1.tgz#83463beb85235a575475955f554290a95f89c65b"
integrity sha512-8eWHvrjXfKVkt5rERXC6/c/eEdcE2stIsl+QmTO5Efgpacf8MOCyVpBisJLVLDYjVlENczhOcRlIzvraODHKxA==
dependencies:
classnames "^2.2.6"
date-fns "^2.0.1"
prop-types "^15.7.2"
react-onclickoutside "^6.9.0"
react-popper "^1.3.4"
react-dev-utils@^10.2.0:
version "10.2.0"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-10.2.0.tgz#b11cc48aa2be2502fb3c27a50d1dfa95cfa9dfe0"
@ -13442,6 +13467,11 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-onclickoutside@^6.9.0:
version "6.9.0"
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz#a54bc317ae8cf6131a5d78acea55a11067f37a1f"
integrity sha512-8ltIY3bC7oGhj2nPAvWOGi+xGFybPNhJM0V1H8hY/whNcXgmDeaeoCMPPd8VatrpTsUWjb/vGzrmu6SrXVty3A==
react-popper-tooltip@^2.8.3:
version "2.10.1"
resolved "https://registry.yarnpkg.com/react-popper-tooltip/-/react-popper-tooltip-2.10.1.tgz#e10875f31916297c694d64a677d6f8fa0a48b4d1"
@ -13450,7 +13480,7 @@ react-popper-tooltip@^2.8.3:
"@babel/runtime" "^7.7.4"
react-popper "^1.3.6"
react-popper@^1.3.6:
react-popper@^1.3.4, react-popper@^1.3.6:
version "1.3.7"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324"
integrity sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==