chore: move Kanban code to separate module

This commit is contained in:
Jordan Knott 2020-04-16 15:05:12 -05:00
parent 16eb9e165f
commit beaa215bc2
8 changed files with 243 additions and 206 deletions

View File

@ -16,7 +16,7 @@ const Routes = ({ history }: RoutesProps) => (
<Switch> <Switch>
<Route exact path="/" component={Dashboard} /> <Route exact path="/" component={Dashboard} />
<Route exact path="/projects" component={Projects} /> <Route exact path="/projects" component={Projects} />
<Route exact path="/projects/:projectId" component={Project} /> <Route path="/projects/:projectId" component={Project} />
<Route exact path="/login" component={Login} /> <Route exact path="/login" component={Login} />
</Switch> </Switch>
</Router> </Router>

View File

@ -0,0 +1,5 @@
import styled from 'styled-components';
export const Board = styled.div`
margin-left: 36px;
`;

View File

@ -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<KanbanBoardProps> = ({
listsData,
onOpenListActionsPopup,
onQuickEditorOpen,
onCardCreate,
onCardDrop,
onListDrop,
onCreateList,
}) => {
const match = useRouteMatch();
const history = useHistory();
return (
<Board>
<Lists
onCardClick={task => {
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}
/>
</Board>
);
};
export default KanbanBoard;

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import produce from 'immer'; import * as BoardStateUtils from 'shared/utils/boardState';
import styled from 'styled-components/macro'; import styled from 'styled-components/macro';
import { useParams } from 'react-router-dom'; import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps } from 'react-router-dom';
import { import {
useFindProjectQuery, useFindProjectQuery,
useUpdateTaskNameMutation, useUpdateTaskNameMutation,
@ -15,7 +15,6 @@ import {
import Navbar from 'App/Navbar'; import Navbar from 'App/Navbar';
import TopNavbar from 'App/TopNavbar'; import TopNavbar from 'App/TopNavbar';
import Lists from 'shared/components/Lists';
import QuickCardEditor from 'shared/components/QuickCardEditor'; import QuickCardEditor from 'shared/components/QuickCardEditor';
import PopupMenu from 'shared/components/PopupMenu'; import PopupMenu from 'shared/components/PopupMenu';
import ListActions from 'shared/components/ListActions'; import ListActions from 'shared/components/ListActions';
@ -23,19 +22,11 @@ import Modal from 'shared/components/Modal';
import TaskDetails from 'shared/components/TaskDetails'; import TaskDetails from 'shared/components/TaskDetails';
import MemberManager from 'shared/components/MemberManager'; import MemberManager from 'shared/components/MemberManager';
import { LabelsPopup } from 'shared/components/PopupMenu/PopupMenu.stories'; import { LabelsPopup } from 'shared/components/PopupMenu/PopupMenu.stories';
import KanbanBoard from 'Projects/Project/KanbanBoard';
interface ColumnState { type TaskRouteProps = {
[key: string]: TaskGroup; taskID: string;
} };
interface TaskState {
[key: string]: Task;
}
interface State {
columns: ColumnState;
tasks: TaskState;
}
interface QuickCardEditorState { interface QuickCardEditorState {
isOpen: boolean; isOpen: boolean;
@ -66,15 +57,11 @@ const Title = styled.span`
color: #fff; color: #fff;
`; `;
const Board = styled.div`
margin-left: 36px;
`;
interface ProjectParams { interface ProjectParams {
projectId: string; projectId: string;
} }
const initialState: State = { tasks: {}, columns: {} }; const initialState: BoardState = { tasks: {}, columns: {} };
const initialPopupState = { left: 0, top: 0, isOpen: false, taskGroupID: '' }; const initialPopupState = { left: 0, top: 0, isOpen: false, taskGroupID: '' };
const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top: 0, left: 0 }; const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top: 0, left: 0 };
const initialMemberPopupState = { taskID: '', 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 Project = () => {
const { projectId } = useParams<ProjectParams>(); const { projectId } = useParams<ProjectParams>();
const match = useRouteMatch();
const history = useHistory();
const [listsData, setListsData] = useState(initialState); const [listsData, setListsData] = useState(initialState);
const [popupData, setPopupData] = useState(initialPopupState); const [popupData, setPopupData] = useState(initialPopupState);
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState); const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
@ -90,92 +80,52 @@ const Project = () => {
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState); const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
const [updateTaskLocation] = useUpdateTaskLocationMutation(); const [updateTaskLocation] = useUpdateTaskLocationMutation();
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation(); const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation();
const [deleteTaskGroup] = useDeleteTaskGroupMutation({ const [deleteTaskGroup] = useDeleteTaskGroupMutation({
onCompleted: deletedTaskGroupData => { onCompleted: deletedTaskGroupData => {
const nextState = produce(listsData, (draftState: State) => { setListsData(
delete draftState.columns[deletedTaskGroupData.deleteTaskGroup.taskGroup.taskGroupID]; BoardStateUtils.deleteTaskGroup(listsData, 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);
},
});
const [createTaskGroup] = useCreateTaskGroupMutation({ const [createTaskGroup] = useCreateTaskGroupMutation({
onCompleted: newTaskGroupData => { onCompleted: newTaskGroupData => {
const newListsData = { const newTaskGroup = {
...listsData, ...newTaskGroupData.createTaskGroup,
columns: {
...listsData.columns,
[newTaskGroupData.createTaskGroup.taskGroupID]: {
taskGroupID: newTaskGroupData.createTaskGroup.taskGroupID,
name: newTaskGroupData.createTaskGroup.name,
position: newTaskGroupData.createTaskGroup.position,
tasks: [], tasks: [],
},
},
}; };
setListsData(newListsData); setListsData(BoardStateUtils.addTaskGroup(listsData, newTaskGroup));
}, },
}); });
const [createTask] = useCreateTaskMutation({ const [createTask] = useCreateTaskMutation({
onCompleted: newTaskData => { onCompleted: newTaskData => {
const newListsData = { const newTask = {
...listsData, ...newTaskData.createTask,
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: [], labels: [],
},
},
}; };
setListsData(newListsData); setListsData(BoardStateUtils.addTask(listsData, newTask));
}, },
}); });
const [deleteTask] = useDeleteTaskMutation({ const [deleteTask] = useDeleteTaskMutation({
onCompleted: deletedTask => { onCompleted: deletedTask => {
const { [deletedTask.deleteTask.taskID]: removedTask, ...remainingTasks } = listsData.tasks; setListsData(BoardStateUtils.deleteTask(listsData, deletedTask.deleteTask.taskID));
const newListsData = {
...listsData,
tasks: remainingTasks,
};
setListsData(newListsData);
}, },
}); });
const [updateTaskName] = useUpdateTaskNameMutation({ const [updateTaskName] = useUpdateTaskNameMutation({
onCompleted: newTaskData => { onCompleted: newTaskData => {
const newListsData = { setListsData(
...listsData, BoardStateUtils.updateTaskName(listsData, newTaskData.updateTaskName.taskID, newTaskData.updateTaskName.name),
tasks: { );
...listsData.tasks,
[newTaskData.updateTaskName.taskID]: {
...listsData.tasks[newTaskData.updateTaskName.taskID],
name: newTaskData.updateTaskName.name,
},
},
};
setListsData(newListsData);
}, },
}); });
const { loading, data } = useFindProjectQuery({ const { loading, data } = useFindProjectQuery({
variables: { projectId }, variables: { projectId },
onCompleted: newData => { onCompleted: newData => {
const newListsData: State = { tasks: {}, columns: {} }; const newListsData: BoardState = { tasks: {}, columns: {} };
newData.findProject.taskGroups.forEach(taskGroup => { newData.findProject.taskGroups.forEach(taskGroup => {
newListsData.columns[taskGroup.taskGroupID] = { newListsData.columns[taskGroup.taskGroupID] = {
taskGroupID: taskGroup.taskGroupID, taskGroupID: taskGroup.taskGroupID,
@ -198,37 +148,7 @@ const Project = () => {
setListsData(newListsData); 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 onCardCreate = (taskGroupID: string, name: string) => {
const taskGroupTasks = Object.values(listsData.tasks).filter( const taskGroupTasks = Object.values(listsData.tasks).filter(
(task: Task) => task.taskGroup.taskGroupID === taskGroupID, (task: Task) => task.taskGroup.taskGroupID === taskGroupID,
@ -241,6 +161,7 @@ const Project = () => {
createTask({ variables: { taskGroupID, name, position } }); createTask({ variables: { taskGroupID, name, position } });
}; };
const onQuickEditorOpen = (e: ContextMenuEvent) => { const onQuickEditorOpen = (e: ContextMenuEvent) => {
const currentTask = Object.values(listsData.tasks).find(task => task.taskID === e.taskID); const currentTask = Object.values(listsData.tasks).find(task => task.taskID === e.taskID);
setQuickCardEditor({ setQuickCardEditor({
@ -250,6 +171,33 @@ const Project = () => {
task: currentTask, 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) { if (loading) {
return <Wrapper>Loading</Wrapper>; return <Wrapper>Loading</Wrapper>;
@ -262,38 +210,35 @@ const Project = () => {
<TopNavbar /> <TopNavbar />
<TitleWrapper> <TitleWrapper>
<Title>{data.findProject.name}</Title> <Title>{data.findProject.name}</Title>
</TitleWrapper> <KanbanBoard
<Board> listsData={listsData}
<Lists
onCardClick={task => {
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}
onCardDrop={onCardDrop} onCardDrop={onCardDrop}
onListDrop={onListDrop} onListDrop={onListDrop}
onCreateList={listName => { onCardCreate={onCardCreate}
const [lastColumn] = Object.values(listsData.columns) onCreateList={onCreateList}
.sort((a, b) => a.position - b.position) onQuickEditorOpen={onQuickEditorOpen}
.slice(-1); onOpenListActionsPopup={(isOpen, left, top, taskGroupID) => {
let position = 65535; setPopupData({ isOpen, top, left, taskGroupID });
if (lastColumn) {
position = lastColumn.position * 2 + 1;
}
createTaskGroup({ variables: { projectID: projectId, name: listName, position } });
}} }}
/> />
</Board> </TitleWrapper>
</MainContent> </MainContent>
{popupData.isOpen && (
<PopupMenu
title="List Actions"
top={popupData.top}
onClose={() => setPopupData(initialPopupState)}
left={popupData.left}
>
<ListActions
taskGroupID={popupData.taskGroupID}
onArchiveTaskGroup={taskGroupID => {
deleteTaskGroup({ variables: { taskGroupID } });
setPopupData(initialPopupState);
}}
/>
</PopupMenu>
)}
{quickCardEditor.isOpen && ( {quickCardEditor.isOpen && (
<QuickCardEditor <QuickCardEditor
isOpen isOpen
@ -311,44 +256,19 @@ const Project = () => {
left={quickCardEditor.left} left={quickCardEditor.left}
/> />
)} )}
{popupData.isOpen && ( <Route
<PopupMenu path={`${match.path}/c/:taskID`}
title="List Actions" render={(routeProps: RouteComponentProps<TaskRouteProps>) => (
top={popupData.top}
onClose={() => setPopupData(initialPopupState)}
left={popupData.left}
>
<ListActions
taskGroupID={popupData.taskGroupID}
onArchiveTaskGroup={taskGroupID => {
deleteTaskGroup({ variables: { taskGroupID } });
setPopupData(initialPopupState);
}}
/>
</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 <Modal
width={1040} width={1040}
onClose={() => { onClose={() => {
setTaskDetails(initialTaskDetailsState); history.push(match.url);
}} }}
renderContent={() => { renderContent={() => {
const task = listsData.tasks[taskDetails.taskID]; const task = listsData.tasks[routeProps.match.params.taskID];
if (!task) {
return <div>loading</div>;
}
return ( return (
<TaskDetails <TaskDetails
task={task} task={task}
@ -362,7 +282,7 @@ const Project = () => {
setTaskDetails(initialTaskDetailsState); setTaskDetails(initialTaskDetailsState);
deleteTask({ variables: { taskID: deletedTask.taskID } }); deleteTask({ variables: { taskID: deletedTask.taskID } });
}} }}
onCloseModal={() => setTaskDetails(initialTaskDetailsState)} onCloseModal={() => history.push(match.url)}
onOpenAddMemberPopup={(task, bounds) => { onOpenAddMemberPopup={(task, bounds) => {
console.log(task, bounds); console.log(task, bounds);
setMemberPopupData({ setMemberPopupData({
@ -378,6 +298,7 @@ const Project = () => {
}} }}
/> />
)} )}
/>
</> </>
); );
} }

View File

@ -58,7 +58,7 @@ const Projects = () => {
<TopNavbar /> <TopNavbar />
<ProjectGrid> <ProjectGrid>
{projects.map(project => ( {projects.map(project => (
<Link to={`/projects/${project.projectID}/`}> <Link to={`/projects/${project.projectID}`}>
<ProjectGridItem project={project} /> <ProjectGridItem project={project} />
</Link> </Link>
))} ))}

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

@ -1,3 +1,16 @@
interface ColumnState {
[key: string]: TaskGroup;
}
interface TaskState {
[key: string]: Task;
}
interface BoardState {
columns: ColumnState;
tasks: TaskState;
}
interface DraggableElement { interface DraggableElement {
id: string; id: string;
position: number; position: number;

View File

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