From 16eb9e165f4f93399be6f699721da514efff0bcb Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Sun, 12 Apr 2020 17:45:51 -0500 Subject: [PATCH] feature: update task details design --- web/package.json | 3 + web/src/App/BaseStyles.ts | 4 + web/src/Projects/Project/Lists/index.tsx | 0 web/src/Projects/Project/index.tsx | 63 +++++++ web/src/citadel.d.ts | 11 ++ .../DueDateManager/DueDateManager.stories.tsx | 32 ++++ .../components/DueDateManager/Styles.ts | 45 +++++ .../components/DueDateManager/index.tsx | 32 ++++ web/src/shared/components/List/Styles.ts | 2 +- .../shared/components/Lists/Lists.stories.tsx | 6 +- web/src/shared/components/Lists/index.tsx | 50 ++--- .../MemberManager/MemberManager.stories.tsx | 18 ++ .../shared/components/MemberManager/Styles.ts | 81 ++++++++ .../shared/components/MemberManager/index.tsx | 73 +++++++ web/src/shared/components/Modal/Styles.ts | 4 +- .../PopupMenu/PopupMenu.stories.tsx | 94 +++++++++ web/src/shared/components/PopupMenu/Styles.ts | 3 +- .../shared/components/TaskDetails/Styles.ts | 178 ++++++++++++++---- .../TaskDetails/TaskDetails.stories.tsx | 8 +- .../shared/components/TaskDetails/index.tsx | 122 ++++++++++-- web/src/shared/undraw/NoData.tsx | 104 ++++++++++ web/src/shared/utils/boundingRect.ts | 20 ++ web/src/shared/utils/draggables.ts | 3 + web/yarn.lock | 36 +++- 24 files changed, 903 insertions(+), 89 deletions(-) create mode 100644 web/src/Projects/Project/Lists/index.tsx create mode 100644 web/src/shared/components/DueDateManager/DueDateManager.stories.tsx create mode 100644 web/src/shared/components/DueDateManager/Styles.ts create mode 100644 web/src/shared/components/DueDateManager/index.tsx create mode 100644 web/src/shared/components/MemberManager/MemberManager.stories.tsx create mode 100644 web/src/shared/components/MemberManager/Styles.ts create mode 100644 web/src/shared/components/MemberManager/index.tsx create mode 100644 web/src/shared/undraw/NoData.tsx create mode 100644 web/src/shared/utils/boundingRect.ts diff --git a/web/package.json b/web/package.json index f60fb04..51b6f3c 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/App/BaseStyles.ts b/web/src/App/BaseStyles.ts index c49e765..96499c4 100644 --- a/web/src/App/BaseStyles.ts +++ b/web/src/App/BaseStyles.ts @@ -106,5 +106,9 @@ export default createGlobalStyle` touch-action: manipulation; } + textarea { + resize: none; + } + ${mixin.placeholderColor(color.textLight)} `; diff --git a/web/src/Projects/Project/Lists/index.tsx b/web/src/Projects/Project/Lists/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/web/src/Projects/Project/index.tsx b/web/src/Projects/Project/index.tsx index b50b0c0..6a34971 100644 --- a/web/src/Projects/Project/index.tsx +++ b/web/src/Projects/Project/index.tsx @@ -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(); 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 = () => { { + setTaskDetails({ isOpen: true, taskID: task.taskID }); + }} onExtraMenuOpen={(taskGroupID, pos, size) => { setPopupData({ isOpen: true, @@ -315,6 +327,57 @@ const Project = () => { /> )} + {memberPopupData.isOpen && ( + setMemberPopupData(initialMemberPopupState)} + top={memberPopupData.top} + left={memberPopupData.left} + > + console.log(member, isActive)} + /> + + )} + {taskDetails.isOpen && ( + { + setTaskDetails(initialTaskDetailsState); + }} + renderContent={() => { + const task = listsData.tasks[taskDetails.taskID]; + return ( + { + 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) => {}} + /> + ); + }} + /> + )} ); } diff --git a/web/src/citadel.d.ts b/web/src/citadel.d.ts index 0cccf87..9cf5130 100644 --- a/web/src/citadel.d.ts +++ b/web/src/citadel.d.ts @@ -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; }; type TaskGroup = { @@ -85,3 +91,8 @@ type ElementSize = { width: number; height: number; }; + +type ElementBounds = { + size: ElementSize; + position: ElementPosition; +}; diff --git a/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx b/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx new file mode 100644 index 0000000..806b5a8 --- /dev/null +++ b/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx @@ -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 ( + + ); +}; diff --git a/web/src/shared/components/DueDateManager/Styles.ts b/web/src/shared/components/DueDateManager/Styles.ts new file mode 100644 index 0000000..2f032de --- /dev/null +++ b/web/src/shared/components/DueDateManager/Styles.ts @@ -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; +`; diff --git a/web/src/shared/components/DueDateManager/index.tsx b/web/src/shared/components/DueDateManager/index.tsx new file mode 100644 index 0000000..bc3bb3b --- /dev/null +++ b/web/src/shared/components/DueDateManager/index.tsx @@ -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 = ({ task, onDueDateChange, onCancel }) => { + const [startDate, setStartDate] = useState(new Date()); + + return ( + + + setStartDate(date ?? new Date())} /> + + + onDueDateChange(task, startDate)}>Save + + + + + + ); +}; + +export default DueDateManager; diff --git a/web/src/shared/components/List/Styles.ts b/web/src/shared/components/List/Styles.ts index 0af939b..04ea547 100644 --- a/web/src/shared/components/List/Styles.ts +++ b/web/src/shared/components/List/Styles.ts @@ -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; `; diff --git a/web/src/shared/components/Lists/Lists.stories.tsx b/web/src/shared/components/Lists/Lists.stories.tsx index e9d6d80..ff6be49 100644 --- a/web/src/shared/components/Lists/Lists.stories.tsx +++ b/web/src/shared/components/Lists/Lists.stories.tsx @@ -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 ( { return ( 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 = ({ columns, tasks, + onCardClick, onCardDrop, onListDrop, onCardCreate, @@ -72,7 +74,6 @@ const Lists: React.FC = ({ console.log(beforeDropDraggables); console.log(destination); - console.log(droppedDraggable); const afterDropDraggables = getAfterDropDraggableList( beforeDropDraggables, droppedDraggable, @@ -85,16 +86,20 @@ const Lists: React.FC = ({ 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 = ({ return ( {columnDragProvided => ( - 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} - > - - {columnDropProvided => ( + + {(columnDropProvided, snapshot) => ( + 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} + > {columnCards.map((task: Task, taskIndex: any) => { return ( @@ -154,7 +159,7 @@ const Lists: React.FC = ({ 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 = ({ ); })} {columnDropProvided.placeholder} - {currentComposer === column.taskGroupID && ( { @@ -176,9 +180,9 @@ const Lists: React.FC = ({ /> )} - )} - - + + )} + )} ); diff --git a/web/src/shared/components/MemberManager/MemberManager.stories.tsx b/web/src/shared/components/MemberManager/MemberManager.stories.tsx new file mode 100644 index 0000000..42598a5 --- /dev/null +++ b/web/src/shared/components/MemberManager/MemberManager.stories.tsx @@ -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 ; +}; diff --git a/web/src/shared/components/MemberManager/Styles.ts b/web/src/shared/components/MemberManager/Styles.ts new file mode 100644 index 0000000..fc7240d --- /dev/null +++ b/web/src/shared/components/MemberManager/Styles.ts @@ -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; +`; diff --git a/web/src/shared/components/MemberManager/index.tsx b/web/src/shared/components/MemberManager/index.tsx new file mode 100644 index 0000000..56db833 --- /dev/null +++ b/web/src/shared/components/MemberManager/index.tsx @@ -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; + activeMembers: Array; + onMemberChange: (member: TaskUser, isActive: boolean) => void; +}; +const MemberManager: React.FC = ({ + availableMembers, + activeMembers: initialActiveMembers, + onMemberChange, +}) => { + const [activeMembers, setActiveMembers] = useState(initialActiveMembers); + const [currentSearch, setCurrentSearch] = useState(''); + return ( + + + ) => { + setCurrentSearch(e.currentTarget.value); + }} + /> + + Board Members + + {availableMembers + .filter( + member => currentSearch === '' || member.displayName.toLowerCase().startsWith(currentSearch.toLowerCase()), + ) + .map(member => { + return ( + + { + 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); + }} + > + JK + {member.displayName} + {activeMembers.findIndex(m => m.userID === member.userID) !== -1 && ( + + + + )} + + + ); + })} + + + ); +}; +export default MemberManager; diff --git a/web/src/shared/components/Modal/Styles.ts b/web/src/shared/components/Modal/Styles.ts index b6ec642..c037d7b 100644 --- a/web/src/shared/components/Modal/Styles.ts +++ b/web/src/shared/components/Modal/Styles.ts @@ -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; diff --git a/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx b/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx index f78fea6..951ba9b 100644 --- a/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx +++ b/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx @@ -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(null); + const [popupData, setPopupData] = useState(initalState); + return ( + <> + + + {popupData.isOpen && ( + setPopupData(initalState)} left={popupData.left}> + + + )} + { + if ($buttonRef && $buttonRef.current) { + const pos = $buttonRef.current.getBoundingClientRect(); + setPopupData({ + isOpen: true, + left: pos.left, + top: pos.top + pos.height + 10, + }); + } + }} + > + Open + + + ); +}; + +export const DueDateManagerPopup = () => { + const $buttonRef = useRef(null); + const [popupData, setPopupData] = useState(initalState); + return ( + <> + + + {popupData.isOpen && ( + setPopupData(initalState)} left={popupData.left}> + + + )} + { + if ($buttonRef && $buttonRef.current) { + const pos = $buttonRef.current.getBoundingClientRect(); + setPopupData({ + isOpen: true, + left: pos.left, + top: pos.top + pos.height + 10, + }); + } + }} + > + Open + + + ); +}; diff --git a/web/src/shared/components/PopupMenu/Styles.ts b/web/src/shared/components/PopupMenu/Styles.ts index 8232edb..a51c525 100644 --- a/web/src/shared/components/PopupMenu/Styles.ts +++ b/web/src/shared/components/PopupMenu/Styles.ts @@ -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; diff --git a/web/src/shared/components/TaskDetails/Styles.ts b/web/src/shared/components/TaskDetails/Styles.ts index e5ae0c9..e5ceeeb 100644 --- a/web/src/shared/components/TaskDetails/Styles.ts +++ b/web/src/shared/components/TaskDetails/Styles.ts @@ -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; +`; diff --git a/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx index 188e20c..3117e5f 100644 --- a/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx +++ b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx @@ -30,15 +30,19 @@ export const Default = () => { setDescription(desc)} onDeleteTask={action('delete task')} onCloseModal={action('close modal')} + onOpenAddMemberPopup={action('open add member popup')} + onOpenAddLabelPopup={action('open add label popup')} /> ); }} diff --git a/web/src/shared/components/TaskDetails/index.tsx b/web/src/shared/components/TaskDetails/index.tsx index 8470e67..01b8a8c 100644 --- a/web/src/shared/components/TaskDetails/index.tsx +++ b/web/src/shared/components/TaskDetails/index.tsx @@ -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 = ({ Save - + @@ -79,37 +94,81 @@ const DetailsEditor: React.FC = ({ 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 = ({ task, onTaskDescriptionChange, onDeleteTask, onCloseModal }) => { +const TaskDetails: React.FC = ({ + 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(null); + const onAddMember = () => { + console.log('beep!'); + const bounds = convertDivElementRefToBounds($addMemberRef); + console.log(bounds); + if (bounds) { + onOpenAddMemberPopup(task, bounds); + } + }; + const $addLabelRef = useRef(null); + const onAddLabel = () => { + const bounds = convertDivElementRefToBounds($addLabelRef); + if (bounds) { + onOpenAddLabelPopup(task, bounds); + } + }; return ( <> + + + + + + + + - - - - - - - - - + + ) => setTaskName(e.currentTarget.value)} + onKeyDown={onKeyDown} + /> + + + {task.taskGroup.name && ( + + in list {task.taskGroup.name} + + )} + - - - Description {editorOpen ? ( = ({ task, onTaskDescriptionChange )} - + + Assignees + + {task.members && + task.members.map(member => { + const initials = 'JK'; + return ( + + {initials} + + ); + })} + + + + + + + Labels + + {task.labels.map(label => { + return {label.name}; + })} + + + + + + + Due Date + No due date + ); diff --git a/web/src/shared/undraw/NoData.tsx b/web/src/shared/undraw/NoData.tsx new file mode 100644 index 0000000..c9f9c05 --- /dev/null +++ b/web/src/shared/undraw/NoData.tsx @@ -0,0 +1,104 @@ +import React from 'react'; + +type Props = { + width: number; + height: number; +}; + +const AccessAccount = ({ width, height }: Props) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; +export default AccessAccount; diff --git a/web/src/shared/utils/boundingRect.ts b/web/src/shared/utils/boundingRect.ts new file mode 100644 index 0000000..bd41f2e --- /dev/null +++ b/web/src/shared/utils/boundingRect.ts @@ -0,0 +1,20 @@ +export const convertDivElementRefToBounds = ($ref: React.RefObject) => { + 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; diff --git a/web/src/shared/utils/draggables.ts b/web/src/shared/utils/draggables.ts index 34d0116..2c8ce74 100644 --- a/web/src/shared/utils/draggables.ts +++ b/web/src/shared/utils/draggables.ts @@ -9,7 +9,9 @@ export const moveItemWithinArray = (arr: Array, item: Draggabl export const insertItemIntoArray = (arr: Array, 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; }; diff --git a/web/yarn.lock b/web/yarn.lock index fa61699..6eca232 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -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==