taskcafe/web/src/Projects/Project/index.tsx

563 lines
18 KiB
TypeScript
Raw Normal View History

2020-05-27 02:53:31 +02:00
import React, { useState, useRef } from 'react';
import * as BoardStateUtils from 'shared/utils/boardState';
2020-05-27 02:53:31 +02:00
import GlobalTopNavbar from 'App/TopNavbar';
2020-04-10 04:40:22 +02:00
import styled from 'styled-components/macro';
2020-05-27 02:53:31 +02:00
import { Bolt, ToggleOn, Tags } from 'shared/icons';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps } from 'react-router-dom';
import {
useFindProjectQuery,
useUpdateTaskNameMutation,
useUpdateProjectLabelMutation,
useCreateTaskMutation,
useDeleteProjectLabelMutation,
useDeleteTaskMutation,
useUpdateTaskLocationMutation,
useUpdateTaskGroupLocationMutation,
useCreateTaskGroupMutation,
2020-04-11 21:24:45 +02:00
useDeleteTaskGroupMutation,
2020-04-20 05:02:55 +02:00
useUpdateTaskDescriptionMutation,
useAssignTaskMutation,
2020-05-27 02:53:31 +02:00
DeleteTaskDocument,
FindProjectDocument,
2020-05-27 23:18:50 +02:00
useCreateProjectLabelMutation,
} from 'shared/generated/graphql';
2020-04-10 04:40:22 +02:00
2020-05-27 23:18:50 +02:00
import TaskAssignee from 'shared/components/TaskAssignee';
2020-04-10 04:40:22 +02:00
import QuickCardEditor from 'shared/components/QuickCardEditor';
import ListActions from 'shared/components/ListActions';
2020-04-13 00:45:51 +02:00
import MemberManager from 'shared/components/MemberManager';
import { LabelsPopup } from 'shared/components/PopupMenu/PopupMenu.stories';
import KanbanBoard from 'Projects/Project/KanbanBoard';
2020-05-27 02:53:31 +02:00
import { mixin } from 'shared/utils/styles';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
import produce from 'immer';
2020-05-27 23:18:50 +02:00
import MiniProfile from 'shared/components/MiniProfile';
import Details from './Details';
const getCacheData = (client: any, projectID: string) => {
const cacheData: any = client.readQuery({
query: FindProjectDocument,
variables: {
projectId: projectID,
},
});
return cacheData;
};
const writeCacheData = (client: any, projectID: string, cacheData: any, newData: any) => {
client.writeQuery({
query: FindProjectDocument,
variables: {
projectId: projectID,
},
data: { ...cacheData, findProject: newData },
});
};
2020-04-10 04:40:22 +02:00
type TaskRouteProps = {
taskID: string;
};
2020-04-10 04:40:22 +02:00
interface QuickCardEditorState {
isOpen: boolean;
left: number;
top: number;
task?: Task;
2020-04-10 04:40:22 +02:00
}
2020-04-10 18:31:29 +02:00
const TitleWrapper = styled.div`
margin-left: 38px;
margin-bottom: 15px;
`;
2020-04-10 04:40:22 +02:00
const Title = styled.span`
text-align: center;
font-size: 24px;
color: #fff;
`;
2020-05-27 23:18:50 +02:00
const ProjectMembers = styled.div`
display: flex;
padding-left: 4px;
padding-top: 4px;
align-items: center;
`;
2020-04-10 04:40:22 +02:00
2020-05-27 02:53:31 +02:00
type LabelManagerEditorProps = {
labels: React.RefObject<Array<Label>>;
2020-05-27 23:18:50 +02:00
projectID: string;
labelColors: Array<LabelColor>;
2020-05-27 02:53:31 +02:00
};
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ labels: labelsRef, projectID, labelColors }) => {
2020-05-27 02:53:31 +02:00
const [currentLabel, setCurrentLabel] = useState('');
const [createProjectLabel] = useCreateProjectLabelMutation({
update: (client, newLabelData) => {
const cacheData = getCacheData(client, projectID);
const newData = {
...cacheData.findProject,
labels: [...cacheData.findProject.labels, { ...newLabelData.data.createProjectLabel }],
};
writeCacheData(client, projectID, cacheData, newData);
},
});
const [updateProjectLabel] = useUpdateProjectLabelMutation();
const [deleteProjectLabel] = useDeleteProjectLabelMutation({
update: (client, newLabelData) => {
const cacheData = getCacheData(client, projectID);
const newData = {
...cacheData.findProject,
labels: cacheData.findProject.labels.filter(
(label: any) => label.id !== newLabelData.data.deleteProjectLabel.id,
),
};
writeCacheData(client, projectID, cacheData, newData);
},
});
const labels = labelsRef.current ? labelsRef.current : [];
2020-05-27 02:53:31 +02:00
const { setTab } = usePopup();
return (
<>
<Popup title="Labels" tab={0} onClose={() => {}}>
<LabelManager
labels={labels}
onLabelCreate={() => {
setTab(2);
}}
onLabelEdit={labelId => {
setCurrentLabel(labelId);
setTab(1);
}}
onLabelToggle={labelId => {
2020-05-27 23:18:50 +02:00
setCurrentLabel(labelId);
setTab(1);
2020-05-27 02:53:31 +02:00
}}
/>
</Popup>
<Popup onClose={() => {}} title="Edit label" tab={1}>
<LabelEditor
2020-05-27 23:18:50 +02:00
labelColors={labelColors}
2020-05-27 02:53:31 +02:00
label={labels.find(label => label.labelId === currentLabel) ?? null}
onLabelEdit={(projectLabelID, name, color) => {
if (projectLabelID) {
updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name } });
}
setTab(0);
}}
onLabelDelete={labelID => {
deleteProjectLabel({ variables: { projectLabelID: labelID } });
2020-05-27 02:53:31 +02:00
setTab(0);
}}
/>
</Popup>
<Popup onClose={() => {}} title="Create new label" tab={2}>
<LabelEditor
2020-05-27 23:18:50 +02:00
labelColors={labelColors}
2020-05-27 02:53:31 +02:00
label={null}
onLabelEdit={(_labelId, name, color) => {
2020-05-27 23:18:50 +02:00
createProjectLabel({ variables: { projectID, labelColorID: color.id, name } });
2020-05-27 02:53:31 +02:00
setTab(0);
}}
/>
</Popup>
</>
);
};
2020-04-10 04:40:22 +02:00
interface ProjectParams {
projectID: string;
2020-04-10 04:40:22 +02:00
}
const initialState: BoardState = { tasks: {}, columns: {} };
const initialPopupState = { left: 0, top: 0, isOpen: false, taskGroupID: '' };
2020-04-10 04:40:22 +02:00
const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top: 0, left: 0 };
2020-04-13 00:45:51 +02:00
const initialTaskDetailsState = { isOpen: false, taskID: '' };
2020-04-10 04:40:22 +02:00
2020-05-27 23:18:50 +02:00
const ProjectBar = styled.div`
2020-05-27 02:53:31 +02:00
display: flex;
align-items: center;
justify-content: flex-end;
height: 40px;
padding: 0 12px;
`;
2020-05-27 23:18:50 +02:00
const ProjectActions = styled.div`
display: flex;
align-items: center;
`;
2020-05-27 02:53:31 +02:00
const ProjectAction = styled.div`
cursor: pointer;
display: flex;
align-items: center;
font-size: 15px;
color: #c2c6dc;
&:not(:last-child) {
margin-right: 16px;
}
&:hover {
color: ${mixin.lighten('#c2c6dc', 0.25)};
}
`;
const ProjectActionText = styled.span`
padding-left: 4px;
`;
2020-04-10 04:40:22 +02:00
const Project = () => {
const { projectID } = useParams<ProjectParams>();
const match = useRouteMatch();
2020-04-20 05:02:55 +02:00
const [updateTaskDescription] = useUpdateTaskDescriptionMutation();
2020-04-10 04:40:22 +02:00
const [listsData, setListsData] = useState(initialState);
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
const [updateTaskLocation] = useUpdateTaskLocationMutation();
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation();
2020-04-11 21:24:45 +02:00
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
onCompleted: deletedTaskGroupData => {
2020-05-27 02:53:31 +02:00
setListsData(BoardStateUtils.deleteTaskGroup(listsData, deletedTaskGroupData.deleteTaskGroup.taskGroup.id));
2020-04-11 21:24:45 +02:00
},
update: (client, deletedTaskGroupData) => {
const cacheData = getCacheData(client, projectID);
const newData = {
...cacheData.findProject,
taskGroups: cacheData.findProject.taskGroups.filter(
(taskGroup: any) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
),
};
writeCacheData(client, projectID, cacheData, newData);
},
2020-04-11 21:24:45 +02:00
});
const [createTaskGroup] = useCreateTaskGroupMutation({
onCompleted: newTaskGroupData => {
const newTaskGroup = {
2020-05-27 02:53:31 +02:00
taskGroupID: newTaskGroupData.createTaskGroup.id,
tasks: [],
2020-05-27 02:53:31 +02:00
...newTaskGroupData.createTaskGroup,
};
setListsData(BoardStateUtils.addTaskGroup(listsData, newTaskGroup));
},
update: (client, newTaskGroupData) => {
const cacheData = getCacheData(client, projectID);
const newData = {
...cacheData.findProject,
taskGroups: [...cacheData.findProject.taskGroups, { ...newTaskGroupData.data.createTaskGroup, tasks: [] }],
};
writeCacheData(client, projectID, cacheData, newData);
},
});
const [createTask] = useCreateTaskMutation({
2020-04-10 04:40:22 +02:00
onCompleted: newTaskData => {
const newTask = {
...newTaskData.createTask,
2020-05-27 02:53:31 +02:00
taskID: newTaskData.createTask.id,
taskGroup: { taskGroupID: newTaskData.createTask.taskGroup.id },
labels: [],
2020-04-10 04:40:22 +02:00
};
setListsData(BoardStateUtils.addTask(listsData, newTask));
2020-04-10 04:40:22 +02:00
},
2020-05-27 02:53:31 +02:00
update: (client, newTaskData) => {
const cacheData = getCacheData(client, projectID);
2020-05-27 02:53:31 +02:00
const newTaskGroups = produce(cacheData.findProject.taskGroups, (draftState: any) => {
const targetIndex = draftState.findIndex(
(taskGroup: any) => taskGroup.id === newTaskData.data.createTask.taskGroup.id,
);
draftState[targetIndex] = {
...draftState[targetIndex],
tasks: [...draftState[targetIndex].tasks, { ...newTaskData.data.createTask }],
};
});
const newData = {
...cacheData.findProject,
taskGroups: newTaskGroups,
};
writeCacheData(client, projectID, cacheData, newData);
2020-05-27 02:53:31 +02:00
},
2020-04-10 04:40:22 +02:00
});
const [deleteTask] = useDeleteTaskMutation({
2020-04-10 04:40:22 +02:00
onCompleted: deletedTask => {
setListsData(BoardStateUtils.deleteTask(listsData, deletedTask.deleteTask.taskID));
2020-04-10 04:40:22 +02:00
},
});
const [updateTaskName] = useUpdateTaskNameMutation({
2020-04-10 04:40:22 +02:00
onCompleted: newTaskData => {
setListsData(
2020-05-27 02:53:31 +02:00
BoardStateUtils.updateTaskName(listsData, newTaskData.updateTaskName.id, newTaskData.updateTaskName.name),
);
2020-04-10 04:40:22 +02:00
},
});
2020-04-21 01:04:27 +02:00
const { loading, data, refetch } = useFindProjectQuery({
variables: { projectId: projectID },
2020-04-10 04:40:22 +02:00
});
2020-04-10 04:40:22 +02:00
const onCardCreate = (taskGroupID: string, name: string) => {
const taskGroupTasks = Object.values(listsData.tasks).filter(
(task: Task) => task.taskGroup.taskGroupID === taskGroupID,
);
2020-04-10 22:31:12 +02:00
let position = 65535;
2020-04-10 04:40:22 +02:00
if (taskGroupTasks.length !== 0) {
const [lastTask] = taskGroupTasks.sort((a: any, b: any) => a.position - b.position).slice(-1);
position = Math.ceil(lastTask.position) * 2 + 1;
}
2020-04-10 22:31:12 +02:00
createTask({ variables: { taskGroupID, name, position } });
2020-04-10 04:40:22 +02:00
};
const onCardDrop = (droppedTask: Task) => {
updateTaskLocation({
variables: {
taskID: droppedTask.taskID,
taskGroupID: droppedTask.taskGroup.taskGroupID,
position: droppedTask.position,
},
optimisticResponse: {
updateTaskLocation: {
name: droppedTask.name,
id: droppedTask.taskID,
position: droppedTask.position,
createdAt: '',
},
},
});
setListsData(BoardStateUtils.updateTask(listsData, droppedTask));
};
const onListDrop = (droppedColumn: TaskGroup) => {
console.log(`list drop ${droppedColumn.taskGroupID}`);
updateTaskGroupLocation({
variables: { taskGroupID: droppedColumn.taskGroupID, position: droppedColumn.position },
optimisticResponse: {
updateTaskGroupLocation: {
id: 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, name: listName, position } });
};
2020-04-10 04:40:22 +02:00
2020-04-20 05:02:55 +02:00
const [assignTask] = useAssignTaskMutation();
const { showPopup, hidePopup } = usePopup();
2020-05-27 02:53:31 +02:00
const $labelsRef = useRef<HTMLDivElement>(null);
const labelsRef = useRef<Array<Label>>([]);
2020-04-10 04:40:22 +02:00
if (loading) {
2020-05-27 02:53:31 +02:00
return (
<>
<GlobalTopNavbar name="Project" />
<Title>Error Loading</Title>
</>
);
2020-04-10 04:40:22 +02:00
}
if (data) {
console.log(data);
2020-05-27 02:53:31 +02:00
const currentListsData: BoardState = { tasks: {}, columns: {} };
data.findProject.taskGroups.forEach(taskGroup => {
currentListsData.columns[taskGroup.id] = {
taskGroupID: taskGroup.id,
name: taskGroup.name,
position: taskGroup.position,
tasks: [],
};
taskGroup.tasks.forEach(task => {
const taskMembers = task.assigned.map(assigned => {
return {
userID: assigned.id,
displayName: `${assigned.firstName} ${assigned.lastName}`,
profileIcon: {
url: null,
initials: assigned.profileIcon.initials ?? '',
bgColor: assigned.profileIcon.bgColor ?? '#7367F0',
},
};
});
currentListsData.tasks[task.id] = {
taskID: task.id,
taskGroup: {
taskGroupID: taskGroup.id,
},
name: task.name,
labels: [],
position: task.position,
description: task.description ?? undefined,
members: taskMembers,
};
});
});
2020-04-20 05:02:55 +02:00
const availableMembers = data.findProject.members.map(member => {
return {
displayName: `${member.firstName} ${member.lastName}`,
2020-04-21 01:04:27 +02:00
profileIcon: {
url: null,
initials: member.profileIcon.initials ?? null,
bgColor: member.profileIcon.bgColor ?? null,
},
2020-05-27 02:53:31 +02:00
userID: member.id,
2020-04-20 05:02:55 +02:00
};
});
2020-05-27 02:53:31 +02:00
const onQuickEditorOpen = (e: ContextMenuEvent) => {
const currentTask = Object.values(currentListsData.tasks).find(task => task.taskID === e.taskID);
setQuickCardEditor({
top: e.top,
left: e.left,
isOpen: true,
task: currentTask,
});
};
2020-05-27 23:18:50 +02:00
labelsRef.current = data.findProject.labels.map(label => {
return {
labelId: label.id,
name: label.name ?? '',
labelColor: label.labelColor,
active: false,
};
});
2020-04-10 04:40:22 +02:00
return (
<>
2020-05-27 23:18:50 +02:00
<GlobalTopNavbar projectMembers={availableMembers} name={data.findProject.name} />
<ProjectBar>
<ProjectActions>
<ProjectAction
ref={$labelsRef}
onClick={() => {
showPopup(
$labelsRef,
<LabelManagerEditor labelColors={data.labelColors} labels={labelsRef} projectID={projectID} />,
2020-05-27 23:18:50 +02:00
);
}}
>
<Tags size={13} color="#c2c6dc" />
<ProjectActionText>Labels</ProjectActionText>
</ProjectAction>
<ProjectAction>
<ToggleOn size={13} color="#c2c6dc" />
<ProjectActionText>Fields</ProjectActionText>
</ProjectAction>
<ProjectAction>
<Bolt size={13} color="#c2c6dc" />
<ProjectActionText>Rules</ProjectActionText>
</ProjectAction>
</ProjectActions>
</ProjectBar>
2020-04-20 05:02:55 +02:00
<KanbanBoard
2020-05-27 02:53:31 +02:00
listsData={currentListsData}
2020-04-20 05:02:55 +02:00
onCardDrop={onCardDrop}
onListDrop={onListDrop}
onCardCreate={onCardCreate}
onCreateList={onCreateList}
2020-05-27 23:18:50 +02:00
onCardMemberClick={($targetRef, taskID, memberID) => {
showPopup(
$targetRef,
<Popup title={null} onClose={() => {}} tab={0}>
<MiniProfile
profileIcon={availableMembers[0].profileIcon}
displayName="Jordan Knott"
username="@jordanthedev"
bio="None"
onRemoveFromTask={() => {
/* unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); */
}}
/>
</Popup>,
);
}}
2020-04-20 05:02:55 +02:00
onQuickEditorOpen={onQuickEditorOpen}
2020-05-27 02:53:31 +02:00
onOpenListActionsPopup={($targetRef, taskGroupID) => {
showPopup(
$targetRef,
<Popup title="List actions" tab={0} onClose={() => {}}>
<ListActions
taskGroupID={taskGroupID}
onArchiveTaskGroup={tgID => {
deleteTaskGroup({ variables: { taskGroupID: tgID } });
hidePopup();
2020-05-27 02:53:31 +02:00
}}
/>
</Popup>,
);
2020-04-20 05:02:55 +02:00
}}
/>
{quickCardEditor.isOpen && (
<QuickCardEditor
isOpen
taskID={quickCardEditor.task ? quickCardEditor.task.taskID : ''}
taskGroupID={quickCardEditor.task ? quickCardEditor.task.taskGroup.taskGroupID : ''}
cardTitle={quickCardEditor.task ? quickCardEditor.task.name : ''}
onCloseEditor={() => setQuickCardEditor(initialQuickCardEditorState)}
onEditCard={(_listId: string, cardId: string, cardName: string) => {
updateTaskName({ variables: { taskID: cardId, name: cardName } });
2020-04-13 00:45:51 +02:00
}}
onOpenPopup={() => {}}
2020-05-27 02:53:31 +02:00
onArchiveCard={(_listId: string, cardId: string) =>
deleteTask({
variables: { taskID: cardId },
update: client => {
const cacheData = getCacheData(client, projectID);
2020-05-27 02:53:31 +02:00
const newData = {
...cacheData.findProject,
taskGroups: cacheData.findProject.taskGroups.map((taskGroup: any) => {
return {
...taskGroup,
tasks: taskGroup.tasks.filter((t: any) => t.id !== cardId),
};
}),
};
writeCacheData(client, projectID, cacheData, newData);
2020-05-27 02:53:31 +02:00
},
})
}
labels={[]}
top={quickCardEditor.top}
left={quickCardEditor.left}
2020-04-13 00:45:51 +02:00
/>
)}
<Route
path={`${match.path}/c/:taskID`}
render={(routeProps: RouteComponentProps<TaskRouteProps>) => (
2020-04-20 05:02:55 +02:00
<Details
refreshCache={() => {}}
2020-04-20 05:02:55 +02:00
availableMembers={availableMembers}
projectURL={match.url}
taskID={routeProps.match.params.taskID}
onTaskNameChange={(updatedTask, newName) => {
updateTaskName({ variables: { taskID: updatedTask.taskID, name: newName } });
}}
onTaskDescriptionChange={(updatedTask, newDescription) => {
updateTaskDescription({ variables: { taskID: updatedTask.taskID, description: newDescription } });
}}
2020-04-20 05:02:55 +02:00
onDeleteTask={deletedTask => {
deleteTask({ variables: { taskID: deletedTask.taskID } });
}}
2020-05-27 02:53:31 +02:00
onOpenAddLabelPopup={(task, $targetRef) => {}}
/>
)}
/>
2020-04-10 04:40:22 +02:00
</>
);
}
2020-04-20 05:02:55 +02:00
return <div>Error</div>;
2020-04-10 04:40:22 +02:00
};
export default Project;