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;
+ });
+};