diff --git a/web/src/App/Routes.tsx b/web/src/App/Routes.tsx index 61482ab..2699205 100644 --- a/web/src/App/Routes.tsx +++ b/web/src/App/Routes.tsx @@ -16,7 +16,7 @@ const Routes = ({ history }: RoutesProps) => ( - + diff --git a/web/src/Projects/Project/KanbanBoard/Styles.ts b/web/src/Projects/Project/KanbanBoard/Styles.ts new file mode 100644 index 0000000..301d2bf --- /dev/null +++ b/web/src/Projects/Project/KanbanBoard/Styles.ts @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +export const Board = styled.div` + margin-left: 36px; +`; diff --git a/web/src/Projects/Project/KanbanBoard/index.tsx b/web/src/Projects/Project/KanbanBoard/index.tsx new file mode 100644 index 0000000..f8126c3 --- /dev/null +++ b/web/src/Projects/Project/KanbanBoard/index.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useRouteMatch, useHistory } from 'react-router'; + +import Lists from 'shared/components/Lists'; +import { Board } from './Styles'; + +type KanbanBoardProps = { + listsData: BoardState; + onOpenListActionsPopup: (isOpen: boolean, left: number, top: number, taskGroupID: string) => void; + onCardDrop: (task: Task) => void; + onListDrop: (taskGroup: TaskGroup) => void; + onCardCreate: (taskGroupID: string, name: string) => void; + onQuickEditorOpen: (e: ContextMenuEvent) => void; + onCreateList: (listName: string) => void; +}; + +const KanbanBoard: React.FC = ({ + listsData, + onOpenListActionsPopup, + onQuickEditorOpen, + onCardCreate, + onCardDrop, + onListDrop, + onCreateList, +}) => { + const match = useRouteMatch(); + const history = useHistory(); + return ( + + { + history.push(`${match.url}/c/${task.taskID}`); + }} + onExtraMenuOpen={(taskGroupID, pos, size) => { + onOpenListActionsPopup(true, pos.left, pos.top + size.height + 5, taskGroupID); + }} + onQuickEditorOpen={onQuickEditorOpen} + onCardCreate={onCardCreate} + onCardDrop={onCardDrop} + onListDrop={onListDrop} + {...listsData} + onCreateList={onCreateList} + /> + + ); +}; + +export default KanbanBoard; diff --git a/web/src/Projects/Project/Lists/index.tsx b/web/src/Projects/Project/Lists/index.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/web/src/Projects/Project/index.tsx b/web/src/Projects/Project/index.tsx index 6a34971..f084069 100644 --- a/web/src/Projects/Project/index.tsx +++ b/web/src/Projects/Project/index.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; -import produce from 'immer'; +import * as BoardStateUtils from 'shared/utils/boardState'; import styled from 'styled-components/macro'; -import { useParams } from 'react-router-dom'; +import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps } from 'react-router-dom'; import { useFindProjectQuery, useUpdateTaskNameMutation, @@ -15,7 +15,6 @@ import { import Navbar from 'App/Navbar'; import TopNavbar from 'App/TopNavbar'; -import Lists from 'shared/components/Lists'; import QuickCardEditor from 'shared/components/QuickCardEditor'; import PopupMenu from 'shared/components/PopupMenu'; import ListActions from 'shared/components/ListActions'; @@ -23,19 +22,11 @@ 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'; +import KanbanBoard from 'Projects/Project/KanbanBoard'; -interface ColumnState { - [key: string]: TaskGroup; -} - -interface TaskState { - [key: string]: Task; -} - -interface State { - columns: ColumnState; - tasks: TaskState; -} +type TaskRouteProps = { + taskID: string; +}; interface QuickCardEditorState { isOpen: boolean; @@ -66,15 +57,11 @@ const Title = styled.span` color: #fff; `; -const Board = styled.div` - margin-left: 36px; -`; - interface ProjectParams { projectId: string; } -const initialState: State = { tasks: {}, columns: {} }; +const initialState: BoardState = { 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 }; @@ -83,6 +70,9 @@ const initialTaskDetailsState = { isOpen: false, taskID: '' }; const Project = () => { const { projectId } = useParams(); + const match = useRouteMatch(); + const history = useHistory(); + const [listsData, setListsData] = useState(initialState); const [popupData, setPopupData] = useState(initialPopupState); const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState); @@ -90,92 +80,52 @@ const Project = () => { const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState); const [updateTaskLocation] = useUpdateTaskLocationMutation(); const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation(); + const [deleteTaskGroup] = useDeleteTaskGroupMutation({ onCompleted: deletedTaskGroupData => { - const nextState = produce(listsData, (draftState: State) => { - delete draftState.columns[deletedTaskGroupData.deleteTaskGroup.taskGroup.taskGroupID]; - const filteredTasks = Object.keys(listsData.tasks) - .filter( - taskID => - listsData.tasks[taskID].taskGroup.taskGroupID !== - deletedTaskGroupData.deleteTaskGroup.taskGroup.taskGroupID, - ) - .reduce((obj: TaskState, key: string) => { - obj[key] = listsData.tasks[key]; - return obj; - }, {}); - draftState.tasks = filteredTasks; - }); - - setListsData(nextState); + setListsData( + BoardStateUtils.deleteTaskGroup(listsData, deletedTaskGroupData.deleteTaskGroup.taskGroup.taskGroupID), + ); }, }); + const [createTaskGroup] = useCreateTaskGroupMutation({ onCompleted: newTaskGroupData => { - const newListsData = { - ...listsData, - columns: { - ...listsData.columns, - [newTaskGroupData.createTaskGroup.taskGroupID]: { - taskGroupID: newTaskGroupData.createTaskGroup.taskGroupID, - name: newTaskGroupData.createTaskGroup.name, - position: newTaskGroupData.createTaskGroup.position, - tasks: [], - }, - }, + const newTaskGroup = { + ...newTaskGroupData.createTaskGroup, + tasks: [], }; - setListsData(newListsData); + setListsData(BoardStateUtils.addTaskGroup(listsData, newTaskGroup)); }, }); + const [createTask] = useCreateTaskMutation({ onCompleted: newTaskData => { - const newListsData = { - ...listsData, - tasks: { - ...listsData.tasks, - [newTaskData.createTask.taskID]: { - taskGroup: { - taskGroupID: newTaskData.createTask.taskGroup.taskGroupID, - }, - taskID: newTaskData.createTask.taskID, - name: newTaskData.createTask.name, - position: newTaskData.createTask.position, - labels: [], - }, - }, + const newTask = { + ...newTaskData.createTask, + labels: [], }; - setListsData(newListsData); + setListsData(BoardStateUtils.addTask(listsData, newTask)); }, }); + const [deleteTask] = useDeleteTaskMutation({ onCompleted: deletedTask => { - const { [deletedTask.deleteTask.taskID]: removedTask, ...remainingTasks } = listsData.tasks; - const newListsData = { - ...listsData, - tasks: remainingTasks, - }; - setListsData(newListsData); + setListsData(BoardStateUtils.deleteTask(listsData, deletedTask.deleteTask.taskID)); }, }); + const [updateTaskName] = useUpdateTaskNameMutation({ onCompleted: newTaskData => { - const newListsData = { - ...listsData, - tasks: { - ...listsData.tasks, - [newTaskData.updateTaskName.taskID]: { - ...listsData.tasks[newTaskData.updateTaskName.taskID], - name: newTaskData.updateTaskName.name, - }, - }, - }; - setListsData(newListsData); + setListsData( + BoardStateUtils.updateTaskName(listsData, newTaskData.updateTaskName.taskID, newTaskData.updateTaskName.name), + ); }, }); const { loading, data } = useFindProjectQuery({ variables: { projectId }, onCompleted: newData => { - const newListsData: State = { tasks: {}, columns: {} }; + const newListsData: BoardState = { tasks: {}, columns: {} }; newData.findProject.taskGroups.forEach(taskGroup => { newListsData.columns[taskGroup.taskGroupID] = { taskGroupID: taskGroup.taskGroupID, @@ -198,37 +148,7 @@ const Project = () => { setListsData(newListsData); }, }); - const onCardDrop = (droppedTask: Task) => { - console.log(droppedTask); - updateTaskLocation({ - variables: { - taskID: droppedTask.taskID, - taskGroupID: droppedTask.taskGroup.taskGroupID, - position: droppedTask.position, - }, - }); - const newState = { - ...listsData, - tasks: { - ...listsData.tasks, - [droppedTask.taskID]: droppedTask, - }, - }; - setListsData(newState); - }; - const onListDrop = (droppedColumn: any) => { - updateTaskGroupLocation({ - variables: { taskGroupID: droppedColumn.taskGroupID, position: droppedColumn.position }, - }); - const newState = { - ...listsData, - columns: { - ...listsData.columns, - [droppedColumn.taskGroupID]: droppedColumn, - }, - }; - setListsData(newState); - }; + const onCardCreate = (taskGroupID: string, name: string) => { const taskGroupTasks = Object.values(listsData.tasks).filter( (task: Task) => task.taskGroup.taskGroupID === taskGroupID, @@ -241,6 +161,7 @@ const Project = () => { createTask({ variables: { taskGroupID, name, position } }); }; + const onQuickEditorOpen = (e: ContextMenuEvent) => { const currentTask = Object.values(listsData.tasks).find(task => task.taskID === e.taskID); setQuickCardEditor({ @@ -250,6 +171,33 @@ const Project = () => { task: currentTask, }); }; + const onCardDrop = (droppedTask: Task) => { + updateTaskLocation({ + variables: { + taskID: droppedTask.taskID, + taskGroupID: droppedTask.taskGroup.taskGroupID, + position: droppedTask.position, + }, + }); + setListsData(BoardStateUtils.updateTask(listsData, droppedTask)); + }; + const onListDrop = (droppedColumn: TaskGroup) => { + updateTaskGroupLocation({ + variables: { taskGroupID: droppedColumn.taskGroupID, position: droppedColumn.position }, + }); + setListsData(BoardStateUtils.updateTaskGroup(listsData, droppedColumn)); + }; + + const onCreateList = (listName: string) => { + const [lastColumn] = Object.values(listsData.columns) + .sort((a, b) => a.position - b.position) + .slice(-1); + let position = 65535; + if (lastColumn) { + position = lastColumn.position * 2 + 1; + } + createTaskGroup({ variables: { projectID: projectId, name: listName, position } }); + }; if (loading) { return Loading; @@ -262,38 +210,35 @@ const Project = () => { {data.findProject.name} - - - { - setTaskDetails({ isOpen: true, taskID: task.taskID }); - }} - onExtraMenuOpen={(taskGroupID, pos, size) => { - setPopupData({ - isOpen: true, - left: pos.left, - top: pos.top + size.height + 5, - taskGroupID, - }); - }} - onQuickEditorOpen={onQuickEditorOpen} - onCardCreate={onCardCreate} - {...listsData} + { - const [lastColumn] = Object.values(listsData.columns) - .sort((a, b) => a.position - b.position) - .slice(-1); - let position = 65535; - if (lastColumn) { - position = lastColumn.position * 2 + 1; - } - createTaskGroup({ variables: { projectID: projectId, name: listName, position } }); + onCardCreate={onCardCreate} + onCreateList={onCreateList} + onQuickEditorOpen={onQuickEditorOpen} + onOpenListActionsPopup={(isOpen, left, top, taskGroupID) => { + setPopupData({ isOpen, top, left, taskGroupID }); }} /> - + + {popupData.isOpen && ( + setPopupData(initialPopupState)} + left={popupData.left} + > + { + deleteTaskGroup({ variables: { taskGroupID } }); + setPopupData(initialPopupState); + }} + /> + + )} {quickCardEditor.isOpen && ( { left={quickCardEditor.left} /> )} - {popupData.isOpen && ( - setPopupData(initialPopupState)} - left={popupData.left} - > - { - deleteTaskGroup({ variables: { taskGroupID } }); - setPopupData(initialPopupState); + ) => ( + { + history.push(match.url); + }} + renderContent={() => { + const task = listsData.tasks[routeProps.match.params.taskID]; + if (!task) { + return
loading
; + } + 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={() => history.push(match.url)} + 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) => {}} + /> + ); }} /> -
- )} - {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/Projects/index.tsx b/web/src/Projects/index.tsx index f28e3aa..dabccca 100644 --- a/web/src/Projects/index.tsx +++ b/web/src/Projects/index.tsx @@ -58,7 +58,7 @@ const Projects = () => { {projects.map(project => ( - + ))} diff --git a/web/src/citadel.d.ts b/web/src/citadel.d.ts index 9cf5130..3b6df43 100644 --- a/web/src/citadel.d.ts +++ b/web/src/citadel.d.ts @@ -1,3 +1,16 @@ +interface ColumnState { + [key: string]: TaskGroup; +} + +interface TaskState { + [key: string]: Task; +} + +interface BoardState { + columns: ColumnState; + tasks: TaskState; +} + interface DraggableElement { id: string; position: number; diff --git a/web/src/shared/utils/boardState.ts b/web/src/shared/utils/boardState.ts new file mode 100644 index 0000000..5c3f0b5 --- /dev/null +++ b/web/src/shared/utils/boardState.ts @@ -0,0 +1,50 @@ +import produce from 'immer'; + +export const addTask = (currentState: BoardState, newTask: Task) => { + return produce(currentState, (draftState: BoardState) => { + currentState.tasks[newTask.taskID] = newTask; + }); +}; + +export const deleteTask = (currentState: BoardState, taskID: string) => { + return produce(currentState, (draftState: BoardState) => { + delete draftState.tasks[taskID]; + }); +}; + +export const addTaskGroup = (currentState: BoardState, newTaskGroup: TaskGroup) => { + return produce(currentState, (draftState: BoardState) => { + draftState.columns[newTaskGroup.taskGroupID] = newTaskGroup; + }); +}; + +export const updateTaskGroup = (currentState: BoardState, newTaskGroup: TaskGroup) => { + return produce(currentState, (draftState: BoardState) => { + draftState.columns[newTaskGroup.taskGroupID] = newTaskGroup; + }); +}; + +export const updateTask = (currentState: BoardState, newTask: Task) => { + return produce(currentState, (draftState: BoardState) => { + draftState.tasks[newTask.taskID] = newTask; + }); +}; + +export const deleteTaskGroup = (currentState: BoardState, deletedTaskGroupID: string) => { + return produce(currentState, (draftState: BoardState) => { + delete draftState.columns[deletedTaskGroupID]; + const filteredTasks = Object.keys(currentState.tasks) + .filter(taskID => currentState.tasks[taskID].taskGroup.taskGroupID !== deletedTaskGroupID) + .reduce((obj: TaskState, key: string) => { + obj[key] = currentState.tasks[key]; + return obj; + }, {}); + draftState.tasks = filteredTasks; + }); +}; + +export const updateTaskName = (currentState: BoardState, taskID: string, newName: string) => { + return produce(currentState, (draftState: BoardState) => { + draftState.tasks[taskID].name = newName; + }); +};