bugfix: fix user checklist item toggle completion

This commit is contained in:
Jordan Knott 2020-07-13 16:21:37 -05:00
parent d8daa60729
commit 1e9813601e
4 changed files with 551 additions and 525 deletions

View File

@ -1,26 +1,26 @@
import React, {useState, useContext, useEffect} from 'react'; import React, { useState, useContext, useEffect } from 'react';
import Modal from 'shared/components/Modal'; import Modal from 'shared/components/Modal';
import TaskDetails from 'shared/components/TaskDetails'; import TaskDetails from 'shared/components/TaskDetails';
import PopupMenu, {Popup, usePopup} from 'shared/components/PopupMenu'; import PopupMenu, { Popup, usePopup } from 'shared/components/PopupMenu';
import MemberManager from 'shared/components/MemberManager'; import MemberManager from 'shared/components/MemberManager';
import {useRouteMatch, useHistory} from 'react-router'; import { useRouteMatch, useHistory } from 'react-router';
import { import {
useDeleteTaskChecklistMutation, useDeleteTaskChecklistMutation,
useUpdateTaskChecklistNameMutation, useUpdateTaskChecklistNameMutation,
useUpdateTaskChecklistItemLocationMutation, useUpdateTaskChecklistItemLocationMutation,
useCreateTaskChecklistMutation, useCreateTaskChecklistMutation,
useFindTaskQuery, useFindTaskQuery,
useUpdateTaskDueDateMutation, useUpdateTaskDueDateMutation,
useSetTaskCompleteMutation, useSetTaskCompleteMutation,
useAssignTaskMutation, useAssignTaskMutation,
useUnassignTaskMutation, useUnassignTaskMutation,
useSetTaskChecklistItemCompleteMutation, useSetTaskChecklistItemCompleteMutation,
useUpdateTaskChecklistLocationMutation, useUpdateTaskChecklistLocationMutation,
useDeleteTaskChecklistItemMutation, useDeleteTaskChecklistItemMutation,
useUpdateTaskChecklistItemNameMutation, useUpdateTaskChecklistItemNameMutation,
useCreateTaskChecklistItemMutation, useCreateTaskChecklistItemMutation,
FindTaskDocument, FindTaskDocument,
FindTaskQuery, FindTaskQuery,
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
import UserIDContext from 'App/context'; import UserIDContext from 'App/context';
import MiniProfile from 'shared/components/MiniProfile'; import MiniProfile from 'shared/components/MiniProfile';
@ -29,27 +29,27 @@ import produce from 'immer';
import styled from 'styled-components'; import styled from 'styled-components';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import Input from 'shared/components/Input'; import Input from 'shared/components/Input';
import {useForm} from 'react-hook-form'; import { useForm } from 'react-hook-form';
import updateApolloCache from 'shared/utils/cache'; import updateApolloCache from 'shared/utils/cache';
const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => { const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => {
const total = checklists.reduce((prev: any, next: any) => { const total = checklists.reduce((prev: any, next: any) => {
return ( return (
prev + prev +
next.items.reduce((innerPrev: any, _item: any) => { next.items.reduce((innerPrev: any, _item: any) => {
return innerPrev + 1; return innerPrev + 1;
}, 0) }, 0)
);
}, 0);
const complete = checklists.reduce(
(prev: any, next: any) =>
prev +
next.items.reduce((innerPrev: any, item: any) => {
return innerPrev + (item.complete ? 1 : 0);
}, 0),
0,
); );
}, 0); return { total, complete };
const complete = checklists.reduce(
(prev: any, next: any) =>
prev +
next.items.reduce((innerPrev: any, item: any) => {
return innerPrev + (item.complete ? 1 : 0);
}, 0),
0,
);
return {total, complete};
}; };
const DeleteChecklistButton = styled(Button)` const DeleteChecklistButton = styled(Button)`
@ -58,7 +58,7 @@ const DeleteChecklistButton = styled(Button)`
margin-top: 8px; margin-top: 8px;
`; `;
type CreateChecklistData = { type CreateChecklistData = {
name: string; name: string;
}; };
const CreateChecklistForm = styled.form` const CreateChecklistForm = styled.form`
display: flex; display: flex;
@ -80,423 +80,437 @@ const InputError = styled.span`
font-size: 12px; font-size: 12px;
`; `;
type CreateChecklistPopupProps = { type CreateChecklistPopupProps = {
onCreateChecklist: (data: CreateChecklistData) => void; onCreateChecklist: (data: CreateChecklistData) => void;
}; };
const CreateChecklistPopup: React.FC<CreateChecklistPopupProps> = ({onCreateChecklist}) => { const CreateChecklistPopup: React.FC<CreateChecklistPopupProps> = ({ onCreateChecklist }) => {
const {register, handleSubmit, errors} = useForm<CreateChecklistData>(); const { register, handleSubmit, errors } = useForm<CreateChecklistData>();
const createUser = (data: CreateChecklistData) => { const createUser = (data: CreateChecklistData) => {
onCreateChecklist(data); onCreateChecklist(data);
}; };
console.log(errors); console.log(errors);
return ( return (
<CreateChecklistForm onSubmit={handleSubmit(createUser)}> <CreateChecklistForm onSubmit={handleSubmit(createUser)}>
<CreateChecklistInput <CreateChecklistInput
floatingLabel floatingLabel
value="Checklist" value="Checklist"
width="100%" width="100%"
label="Name" label="Name"
id="name" id="name"
name="name" name="name"
variant="alternate" variant="alternate"
ref={register({required: 'Checklist name is required'})} ref={register({ required: 'Checklist name is required' })}
/> />
<CreateChecklistButton type="submit">Create</CreateChecklistButton> <CreateChecklistButton type="submit">Create</CreateChecklistButton>
</CreateChecklistForm> </CreateChecklistForm>
); );
}; };
type DetailsProps = { type DetailsProps = {
taskID: string; taskID: string;
projectURL: string; projectURL: string;
onTaskNameChange: (task: Task, newName: string) => void; onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void; onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void; onDeleteTask: (task: Task) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void; onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
availableMembers: Array<TaskUser>; availableMembers: Array<TaskUser>;
refreshCache: () => void; refreshCache: () => void;
}; };
const initialMemberPopupState = {taskID: '', isOpen: false, top: 0, left: 0}; const initialMemberPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const Details: React.FC<DetailsProps> = ({ const Details: React.FC<DetailsProps> = ({
projectURL, projectURL,
taskID, taskID,
onTaskNameChange, onTaskNameChange,
onTaskDescriptionChange, onTaskDescriptionChange,
onDeleteTask, onDeleteTask,
onOpenAddLabelPopup, onOpenAddLabelPopup,
availableMembers, availableMembers,
refreshCache, refreshCache,
}) => { }) => {
const {userID} = useContext(UserIDContext); const { userID } = useContext(UserIDContext);
const {showPopup, hidePopup} = usePopup(); const { showPopup, hidePopup } = usePopup();
const history = useHistory(); const history = useHistory();
const match = useRouteMatch(); const match = useRouteMatch();
const [currentMemberTask, setCurrentMemberTask] = useState(''); const [currentMemberTask, setCurrentMemberTask] = useState('');
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState); const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
const [updateTaskChecklistLocation] = useUpdateTaskChecklistLocationMutation(); const [updateTaskChecklistLocation] = useUpdateTaskChecklistLocationMutation();
const [updateTaskChecklistItemLocation] = useUpdateTaskChecklistItemLocationMutation({ const [updateTaskChecklistItemLocation] = useUpdateTaskChecklistItemLocationMutation({
update: (client, response) => { update: (client, response) => {
updateApolloCache<FindTaskQuery>( updateApolloCache<FindTaskQuery>(
client, client,
FindTaskDocument, FindTaskDocument,
cache => cache =>
produce(cache, draftCache => { produce(cache, draftCache => {
const {prevChecklistID, checklistID, checklistItem} = response.data.updateTaskChecklistItemLocation; const { prevChecklistID, checklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
console.log(`${checklistID} !== ${prevChecklistID}`); console.log(`${checklistID} !== ${prevChecklistID}`);
if (checklistID !== prevChecklistID) { if (checklistID !== prevChecklistID) {
const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID); const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID);
const newIdx = cache.findTask.checklists.findIndex(c => c.id === checklistID); const newIdx = cache.findTask.checklists.findIndex(c => c.id === checklistID);
console.log(`oldIdx ${oldIdx} newIdx ${newIdx}`); console.log(`oldIdx ${oldIdx} newIdx ${newIdx}`);
if (oldIdx > -1 && newIdx > -1) { if (oldIdx > -1 && newIdx > -1) {
const item = cache.findTask.checklists[oldIdx].items.find(item => item.id === checklistItem.id); const item = cache.findTask.checklists[oldIdx].items.find(item => item.id === checklistItem.id);
console.log(item); console.log(item);
if (item) { if (item) {
draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter( draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter(
i => i.id !== checklistItem.id, i => i.id !== checklistItem.id,
); );
draftCache.findTask.checklists[newIdx].items.push({ draftCache.findTask.checklists[newIdx].items.push({
...item, ...item,
position: checklistItem.position, position: checklistItem.position,
taskChecklistID: checklistID, taskChecklistID: checklistID,
}); });
} }
} }
}
}),
{taskID},
);
},
});
const [setTaskChecklistItemComplete] = useSetTaskChecklistItemCompleteMutation({
update: client => {
updateApolloCache<FindTaskQuery>(
client,
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const {complete, total} = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
total,
};
}),
{taskID},
);
},
});
const [deleteTaskChecklist] = useDeleteTaskChecklistMutation({
update: (client, deleteData) => {
updateApolloCache<FindTaskQuery>(
client,
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const {checklists} = cache.findTask;
console.log(deleteData)
draftCache.findTask.checklists = checklists.filter(c => c.id !== deleteData.data.deleteTaskChecklist.taskChecklist.id);
const {complete, total} = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
total,
};
if (complete === 0 && total === 0) {
draftCache.findTask.badges.checklist = null;
}
}),
{taskID},
);
},
});
const [updateTaskChecklistItemName] = useUpdateTaskChecklistItemNameMutation();
const [createTaskChecklist] = useCreateTaskChecklistMutation({
update: (client, createData) => {
updateApolloCache<FindTaskQuery>(
client,
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const item = createData.data.createTaskChecklist;
draftCache.findTask.checklists.push({...item});
}),
{taskID},
);
},
});
const [updateTaskChecklistName] = useUpdateTaskChecklistNameMutation();
const [deleteTaskChecklistItem] = useDeleteTaskChecklistItemMutation({
update: (client, deleteData) => {
updateApolloCache<FindTaskQuery>(
client,
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID)
if (targetIdx > -1) {
draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(c => item.id !== c.id);
}
const {complete, total} = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
total,
};
}),
{taskID},
);
},
});
const [createTaskChecklistItem] = useCreateTaskChecklistItemMutation({
update: (client, newTaskItem) => {
updateApolloCache<FindTaskQuery>(
client,
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const item = newTaskItem.data.createTaskChecklistItem;
const {checklists} = cache.findTask;
const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
if (idx !== -1) {
draftCache.findTask.checklists[idx].items.push({...item});
const {complete, total} = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
total,
};
}
}),
{taskID},
);
},
});
const {loading, data, refetch} = useFindTaskQuery({variables: {taskID}});
const [setTaskComplete] = useSetTaskCompleteMutation();
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => {
refetch();
refreshCache();
},
});
const [assignTask] = useAssignTaskMutation({
onCompleted: () => {
refetch();
refreshCache();
},
});
const [unassignTask] = useUnassignTaskMutation({
onCompleted: () => {
refetch();
refreshCache();
},
});
if (loading) {
return <div>loading</div>;
}
if (!data) {
return <div>loading</div>;
}
return (
<>
<Modal
width={768}
onClose={() => {
history.push(projectURL);
}}
renderContent={() => {
return (
<TaskDetails
task={data.findTask}
onChecklistDrop={checklist => {
updateTaskChecklistLocation({
variables: {checklistID: checklist.id, position: checklist.position},
optimisticResponse: {
__typename: 'Mutation',
updateTaskChecklistLocation: {
__typename: 'UpdateTaskChecklistLocationPayload',
checklist: {
__typename: 'TaskChecklist',
position: checklist.position,
id: checklist.id,
},
},
},
});
}}
onChecklistItemDrop={(prevChecklistID, checklistID, checklistItem) => {
updateTaskChecklistItemLocation({
variables: {checklistID, checklistItemID: checklistItem.id, position: checklistItem.position},
optimisticResponse: {
__typename: 'Mutation',
updateTaskChecklistItemLocation: {
__typename: 'UpdateTaskChecklistItemLocationPayload',
prevChecklistID,
checklistID,
checklistItem: {
__typename: 'TaskChecklistItem',
position: checklistItem.position,
id: checklistItem.id,
taskChecklistID: checklistID,
},
},
},
});
}}
onTaskNameChange={onTaskNameChange}
onTaskDescriptionChange={onTaskDescriptionChange}
onToggleTaskComplete={task => {
setTaskComplete({variables: {taskID: task.id, complete: !task.complete}});
}}
onDeleteTask={onDeleteTask}
onChangeItemName={(itemID, itemName) => {
updateTaskChecklistItemName({variables: {taskChecklistItemID: itemID, name: itemName}});
}}
onCloseModal={() => history.push(projectURL)}
onChangeChecklistName={(checklistID, newName) => {
updateTaskChecklistName({variables: {taskChecklistID: checklistID, name: newName}});
}}
onDeleteItem={itemID => {
deleteTaskChecklistItem({variables: {taskChecklistItemID: itemID}});
}}
onToggleChecklistItem={(itemID, complete) => {
setTaskChecklistItemComplete({
variables: {taskChecklistItemID: itemID, complete},
optimisticResponse: {
__typename: 'Mutation',
setTaskChecklistItemComplete: {
__typename: 'TaskChecklistItem',
id: itemID,
complete,
},
},
});
}}
onAddItem={(taskChecklistID, name, position) => {
createTaskChecklistItem({variables: {taskChecklistID, name, position}});
}}
onMemberProfile={($targetRef, memberID) => {
const member = data.findTask.assigned.find(m => m.id === memberID);
if (member) {
showPopup(
$targetRef,
<Popup title={null} onClose={() => {}} tab={0}>
<MiniProfile
user={member}
bio="None"
onRemoveFromTask={() => {
unassignTask({variables: {taskID: data.findTask.id, userID: userID ?? ''}});
}}
/>
</Popup>,
);
}
}}
onOpenAddMemberPopup={(task, $targetRef) => {
showPopup(
$targetRef,
<Popup title="Members" tab={0} onClose={() => {}}>
<MemberManager
availableMembers={availableMembers}
activeMembers={data.findTask.assigned}
onMemberChange={(member, isActive) => {
if (isActive) {
assignTask({variables: {taskID: data.findTask.id, userID: userID ?? ''}});
} else {
unassignTask({variables: {taskID: data.findTask.id, userID: userID ?? ''}});
} }
}} }),
/> { taskID },
</Popup>, );
); },
}} });
onOpenAddLabelPopup={onOpenAddLabelPopup} const [setTaskChecklistItemComplete] = useSetTaskChecklistItemCompleteMutation({
onOpenAddChecklistPopup={(_task, $target) => { update: client => {
showPopup( updateApolloCache<FindTaskQuery>(
$target, client,
<Popup FindTaskDocument,
title={'Add checklist'} cache =>
tab={0} produce(cache, draftCache => {
onClose={() => { const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
hidePopup(); draftCache.findTask.badges.checklist = {
}} __typename: 'ChecklistBadge',
> complete,
<CreateChecklistPopup total,
onCreateChecklist={checklistData => { };
let position = 65535; }),
console.log(data.findTask.checklists); { taskID },
if (data.findTask.checklists) { );
const [lastChecklist] = data.findTask.checklists.slice(-1); },
console.log(`lastCheclist ${lastChecklist}`); });
if (lastChecklist) { const [deleteTaskChecklist] = useDeleteTaskChecklistMutation({
position = lastChecklist.position * 2 + 1; update: (client, deleteData) => {
} updateApolloCache<FindTaskQuery>(
client,
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const { checklists } = cache.findTask;
console.log(deleteData)
draftCache.findTask.checklists = checklists.filter(c => c.id !== deleteData.data.deleteTaskChecklist.taskChecklist.id);
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
total,
};
if (complete === 0 && total === 0) {
draftCache.findTask.badges.checklist = null;
} }
createTaskChecklist({ }),
variables: { { taskID },
taskID: data.findTask.id, );
name: checklistData.name, },
position, });
}, const [updateTaskChecklistItemName] = useUpdateTaskChecklistItemNameMutation();
}); const [createTaskChecklist] = useCreateTaskChecklistMutation({
hidePopup(); update: (client, createData) => {
}} updateApolloCache<FindTaskQuery>(
/> client,
</Popup>, FindTaskDocument,
); cache =>
}} produce(cache, draftCache => {
onDeleteChecklist={($target, checklistID) => { const item = createData.data.createTaskChecklist;
showPopup( draftCache.findTask.checklists.push({ ...item });
$target, }),
<Popup tab={0} title="Delete checklist?" onClose={() => hidePopup()}> { taskID },
<p>Deleting a checklist is permanent and there is no way to get it back.</p> );
<DeleteChecklistButton },
color="danger" });
onClick={() => { const [updateTaskChecklistName] = useUpdateTaskChecklistNameMutation();
deleteTaskChecklist({variables: {taskChecklistID: checklistID}}); const [deleteTaskChecklistItem] = useDeleteTaskChecklistItemMutation({
hidePopup(); update: (client, deleteData) => {
}} updateApolloCache<FindTaskQuery>(
> client,
Delete Checklist FindTaskDocument,
cache =>
produce(cache, draftCache => {
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID)
if (targetIdx > -1) {
draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(c => item.id !== c.id);
}
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
total,
};
}),
{ taskID },
);
},
});
const [createTaskChecklistItem] = useCreateTaskChecklistItemMutation({
update: (client, newTaskItem) => {
updateApolloCache<FindTaskQuery>(
client,
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const item = newTaskItem.data.createTaskChecklistItem;
const { checklists } = cache.findTask;
const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
if (idx !== -1) {
draftCache.findTask.checklists[idx].items.push({ ...item });
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
total,
};
}
}),
{ taskID },
);
},
});
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const [setTaskComplete] = useSetTaskCompleteMutation();
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => {
refetch();
refreshCache();
},
});
const [assignTask] = useAssignTaskMutation({
onCompleted: () => {
refetch();
refreshCache();
},
});
const [unassignTask] = useUnassignTaskMutation({
onCompleted: () => {
refetch();
refreshCache();
},
});
if (loading) {
return <div>loading</div>;
}
if (!data) {
return <div>loading</div>;
}
return (
<>
<Modal
width={768}
onClose={() => {
history.push(projectURL);
}}
renderContent={() => {
return (
<TaskDetails
task={data.findTask}
onChecklistDrop={checklist => {
updateTaskChecklistLocation({
variables: { checklistID: checklist.id, position: checklist.position },
optimisticResponse: {
__typename: 'Mutation',
updateTaskChecklistLocation: {
__typename: 'UpdateTaskChecklistLocationPayload',
checklist: {
__typename: 'TaskChecklist',
position: checklist.position,
id: checklist.id,
},
},
},
});
}}
onChecklistItemDrop={(prevChecklistID, checklistID, checklistItem) => {
updateTaskChecklistItemLocation({
variables: { checklistID, checklistItemID: checklistItem.id, position: checklistItem.position },
optimisticResponse: {
__typename: 'Mutation',
updateTaskChecklistItemLocation: {
__typename: 'UpdateTaskChecklistItemLocationPayload',
prevChecklistID,
checklistID,
checklistItem: {
__typename: 'TaskChecklistItem',
position: checklistItem.position,
id: checklistItem.id,
taskChecklistID: checklistID,
},
},
},
});
}}
onTaskNameChange={onTaskNameChange}
onTaskDescriptionChange={onTaskDescriptionChange}
onToggleTaskComplete={task => {
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
}}
onDeleteTask={onDeleteTask}
onChangeItemName={(itemID, itemName) => {
updateTaskChecklistItemName({ variables: { taskChecklistItemID: itemID, name: itemName } });
}}
onCloseModal={() => history.push(projectURL)}
onChangeChecklistName={(checklistID, newName) => {
updateTaskChecklistName({ variables: { taskChecklistID: checklistID, name: newName } });
}}
onDeleteItem={(checklistID, itemID) => {
deleteTaskChecklistItem({
variables: { taskChecklistItemID: itemID },
optimisticResponse: {
__typename: 'Mutation',
deleteTaskChecklistItem: {
__typename: 'DeleteTaskChecklistItemPayload',
ok: true,
taskChecklistItem: {
__typename: 'TaskChecklistItem',
id: itemID,
taskChecklistID: checklistID,
}
}
}
});
}}
onToggleChecklistItem={(itemID, complete) => {
setTaskChecklistItemComplete({
variables: { taskChecklistItemID: itemID, complete },
optimisticResponse: {
__typename: 'Mutation',
setTaskChecklistItemComplete: {
__typename: 'TaskChecklistItem',
id: itemID,
complete,
},
},
});
}}
onAddItem={(taskChecklistID, name, position) => {
createTaskChecklistItem({ variables: { taskChecklistID, name, position } });
}}
onMemberProfile={($targetRef, memberID) => {
const member = data.findTask.assigned.find(m => m.id === memberID);
if (member) {
showPopup(
$targetRef,
<Popup title={null} onClose={() => { }} tab={0}>
<MiniProfile
user={member}
bio="None"
onRemoveFromTask={() => {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
}}
/>
</Popup>,
);
}
}}
onOpenAddMemberPopup={(task, $targetRef) => {
showPopup(
$targetRef,
<Popup title="Members" tab={0} onClose={() => { }}>
<MemberManager
availableMembers={availableMembers}
activeMembers={data.findTask.assigned}
onMemberChange={(member, isActive) => {
if (isActive) {
assignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
} else {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
}
}}
/>
</Popup>,
);
}}
onOpenAddLabelPopup={onOpenAddLabelPopup}
onOpenAddChecklistPopup={(_task, $target) => {
showPopup(
$target,
<Popup
title={'Add checklist'}
tab={0}
onClose={() => {
hidePopup();
}}
>
<CreateChecklistPopup
onCreateChecklist={checklistData => {
let position = 65535;
console.log(data.findTask.checklists);
if (data.findTask.checklists) {
const [lastChecklist] = data.findTask.checklists.slice(-1);
console.log(`lastCheclist ${lastChecklist}`);
if (lastChecklist) {
position = lastChecklist.position * 2 + 1;
}
}
createTaskChecklist({
variables: {
taskID: data.findTask.id,
name: checklistData.name,
position,
},
});
hidePopup();
}}
/>
</Popup>,
);
}}
onDeleteChecklist={($target, checklistID) => {
showPopup(
$target,
<Popup tab={0} title="Delete checklist?" onClose={() => hidePopup()}>
<p>Deleting a checklist is permanent and there is no way to get it back.</p>
<DeleteChecklistButton
color="danger"
onClick={() => {
deleteTaskChecklist({ variables: { taskChecklistID: checklistID } });
hidePopup();
}}
>
Delete Checklist
</DeleteChecklistButton> </DeleteChecklistButton>
</Popup>, </Popup>,
); );
}} }}
onOpenDueDatePopop={(task, $targetRef) => { onOpenDueDatePopop={(task, $targetRef) => {
showPopup( showPopup(
$targetRef, $targetRef,
<Popup <Popup
title={'Change Due Date'} title={'Change Due Date'}
tab={0} tab={0}
onClose={() => { onClose={() => {
hidePopup(); hidePopup();
}} }}
> >
<DueDateManager <DueDateManager
task={task} task={task}
onRemoveDueDate={t => { onRemoveDueDate={t => {
updateTaskDueDate({variables: {taskID: t.id, dueDate: null}}); updateTaskDueDate({ variables: { taskID: t.id, dueDate: null } });
hidePopup(); hidePopup();
}} }}
onDueDateChange={(t, newDueDate) => { onDueDateChange={(t, newDueDate) => {
updateTaskDueDate({variables: {taskID: t.id, dueDate: newDueDate}}); updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } });
hidePopup(); hidePopup();
}} }}
onCancel={() => {}} onCancel={() => { }}
/> />
</Popup>, </Popup>,
); );
}} }}
/>
);
}}
/> />
); </>
}} );
/>
</>
);
}; };
export default Details; export default Details;

View File

@ -1,19 +1,19 @@
import React, { useState } from 'react'; import React, {useState} from 'react';
import { action } from '@storybook/addon-actions'; import {action} from '@storybook/addon-actions';
import BaseStyles from 'App/BaseStyles'; import BaseStyles from 'App/BaseStyles';
import NormalizeStyles from 'App/NormalizeStyles'; import NormalizeStyles from 'App/NormalizeStyles';
import { theme } from 'App/ThemeStyles'; import {theme} from 'App/ThemeStyles';
import produce from 'immer'; import produce from 'immer';
import styled, { ThemeProvider } from 'styled-components'; import styled, {ThemeProvider} from 'styled-components';
import Checklist, { ChecklistItem } from '.'; import Checklist, {ChecklistItem} from '.';
export default { export default {
component: Checklist, component: Checklist,
title: 'Checklist', title: 'Checklist',
parameters: { parameters: {
backgrounds: [ backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true }, {name: 'gray', value: '#f8f8f8', default: true},
{ name: 'white', value: '#ffffff' }, {name: 'white', value: '#ffffff'},
], ],
}, },
}; };
@ -138,6 +138,7 @@ export const Default = () => {
key={item.id} key={item.id}
wrapperProps={{}} wrapperProps={{}}
handleProps={{}} handleProps={{}}
checklistID='id'
itemID={item.id} itemID={item.id}
name={item.name} name={item.name}
complete={item.complete} complete={item.complete}

View File

@ -1,7 +1,7 @@
import React, { useState, useRef, useEffect } from 'react'; import React, {useState, useRef, useEffect} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { CheckSquare, Trash, Square, CheckSquareOutline, Clock, Cross, AccountPlus } from 'shared/icons'; import {CheckSquare, Trash, Square, CheckSquareOutline, Clock, Cross, AccountPlus} from 'shared/icons';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; import {DragDropContext, Droppable, Draggable, DropResult} from 'react-beautiful-dnd';
import { import {
isPositionChanged, isPositionChanged,
getSortedDraggables, getSortedDraggables,
@ -81,7 +81,7 @@ const ChecklistProgressBar = styled.div`
overflow: hidden; overflow: hidden;
position: relative; position: relative;
`; `;
const ChecklistProgressBarCurrent = styled.div<{ width: number }>` const ChecklistProgressBarCurrent = styled.div<{width: number}>`
width: ${props => props.width}%; width: ${props => props.width}%;
background: rgba(${props => (props.width === 100 ? props.theme.colors.success : props.theme.colors.primary)}); background: rgba(${props => (props.width === 100 ? props.theme.colors.success : props.theme.colors.primary)});
bottom: 0; bottom: 0;
@ -129,9 +129,10 @@ const ChecklistItemTextControls = styled.div`
padding: 6px 0; padding: 6px 0;
width: 100%; width: 100%;
display: inline-flex; display: inline-flex;
align-items: center;
`; `;
const ChecklistItemText = styled.span<{ complete: boolean }>` const ChecklistItemText = styled.span<{complete: boolean}>`
color: ${props => (props.complete ? '#5e6c84' : `rgba(${props.theme.colors.text.primary})`)}; color: ${props => (props.complete ? '#5e6c84' : `rgba(${props.theme.colors.text.primary})`)};
${props => props.complete && 'text-decoration: line-through;'} ${props => props.complete && 'text-decoration: line-through;'}
line-height: 20px; line-height: 20px;
@ -155,6 +156,11 @@ const ControlButton = styled.div`
padding: 4px 6px; padding: 4px 6px;
border-radius: 6px; border-radius: 6px;
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.8); background-color: rgba(${props => props.theme.colors.bg.primary}, 0.8);
display: flex;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
&:hover { &:hover {
background-color: rgba(${props => props.theme.colors.primary}, 1); background-color: rgba(${props => props.theme.colors.primary}, 1);
} }
@ -206,7 +212,7 @@ const TrashButton = styled(Trash)`
fill: rgba(${props => props.theme.colors.text.primary}); fill: rgba(${props => props.theme.colors.text.primary});
`; `;
const ChecklistItemWrapper = styled.div<{ ref: any }>` const ChecklistItemWrapper = styled.div<{ref: any}>`
user-select: none; user-select: none;
clear: both; clear: both;
padding-left: 40px; padding-left: 40px;
@ -216,6 +222,9 @@ const ChecklistItemWrapper = styled.div<{ ref: any }>`
transition-property: transform, opacity, height, padding, margin; transition-property: transform, opacity, height, padding, margin;
transition-duration: 0.14s; transition-duration: 0.14s;
transition-timing-function: ease-in; transition-timing-function: ease-in;
& ${ControlButton}:last-child {
margin-right: 4px;
}
&:hover { &:hover {
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4); background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
@ -274,18 +283,19 @@ const ChecklistNewItem = styled.div`
type ChecklistItemProps = { type ChecklistItemProps = {
itemID: string; itemID: string;
checklistID: string;
complete: boolean; complete: boolean;
name: string; name: string;
onChangeName: (itemID: string, currentName: string) => void; onChangeName: (itemID: string, currentName: string) => void;
wrapperProps: any; wrapperProps: any;
handleProps: any; handleProps: any;
onToggleItem: (itemID: string, complete: boolean) => void; onToggleItem: (itemID: string, complete: boolean) => void;
onDeleteItem: (itemID: string) => void; onDeleteItem: (checklistIDID: string, itemID: string) => void;
}; };
export const ChecklistItem = React.forwardRef( export const ChecklistItem = React.forwardRef(
( (
{ itemID, complete, name, wrapperProps, handleProps, onChangeName, onToggleItem, onDeleteItem }: ChecklistItemProps, {itemID, checklistID, complete, name, wrapperProps, handleProps, onChangeName, onToggleItem, onDeleteItem}: ChecklistItemProps,
$item, $item,
) => { ) => {
const $editor = useRef<HTMLTextAreaElement>(null); const $editor = useRef<HTMLTextAreaElement>(null);
@ -309,8 +319,8 @@ export const ChecklistItem = React.forwardRef(
{complete ? ( {complete ? (
<ChecklistItemCheckedIcon width={20} height={20} /> <ChecklistItemCheckedIcon width={20} height={20} />
) : ( ) : (
<ChecklistItemUncheckedIcon width={20} height={20} /> <ChecklistItemUncheckedIcon width={20} height={20} />
)} )}
</ChecklistIcon> </ChecklistIcon>
{editting ? ( {editting ? (
<> <>
@ -352,7 +362,7 @@ export const ChecklistItem = React.forwardRef(
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
setEditting(false); setEditting(false);
onDeleteItem(itemID); onDeleteItem(checklistID, itemID);
}} }}
> >
<Trash width={16} height={16} /> <Trash width={16} height={16} />
@ -360,34 +370,34 @@ export const ChecklistItem = React.forwardRef(
</EditControls> </EditControls>
</> </>
) : ( ) : (
<ChecklistItemDetails <ChecklistItemDetails
onClick={() => { onClick={() => {
setEditting(true); setEditting(true);
}} }}
> >
<ChecklistItemRow> <ChecklistItemRow>
<ChecklistItemTextControls> <ChecklistItemTextControls>
<ChecklistItemText complete={complete}>{name}</ChecklistItemText> <ChecklistItemText complete={complete}>{name}</ChecklistItemText>
<ChecklistControls> <ChecklistControls>
<ControlButton> <ControlButton>
<AssignUserButton width={14} height={14} /> <AssignUserButton width={14} height={14} />
</ControlButton> </ControlButton>
<ControlButton> <ControlButton>
<ClockButton width={14} height={14} /> <ClockButton width={14} height={14} />
</ControlButton> </ControlButton>
<ControlButton <ControlButton
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
onDeleteItem(itemID); onDeleteItem(checklistID, itemID);
}} }}
> >
<TrashButton width={14} height={14} /> <TrashButton width={14} height={14} />
</ControlButton> </ControlButton>
</ChecklistControls> </ChecklistControls>
</ChecklistItemTextControls> </ChecklistItemTextControls>
</ChecklistItemRow> </ChecklistItemRow>
</ChecklistItemDetails> </ChecklistItemDetails>
)} )}
</ChecklistItemWrapper> </ChecklistItemWrapper>
); );
}, },
@ -397,7 +407,7 @@ type AddNewItemProps = {
onAddItem: (name: string) => void; onAddItem: (name: string) => void;
}; };
const AddNewItem: React.FC<AddNewItemProps> = ({ onAddItem }) => { const AddNewItem: React.FC<AddNewItemProps> = ({onAddItem}) => {
const $editor = useRef<HTMLTextAreaElement>(null); const $editor = useRef<HTMLTextAreaElement>(null);
const $wrapper = useRef<HTMLDivElement>(null); const $wrapper = useRef<HTMLDivElement>(null);
const [currentName, setCurrentName] = useState(''); const [currentName, setCurrentName] = useState('');
@ -454,8 +464,8 @@ const AddNewItem: React.FC<AddNewItemProps> = ({ onAddItem }) => {
</EditControls> </EditControls>
</> </>
) : ( ) : (
<NewItemButton onClick={() => setEditting(true)}>Add an item</NewItemButton> <NewItemButton onClick={() => setEditting(true)}>Add an item</NewItemButton>
)} )}
</ChecklistNewItem> </ChecklistNewItem>
); );
}; };
@ -467,7 +477,7 @@ type ChecklistTitleEditorProps = {
}; };
const ChecklistTitleEditor = React.forwardRef( const ChecklistTitleEditor = React.forwardRef(
({ name, onChangeName, onCancel }: ChecklistTitleEditorProps, $name: any) => { ({name, onChangeName, onCancel}: ChecklistTitleEditorProps, $name: any) => {
const [currentName, setCurrentName] = useState(name); const [currentName, setCurrentName] = useState(name);
return ( return (
<> <>
@ -515,7 +525,7 @@ type ChecklistProps = {
onChangeItemName: (itemID: string, currentName: string) => void; onChangeItemName: (itemID: string, currentName: string) => void;
wrapperProps: any; wrapperProps: any;
handleProps: any; handleProps: any;
onDeleteItem: (itemID: string) => void; onDeleteItem: (checklistID: string, itemID: string) => void;
onAddItem: (itemName: string) => void; onAddItem: (itemName: string) => void;
items: Array<TaskChecklistItem>; items: Array<TaskChecklistItem>;
}; };
@ -569,21 +579,21 @@ const Checklist = React.forwardRef(
}} }}
/> />
) : ( ) : (
<WindowChecklistTitle {...handleProps}> <WindowChecklistTitle {...handleProps}>
<WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText> <WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText>
<WindowOptions> <WindowOptions>
<DeleteButton <DeleteButton
onClick={$target => { onClick={$target => {
onDeleteChecklist($target, checklistID); onDeleteChecklist($target, checklistID);
}} }}
color="danger" color="danger"
variant="outline" variant="outline"
> >
Delete Delete
</DeleteButton> </DeleteButton>
</WindowOptions> </WindowOptions>
</WindowChecklistTitle> </WindowChecklistTitle>
)} )}
</WindowTitle> </WindowTitle>
<ChecklistProgress> <ChecklistProgress>
<ChecklistProgressPercent>{`${percent}%`}</ChecklistProgressPercent> <ChecklistProgressPercent>{`${percent}%`}</ChecklistProgressPercent>

View File

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react'; import React, {useState, useRef, useEffect} from 'react';
import { Bin, Cross, Plus } from 'shared/icons'; import {Bin, Cross, Plus} from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
@ -9,7 +9,7 @@ import {
getNewDraggablePosition, getNewDraggablePosition,
getAfterDropDraggableList, getAfterDropDraggableList,
} from 'shared/utils/draggables'; } from 'shared/utils/draggables';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; import {DragDropContext, Droppable, Draggable, DropResult} from 'react-beautiful-dnd';
import TaskAssignee from 'shared/components/TaskAssignee'; import TaskAssignee from 'shared/components/TaskAssignee';
import moment from 'moment'; import moment from 'moment';
@ -54,7 +54,7 @@ import {
MetaDetailTitle, MetaDetailTitle,
MetaDetailContent, MetaDetailContent,
} from './Styles'; } from './Styles';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist'; import Checklist, {ChecklistItem, ChecklistItems} from '../Checklist';
import styled from 'styled-components'; import styled from 'styled-components';
const ChecklistContainer = styled.div``; const ChecklistContainer = styled.div``;
@ -69,7 +69,7 @@ type TaskLabelProps = {
onClick: ($target: React.RefObject<HTMLElement>) => void; onClick: ($target: React.RefObject<HTMLElement>) => void;
}; };
const TaskLabelItem: React.FC<TaskLabelProps> = ({ label, onClick }) => { const TaskLabelItem: React.FC<TaskLabelProps> = ({label, onClick}) => {
const $label = useRef<HTMLDivElement>(null); const $label = useRef<HTMLDivElement>(null);
return ( return (
<TaskDetailLabel <TaskDetailLabel
@ -84,14 +84,14 @@ const TaskLabelItem: React.FC<TaskLabelProps> = ({ label, onClick }) => {
); );
}; };
const TaskContent: React.FC<TaskContentProps> = ({ description, onEditContent }) => { const TaskContent: React.FC<TaskContentProps> = ({description, onEditContent}) => {
return description === '' ? ( return description === '' ? (
<TaskDetailsAddDetailsButton onClick={onEditContent}>Add a more detailed description</TaskDetailsAddDetailsButton> <TaskDetailsAddDetailsButton onClick={onEditContent}>Add a more detailed description</TaskDetailsAddDetailsButton>
) : ( ) : (
<TaskDetailsMarkdown onClick={onEditContent}> <TaskDetailsMarkdown onClick={onEditContent}>
<ReactMarkdown source={description} /> <ReactMarkdown source={description} />
</TaskDetailsMarkdown> </TaskDetailsMarkdown>
); );
}; };
type DetailsEditorProps = { type DetailsEditorProps = {
@ -144,7 +144,7 @@ type TaskDetailsProps = {
onTaskDescriptionChange: (task: Task, newDescription: string) => void; onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void; onDeleteTask: (task: Task) => void;
onAddItem: (checklistID: string, name: string, position: number) => void; onAddItem: (checklistID: string, name: string, position: number) => void;
onDeleteItem: (itemID: string) => void; onDeleteItem: (checklistID: string, itemID: string) => void;
onChangeItemName: (itemID: string, itemName: string) => void; onChangeItemName: (itemID: string, itemName: string) => void;
onToggleTaskComplete: (task: Task) => void; onToggleTaskComplete: (task: Task) => void;
onToggleChecklistItem: (itemID: string, complete: boolean) => void; onToggleChecklistItem: (itemID: string, complete: boolean) => void;
@ -214,7 +214,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onOpenAddLabelPopup(task, $target); onOpenAddLabelPopup(task, $target);
}; };
const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => { const onDragEnd = ({draggableId, source, destination, type}: DropResult) => {
if (typeof destination === 'undefined') return; if (typeof destination === 'undefined') return;
if (!isPositionChanged(source, destination)) return; if (!isPositionChanged(source, destination)) return;
@ -233,7 +233,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
}; };
beforeDropDraggables = getSortedDraggables( beforeDropDraggables = getSortedDraggables(
task.checklists.map(checklist => { task.checklists.map(checklist => {
return { id: checklist.id, position: checklist.position }; return {id: checklist.id, position: checklist.position};
}), }),
); );
if (droppedDraggable === null || beforeDropDraggables === null) { if (droppedDraggable === null || beforeDropDraggables === null) {
@ -249,9 +249,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index); const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
console.log(droppedGroup); console.log(droppedGroup);
console.log(`positiion: ${newPosition}`); console.log(`positiion: ${newPosition}`);
onChecklistDrop({ ...droppedGroup, position: newPosition }); onChecklistDrop({...droppedGroup, position: newPosition});
} else { } else {
throw { error: 'task group can not be found' }; throw {error: 'task group can not be found'};
} }
} else { } else {
const targetChecklist = task.checklists.findIndex( const targetChecklist = task.checklists.findIndex(
@ -266,7 +266,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
}; };
beforeDropDraggables = getSortedDraggables( beforeDropDraggables = getSortedDraggables(
task.checklists[targetChecklist].items.map(item => { task.checklists[targetChecklist].items.map(item => {
return { id: item.id, position: item.position }; return {id: item.id, position: item.position};
}), }),
); );
if (droppedDraggable === null || beforeDropDraggables === null) { if (droppedDraggable === null || beforeDropDraggables === null) {
@ -379,8 +379,8 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
}} }}
/> />
) : ( ) : (
<TaskContent description={description} onEditContent={handleClick} /> <TaskContent description={description} onEditContent={handleClick} />
)} )}
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<Droppable direction="vertical" type="checklist" droppableId="root"> <Droppable direction="vertical" type="checklist" droppableId="root">
{dropProvided => ( {dropProvided => (
@ -430,6 +430,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<ChecklistItem <ChecklistItem
key={item.id} key={item.id}
itemID={item.id} itemID={item.id}
checklistID={item.taskChecklistID}
ref={itemDrop.innerRef} ref={itemDrop.innerRef}
wrapperProps={itemDrop.draggableProps} wrapperProps={itemDrop.draggableProps}
handleProps={itemDrop.dragHandleProps} handleProps={itemDrop.dragHandleProps}
@ -437,7 +438,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
complete={item.complete} complete={item.complete}
onDeleteItem={onDeleteItem} onDeleteItem={onDeleteItem}
onChangeName={onChangeItemName} onChangeName={onChangeItemName}
onToggleItem={() => {}} onToggleItem={(itemID, complete) => onToggleChecklistItem(item.id, complete)}
/> />
)} )}
</Draggable> </Draggable>