chore: project cleanup and bugfixes

This commit is contained in:
Jordan Knott 2020-07-12 02:06:11 -05:00
parent e7e6fdc24c
commit a20ff90106
43 changed files with 3317 additions and 1143 deletions

View File

@ -24,9 +24,8 @@
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"rules": { "rules": {
"prettier/prettier": "error", "prettier/prettier": "warning",
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"], "no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",

View File

@ -3,5 +3,6 @@ module.exports = {
trailingComma: "all", trailingComma: "all",
singleQuote: true, singleQuote: true,
printWidth: 120, printWidth: 120,
tabWidth: 2 tabWidth: 2,
bracketSpacing: true,
}; };

View File

@ -1,6 +1,6 @@
overwrite: true overwrite: true
schema: schema:
- '../api/graph/schema.graphqls' - '../internal/graph/schema.graphqls'
documents: documents:
- 'src/shared/graphql/*.graphqls' - 'src/shared/graphql/*.graphqls'
- 'src/shared/graphql/**/*.ts' - 'src/shared/graphql/**/*.ts'

View File

@ -0,0 +1,527 @@
import React, {useState, useRef, useContext, useEffect} from 'react';
import {MENU_TYPES} from 'shared/components/TopNavbar';
import updateApolloCache from 'shared/utils/cache';
import GlobalTopNavbar, {ProjectPopup} from 'App/TopNavbar';
import styled, {css} from 'styled-components/macro';
import {Bolt, ToggleOn, Tags, CheckCircle, Sort, Filter} from 'shared/icons';
import LabelManagerEditor from '../LabelManagerEditor'
import {usePopup, Popup} from 'shared/components/PopupMenu';
import {useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation} from 'react-router-dom';
import {
useSetProjectOwnerMutation,
useUpdateProjectMemberRoleMutation,
useCreateProjectMemberMutation,
useDeleteProjectMemberMutation,
useSetTaskCompleteMutation,
useToggleTaskLabelMutation,
useUpdateProjectNameMutation,
useFindProjectQuery,
useUpdateTaskGroupNameMutation,
useUpdateTaskNameMutation,
useUpdateProjectLabelMutation,
useCreateTaskMutation,
useDeleteProjectLabelMutation,
useDeleteTaskMutation,
useUpdateTaskLocationMutation,
useUpdateTaskGroupLocationMutation,
useCreateTaskGroupMutation,
useDeleteTaskGroupMutation,
useUpdateTaskDescriptionMutation,
useAssignTaskMutation,
DeleteTaskDocument,
FindProjectDocument,
useCreateProjectLabelMutation,
useUnassignTaskMutation,
useUpdateTaskDueDateMutation,
FindProjectQuery,
useUsersQuery,
} from 'shared/generated/graphql';
import QuickCardEditor from 'shared/components/QuickCardEditor';
import ListActions from 'shared/components/ListActions';
import MemberManager from 'shared/components/MemberManager';
import SimpleLists from 'shared/components/Lists';
import produce from 'immer';
import MiniProfile from 'shared/components/MiniProfile';
import DueDateManager from 'shared/components/DueDateManager';
import UserIDContext from 'App/context';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
const ProjectBar = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding: 0 12px;
`;
const ProjectActions = styled.div`
display: flex;
align-items: center;
`;
const ProjectAction = styled.div<{disabled?: boolean}>`
cursor: pointer;
display: flex;
align-items: center;
font-size: 15px;
color: rgba(${props => props.theme.colors.text.primary});
&:not(:last-child) {
margin-right: 16px;
}
&:hover {
color: rgba(${props => props.theme.colors.text.secondary});
}
${props =>
props.disabled &&
css`
opacity: 0.5;
cursor: default;
pointer-events: none;
`}
`;
const ProjectActionText = styled.span`
padding-left: 4px;
`;
interface QuickCardEditorState {
isOpen: boolean;
target: React.RefObject<HTMLElement> | null;
taskID: string | null;
taskGroupID: string | null;
}
const initialQuickCardEditorState: QuickCardEditorState = {
taskID: null,
taskGroupID: null,
isOpen: false,
target: null,
};
type ProjectBoardProps = {
projectID: string;
};
const ProjectBoard: React.FC<ProjectBoardProps> = ({projectID}) => {
const [assignTask] = useAssignTaskMutation();
const [unassignTask] = useUnassignTaskMutation();
const $labelsRef = useRef<HTMLDivElement>(null);
const match = useRouteMatch();
const labelsRef = useRef<Array<ProjectLabel>>([]);
const {showPopup, hidePopup} = usePopup();
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
const {userID} = useContext(UserIDContext);
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation({});
const history = useHistory();
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
update: (client, deletedTaskGroupData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
);
}),
{projectId: projectID},
);
},
});
const [updateTaskName] = useUpdateTaskNameMutation();
const [createTask] = useCreateTaskMutation({
update: (client, newTaskData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const {taskGroups} = cache.findProject;
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data.createTask.taskGroup.id);
if (idx !== -1) {
draftCache.findProject.taskGroups[idx].tasks.push({...newTaskData.data.createTask});
}
}),
{projectId: projectID},
);
},
});
const [createTaskGroup] = useCreateTaskGroupMutation({
update: (client, newTaskGroupData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache => produce(cache, draftCache => {
draftCache.findProject.taskGroups.push({...newTaskGroupData.data.createTaskGroup, tasks: []});
}),
{projectId: projectID},
);
},
});
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
const {loading, data} = useFindProjectQuery({
variables: {projectId: projectID},
});
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
const [setTaskComplete] = useSetTaskCompleteMutation();
const [updateTaskLocation] = useUpdateTaskLocationMutation({
update: (client, newTask) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const {previousTaskGroupID, task} = newTask.data.updateTaskLocation;
if (previousTaskGroupID !== task.taskGroup.id) {
const {taskGroups} = cache.findProject;
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
(t: Task) => t.id !== task.id,
);
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
...taskGroups[newTaskGroupIdx].tasks,
{...task},
];
}
}
}),
{projectId: projectID},
);
},
});
const [deleteTask] = useDeleteTaskMutation();
const [toggleTaskLabel] = useToggleTaskLabelMutation({
onCompleted: newTaskLabel => {
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
console.log(taskLabelsRef.current);
},
});
const onCreateTask = (taskGroupID: string, name: string) => {
if (data) {
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
console.log(`taskGroup ${taskGroup}`);
if (taskGroup) {
let position = 65535;
if (taskGroup.tasks.length !== 0) {
const [lastTask] = taskGroup.tasks
.slice()
.sort((a: any, b: any) => a.position - b.position)
.slice(-1);
position = Math.ceil(lastTask.position) * 2 + 1;
}
console.log(`position ${position}`);
createTask({
variables: {taskGroupID, name, position},
optimisticResponse: {
__typename: 'Mutation',
createTask: {
__typename: 'Task',
id: '' + Math.round(Math.random() * -1000000),
name,
complete: false,
taskGroup: {
__typename: 'TaskGroup',
id: taskGroup.id,
name: taskGroup.name,
position: taskGroup.position,
},
badges: {
checklist: null,
},
position,
dueDate: null,
description: null,
labels: [],
assigned: [],
},
},
});
}
}
};
const onCreateList = (listName: string) => {
if (data) {
const [lastColumn] = data.findProject.taskGroups.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}});
}
};
if (loading) {
return <span>loading</span>;
}
if (data) {
labelsRef.current = data.findProject.labels;
const onQuickEditorOpen = ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => {
if ($target && $target.current) {
const pos = $target.current.getBoundingClientRect();
const height = 120;
if (window.innerHeight - pos.bottom < height) {
}
}
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
const currentTask = taskGroup ? taskGroup.tasks.find(t => t.id === taskID) : null;
if (currentTask) {
setQuickCardEditor({
target: $target,
isOpen: true,
taskID: currentTask.id,
taskGroupID: currentTask.taskGroup.id,
});
}
};
let currentQuickTask = null;
if (quickCardEditor.taskID && quickCardEditor.taskGroupID) {
const targetGroup = data.findProject.taskGroups.find(t => t.id === quickCardEditor.taskGroupID);
if (targetGroup) {
currentQuickTask = targetGroup.tasks.find(t => t.id === quickCardEditor.taskID);
}
}
return (
<>
<ProjectBar>
<ProjectActions>
<ProjectAction disabled>
<CheckCircle width={13} height={13} />
<ProjectActionText>All Tasks</ProjectActionText>
</ProjectAction>
<ProjectAction disabled>
<Filter width={13} height={13} />
<ProjectActionText>Filter</ProjectActionText>
</ProjectAction>
<ProjectAction disabled>
<Sort width={13} height={13} />
<ProjectActionText>Sort</ProjectActionText>
</ProjectAction>
</ProjectActions>
<ProjectActions>
<ProjectAction
ref={$labelsRef}
onClick={() => {
showPopup(
$labelsRef,
<LabelManagerEditor
taskLabels={null}
labelColors={data.labelColors}
labels={labelsRef}
projectID={projectID}
/>,
);
}}
>
<Tags width={13} height={13} />
<ProjectActionText>Labels</ProjectActionText>
</ProjectAction>
<ProjectAction disabled>
<ToggleOn width={13} height={13} />
<ProjectActionText>Fields</ProjectActionText>
</ProjectAction>
<ProjectAction disabled>
<Bolt width={13} height={13} />
<ProjectActionText>Rules</ProjectActionText>
</ProjectAction>
</ProjectActions>
</ProjectBar>
<SimpleLists
onTaskClick={task => {
history.push(`${match.url}/c/${task.id}`);
}}
onTaskDrop={(droppedTask, previousTaskGroupID) => {
updateTaskLocation({
variables: {
taskID: droppedTask.id,
taskGroupID: droppedTask.taskGroup.id,
position: droppedTask.position,
},
optimisticResponse: {
__typename: 'Mutation',
updateTaskLocation: {
__typename: 'UpdateTaskLocationPayload',
previousTaskGroupID,
task: {
__typename: 'Task',
name: droppedTask.name,
id: droppedTask.id,
position: droppedTask.position,
taskGroup: {
id: droppedTask.taskGroup.id,
__typename: 'TaskGroup',
},
createdAt: '',
},
},
},
});
}}
onTaskGroupDrop={droppedTaskGroup => {
updateTaskGroupLocation({
variables: {taskGroupID: droppedTaskGroup.id, position: droppedTaskGroup.position},
optimisticResponse: {
__typename: 'Mutation',
updateTaskGroupLocation: {
id: droppedTaskGroup.id,
position: droppedTaskGroup.position,
__typename: 'TaskGroup',
},
},
});
}}
taskGroups={data.findProject.taskGroups}
onCreateTask={onCreateTask}
onCreateTaskGroup={onCreateList}
onCardMemberClick={($targetRef, taskID, memberID) => {
const member = data.findProject.members.find(m => m.id === memberID);
if (member) {
showPopup(
$targetRef,
<MiniProfile
user={member}
bio="None"
onRemoveFromTask={() => {
/* unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); */
}}
/>,
);
}
}}
onChangeTaskGroupName={(taskGroupID, name) => {
updateTaskGroupName({variables: {taskGroupID, name}});
}}
onQuickEditorOpen={onQuickEditorOpen}
onExtraMenuOpen={(taskGroupID: string, $targetRef: any) => {
showPopup(
$targetRef,
<Popup title="List actions" tab={0} onClose={() => hidePopup()}>
<ListActions
taskGroupID={taskGroupID}
onArchiveTaskGroup={tgID => {
deleteTaskGroup({variables: {taskGroupID: tgID}});
hidePopup();
}}
/>
</Popup>,
);
}}
/>
{quickCardEditor.isOpen && currentQuickTask && quickCardEditor.target && (
<QuickCardEditor
task={currentQuickTask}
onCloseEditor={() => setQuickCardEditor(initialQuickCardEditorState)}
onEditCard={(_taskGroupID: string, taskID: string, cardName: string) => {
updateTaskName({variables: {taskID, name: cardName}});
}}
onOpenMembersPopup={($targetRef, task) => {
showPopup(
$targetRef,
<Popup title="Members" tab={0} onClose={() => hidePopup()}>
<MemberManager
availableMembers={data.findProject.members}
activeMembers={task.assigned ?? []}
onMemberChange={(member, isActive) => {
if (isActive) {
assignTask({variables: {taskID: task.id, userID: userID ?? ''}});
} else {
unassignTask({variables: {taskID: task.id, userID: userID ?? ''}});
}
}}
/>
</Popup>,
);
}}
onCardMemberClick={($targetRef, taskID, memberID) => {
const member = data.findProject.members.find(m => m.id === memberID);
if (member) {
showPopup(
$targetRef,
<MiniProfile
bio="None"
user={member}
onRemoveFromTask={() => {
/* unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); */
}}
/>,
);
}
}}
onOpenLabelsPopup={($targetRef, task) => {
taskLabelsRef.current = task.labels;
showPopup(
$targetRef,
<LabelManagerEditor
onLabelToggle={labelID => {
toggleTaskLabel({variables: {taskID: task.id, projectLabelID: labelID}});
}}
labelColors={data.labelColors}
labels={labelsRef}
taskLabels={taskLabelsRef}
projectID={projectID}
/>,
);
}}
onArchiveCard={(_listId: string, cardId: string) =>
deleteTask({
variables: {taskID: cardId},
update: client => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups = cache.findProject.taskGroups.map(taskGroup => ({
...taskGroup,
tasks: taskGroup.tasks.filter(t => t.id !== cardId),
}));
}),
{projectId: projectID},
);
},
})
}
onOpenDueDatePopup={($targetRef, task) => {
showPopup(
$targetRef,
<Popup title="Change Due Date" tab={0} onClose={() => hidePopup()}>
<DueDateManager
task={task}
onRemoveDueDate={t => {
updateTaskDueDate({variables: {taskID: t.id, dueDate: null}});
hidePopup();
}}
onDueDateChange={(t, newDueDate) => {
updateTaskDueDate({variables: {taskID: t.id, dueDate: newDueDate}});
hidePopup();
}}
onCancel={() => {}}
/>
</Popup>,
);
}}
onToggleComplete={task => {
setTaskComplete({variables: {taskID: task.id, complete: !task.complete}});
}}
target={quickCardEditor.target}
/>
)}
</>
);
}
return <span>Error</span>;
};
export default ProjectBoard;

View File

@ -1,12 +1,13 @@
import React, { useState, useContext } 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,
useCreateTaskChecklistMutation, useCreateTaskChecklistMutation,
useFindTaskQuery, useFindTaskQuery,
useUpdateTaskDueDateMutation, useUpdateTaskDueDateMutation,
@ -14,6 +15,7 @@ import {
useAssignTaskMutation, useAssignTaskMutation,
useUnassignTaskMutation, useUnassignTaskMutation,
useSetTaskChecklistItemCompleteMutation, useSetTaskChecklistItemCompleteMutation,
useUpdateTaskChecklistLocationMutation,
useDeleteTaskChecklistItemMutation, useDeleteTaskChecklistItemMutation,
useUpdateTaskChecklistItemNameMutation, useUpdateTaskChecklistItemNameMutation,
useCreateTaskChecklistItemMutation, useCreateTaskChecklistItemMutation,
@ -27,7 +29,7 @@ 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>) => {
@ -47,7 +49,7 @@ const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => {
}, 0), }, 0),
0, 0,
); );
return { total, complete }; return {total, complete};
}; };
const DeleteChecklistButton = styled(Button)` const DeleteChecklistButton = styled(Button)`
@ -80,8 +82,8 @@ const InputError = styled.span`
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);
}; };
@ -90,12 +92,13 @@ const CreateChecklistPopup: React.FC<CreateChecklistPopupProps> = ({ onCreateChe
<CreateChecklistForm onSubmit={handleSubmit(createUser)}> <CreateChecklistForm onSubmit={handleSubmit(createUser)}>
<CreateChecklistInput <CreateChecklistInput
floatingLabel floatingLabel
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>
@ -113,7 +116,7 @@ type DetailsProps = {
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,
@ -125,12 +128,46 @@ const Details: React.FC<DetailsProps> = ({
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 [updateTaskChecklistItemLocation] = useUpdateTaskChecklistItemLocationMutation({
update: (client, response) => {
updateApolloCache<FindTaskQuery>(
client,
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const {prevChecklistID, checklistID, checklistItem} = response.data.updateTaskChecklistItemLocation;
console.log(`${checklistID} !== ${prevChecklistID}`);
if (checklistID !== prevChecklistID) {
const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID);
const newIdx = cache.findTask.checklists.findIndex(c => c.id === checklistID);
console.log(`oldIdx ${oldIdx} newIdx ${newIdx}`);
if (oldIdx > -1 && newIdx > -1) {
const item = cache.findTask.checklists[oldIdx].items.find(item => item.id === checklistItem.id);
console.log(item);
if (item) {
draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter(
i => i.id !== checklistItem.id,
);
draftCache.findTask.checklists[newIdx].items.push({
...item,
position: checklistItem.position,
taskChecklistID: checklistID,
});
}
}
}
}),
{taskID},
);
},
});
const [setTaskChecklistItemComplete] = useSetTaskChecklistItemCompleteMutation({ const [setTaskChecklistItemComplete] = useSetTaskChecklistItemCompleteMutation({
update: client => { update: client => {
updateApolloCache<FindTaskQuery>( updateApolloCache<FindTaskQuery>(
@ -138,14 +175,14 @@ const Details: React.FC<DetailsProps> = ({
FindTaskDocument, FindTaskDocument,
cache => cache =>
produce(cache, draftCache => { produce(cache, draftCache => {
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists); const {complete, total} = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = { draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge', __typename: 'ChecklistBadge',
complete, complete,
total, total,
}; };
}), }),
{ taskID }, {taskID},
); );
}, },
}); });
@ -156,10 +193,10 @@ const Details: React.FC<DetailsProps> = ({
FindTaskDocument, FindTaskDocument,
cache => cache =>
produce(cache, draftCache => { produce(cache, draftCache => {
const { checklists } = cache.findTask; const {checklists} = cache.findTask;
const item = deleteData.deleteTaskChecklist; console.log(deleteData)
draftCache.findTask.checklists = checklists.filter(c => c.id !== item.taskChecklist.id); draftCache.findTask.checklists = checklists.filter(c => c.id !== deleteData.data.deleteTaskChecklist.taskChecklist.id);
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists); const {complete, total} = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = { draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge', __typename: 'ChecklistBadge',
complete, complete,
@ -169,7 +206,7 @@ const Details: React.FC<DetailsProps> = ({
draftCache.findTask.badges.checklist = null; draftCache.findTask.badges.checklist = null;
} }
}), }),
{ taskID }, {taskID},
); );
}, },
}); });
@ -182,9 +219,9 @@ const Details: React.FC<DetailsProps> = ({
cache => cache =>
produce(cache, draftCache => { produce(cache, draftCache => {
const item = createData.data.createTaskChecklist; const item = createData.data.createTaskChecklist;
draftCache.findTask.checklists.push({ ...item }); draftCache.findTask.checklists.push({...item});
}), }),
{ taskID }, {taskID},
); );
}, },
}); });
@ -197,15 +234,18 @@ const Details: React.FC<DetailsProps> = ({
cache => cache =>
produce(cache, draftCache => { produce(cache, draftCache => {
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem; const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
draftCache.findTask.checklists = cache.findTask.checklists.filter(c => item.id !== c.id); const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID)
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists); 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 = { draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge', __typename: 'ChecklistBadge',
complete, complete,
total, total,
}; };
}), }),
{ taskID }, {taskID},
); );
}, },
}); });
@ -217,11 +257,11 @@ const Details: React.FC<DetailsProps> = ({
cache => cache =>
produce(cache, draftCache => { produce(cache, draftCache => {
const item = newTaskItem.data.createTaskChecklistItem; const item = newTaskItem.data.createTaskChecklistItem;
const { checklists } = cache.findTask; const {checklists} = cache.findTask;
const idx = checklists.findIndex(c => c.id === item.taskChecklistID); const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
if (idx !== -1) { if (idx !== -1) {
draftCache.findTask.checklists[idx].items.push({ ...item }); draftCache.findTask.checklists[idx].items.push({...item});
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists); const {complete, total} = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = { draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge', __typename: 'ChecklistBadge',
complete, complete,
@ -229,11 +269,11 @@ const Details: React.FC<DetailsProps> = ({
}; };
} }
}), }),
{ taskID }, {taskID},
); );
}, },
}); });
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } }); const {loading, data, refetch} = useFindTaskQuery({variables: {taskID}});
const [setTaskComplete] = useSetTaskCompleteMutation(); const [setTaskComplete] = useSetTaskCompleteMutation();
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({ const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => { onCompleted: () => {
@ -259,7 +299,6 @@ const Details: React.FC<DetailsProps> = ({
if (!data) { if (!data) {
return <div>loading</div>; return <div>loading</div>;
} }
console.log(data.findTask);
return ( return (
<> <>
<Modal <Modal
@ -271,25 +310,62 @@ const Details: React.FC<DetailsProps> = ({
return ( return (
<TaskDetails <TaskDetails
task={data.findTask} 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} onTaskNameChange={onTaskNameChange}
onTaskDescriptionChange={onTaskDescriptionChange} onTaskDescriptionChange={onTaskDescriptionChange}
onToggleTaskComplete={task => { onToggleTaskComplete={task => {
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } }); setTaskComplete({variables: {taskID: task.id, complete: !task.complete}});
}} }}
onDeleteTask={onDeleteTask} onDeleteTask={onDeleteTask}
onChangeItemName={(itemID, itemName) => { onChangeItemName={(itemID, itemName) => {
updateTaskChecklistItemName({ variables: { taskChecklistItemID: itemID, name: itemName } }); updateTaskChecklistItemName({variables: {taskChecklistItemID: itemID, name: itemName}});
}} }}
onCloseModal={() => history.push(projectURL)} onCloseModal={() => history.push(projectURL)}
onChangeChecklistName={(checklistID, newName) => { onChangeChecklistName={(checklistID, newName) => {
updateTaskChecklistName({ variables: { taskChecklistID: checklistID, name: newName } }); updateTaskChecklistName({variables: {taskChecklistID: checklistID, name: newName}});
}} }}
onDeleteItem={itemID => { onDeleteItem={itemID => {
deleteTaskChecklistItem({ variables: { taskChecklistItemID: itemID } }); deleteTaskChecklistItem({variables: {taskChecklistItemID: itemID}});
}} }}
onToggleChecklistItem={(itemID, complete) => { onToggleChecklistItem={(itemID, complete) => {
setTaskChecklistItemComplete({ setTaskChecklistItemComplete({
variables: { taskChecklistItemID: itemID, complete }, variables: {taskChecklistItemID: itemID, complete},
optimisticResponse: { optimisticResponse: {
__typename: 'Mutation', __typename: 'Mutation',
setTaskChecklistItemComplete: { setTaskChecklistItemComplete: {
@ -301,7 +377,7 @@ const Details: React.FC<DetailsProps> = ({
}); });
}} }}
onAddItem={(taskChecklistID, name, position) => { onAddItem={(taskChecklistID, name, position) => {
createTaskChecklistItem({ variables: { taskChecklistID, name, position } }); createTaskChecklistItem({variables: {taskChecklistID, name, position}});
}} }}
onMemberProfile={($targetRef, memberID) => { onMemberProfile={($targetRef, memberID) => {
const member = data.findTask.assigned.find(m => m.id === memberID); const member = data.findTask.assigned.find(m => m.id === memberID);
@ -313,7 +389,7 @@ const Details: React.FC<DetailsProps> = ({
user={member} user={member}
bio="None" bio="None"
onRemoveFromTask={() => { onRemoveFromTask={() => {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); unassignTask({variables: {taskID: data.findTask.id, userID: userID ?? ''}});
}} }}
/> />
</Popup>, </Popup>,
@ -329,9 +405,9 @@ const Details: React.FC<DetailsProps> = ({
activeMembers={data.findTask.assigned} activeMembers={data.findTask.assigned}
onMemberChange={(member, isActive) => { onMemberChange={(member, isActive) => {
if (isActive) { if (isActive) {
assignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); assignTask({variables: {taskID: data.findTask.id, userID: userID ?? ''}});
} else { } else {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); unassignTask({variables: {taskID: data.findTask.id, userID: userID ?? ''}});
} }
}} }}
/> />
@ -381,7 +457,7 @@ const Details: React.FC<DetailsProps> = ({
<DeleteChecklistButton <DeleteChecklistButton
color="danger" color="danger"
onClick={() => { onClick={() => {
deleteTaskChecklist({ variables: { taskChecklistID: checklistID } }); deleteTaskChecklist({variables: {taskChecklistID: checklistID}});
hidePopup(); hidePopup();
}} }}
> >
@ -403,11 +479,11 @@ const Details: React.FC<DetailsProps> = ({
<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={() => {}}

View File

@ -1,6 +0,0 @@
import styled from 'styled-components';
export const Board = styled.div`
margin-top: 12px;
margin-left: 8px;
`;

View File

@ -1,31 +0,0 @@
import React from 'react';
import { useRouteMatch, useHistory } from 'react-router';
import Lists from 'shared/components/Lists';
import { Board } from './Styles';
type KanbanBoardProps = {
onOpenListActionsPopup: ($targetRef: React.RefObject<HTMLElement>, 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;
onCardMemberClick: OnCardMemberClick;
};
const KanbanBoard: React.FC<KanbanBoardProps> = ({
onOpenListActionsPopup,
onQuickEditorOpen,
onCardCreate,
onCardDrop,
onListDrop,
onCreateList,
onCardMemberClick,
}) => {
const match = useRouteMatch();
const history = useHistory();
return <Board></Board>;
};
export default KanbanBoard;

View File

@ -0,0 +1,154 @@
import React, {useState} from 'react';
import updateApolloCache from 'shared/utils/cache';
import {usePopup, Popup} from 'shared/components/PopupMenu';
import produce from 'immer';
import {
useSetProjectOwnerMutation,
useUpdateProjectMemberRoleMutation,
useCreateProjectMemberMutation,
useDeleteProjectMemberMutation,
useSetTaskCompleteMutation,
useToggleTaskLabelMutation,
useUpdateProjectNameMutation,
useFindProjectQuery,
useUpdateTaskGroupNameMutation,
useUpdateTaskNameMutation,
useUpdateProjectLabelMutation,
useCreateTaskMutation,
useDeleteProjectLabelMutation,
useDeleteTaskMutation,
useUpdateTaskLocationMutation,
useUpdateTaskGroupLocationMutation,
useCreateTaskGroupMutation,
useDeleteTaskGroupMutation,
useUpdateTaskDescriptionMutation,
useAssignTaskMutation,
DeleteTaskDocument,
FindProjectDocument,
useCreateProjectLabelMutation,
useUnassignTaskMutation,
useUpdateTaskDueDateMutation,
FindProjectQuery,
useUsersQuery,
} from 'shared/generated/graphql';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
type LabelManagerEditorProps = {
labels: React.RefObject<Array<ProjectLabel>>;
taskLabels: null | React.RefObject<Array<TaskLabel>>;
projectID: string;
labelColors: Array<LabelColor>;
onLabelToggle?: (labelId: string) => void;
};
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
labels: labelsRef,
projectID,
labelColors,
onLabelToggle,
taskLabels: taskLabelsRef,
}) => {
const [currentLabel, setCurrentLabel] = useState('');
const {setTab, hidePopup} = usePopup();
const [createProjectLabel] = useCreateProjectLabelMutation({
update: (client, newLabelData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.labels.push({...newLabelData.data.createProjectLabel});
}),
{
projectId: projectID,
},
);
},
});
const [updateProjectLabel] = useUpdateProjectLabelMutation();
const [deleteProjectLabel] = useDeleteProjectLabelMutation({
update: (client, newLabelData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.labels = cache.findProject.labels.filter(
label => label.id !== newLabelData.data.deleteProjectLabel.id,
);
}),
{projectId: projectID},
);
},
});
const labels = labelsRef.current ? labelsRef.current : [];
const taskLabels = taskLabelsRef && taskLabelsRef.current ? taskLabelsRef.current : [];
const [currentTaskLabels, setCurrentTaskLabels] = useState(taskLabels);
console.log(taskLabels);
return (
<>
<Popup title="Labels" tab={0} onClose={() => hidePopup()}>
<LabelManager
labels={labels}
taskLabels={currentTaskLabels}
onLabelCreate={() => {
setTab(2);
}}
onLabelEdit={labelId => {
setCurrentLabel(labelId);
setTab(1);
}}
onLabelToggle={labelId => {
if (onLabelToggle) {
if (currentTaskLabels.find(t => t.projectLabel.id === labelId)) {
setCurrentTaskLabels(currentTaskLabels.filter(t => t.projectLabel.id !== labelId));
} else {
const newProjectLabel = labels.find(l => l.id === labelId);
if (newProjectLabel) {
setCurrentTaskLabels([
...currentTaskLabels,
{id: '', assignedDate: '', projectLabel: {...newProjectLabel}},
]);
}
}
setCurrentLabel(labelId);
onLabelToggle(labelId);
} else {
setCurrentLabel(labelId);
setTab(1);
}
}}
/>
</Popup>
<Popup onClose={() => hidePopup()} title="Edit label" tab={1}>
<LabelEditor
labelColors={labelColors}
label={labels.find(label => label.id === currentLabel) ?? null}
onLabelEdit={(projectLabelID, name, color) => {
if (projectLabelID) {
updateProjectLabel({variables: {projectLabelID, labelColorID: color.id, name: name ?? ''}});
}
setTab(0);
}}
onLabelDelete={labelID => {
deleteProjectLabel({variables: {projectLabelID: labelID}});
setTab(0);
}}
/>
</Popup>
<Popup onClose={() => hidePopup()} title="Create new label" tab={2}>
<LabelEditor
labelColors={labelColors}
label={null}
onLabelEdit={(_labelId, name, color) => {
createProjectLabel({variables: {projectID, labelColorID: color.id, name: name ?? ''}});
setTab(0);
}}
/>
</Popup>
</>
);
};
export default LabelManagerEditor

View File

@ -1,60 +1,36 @@
// LOC830 // LOC830
import React, { useState, useRef, useContext, useEffect } from 'react'; import React, {useState, useRef, useEffect, useContext} from 'react';
import { MENU_TYPES } from 'shared/components/TopNavbar';
import updateApolloCache from 'shared/utils/cache'; import updateApolloCache from 'shared/utils/cache';
import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar'; import GlobalTopNavbar, {ProjectPopup} from 'App/TopNavbar';
import styled, { css } from 'styled-components/macro'; import styled from 'styled-components/macro';
import { Bolt, ToggleOn, Tags, CheckCircle, Sort, Filter } from 'shared/icons'; import {usePopup, Popup} from 'shared/components/PopupMenu';
import { usePopup, Popup } from 'shared/components/PopupMenu'; import LabelManagerEditor from './LabelManagerEditor'
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation } from 'react-router-dom'; import {useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation, Redirect} from 'react-router-dom';
import { import {
useSetProjectOwnerMutation, useSetProjectOwnerMutation,
useUpdateProjectMemberRoleMutation, useUpdateProjectMemberRoleMutation,
useCreateProjectMemberMutation, useCreateProjectMemberMutation,
useDeleteProjectMemberMutation, useDeleteProjectMemberMutation,
useSetTaskCompleteMutation,
useToggleTaskLabelMutation, useToggleTaskLabelMutation,
useUpdateProjectNameMutation, useUpdateProjectNameMutation,
useFindProjectQuery, useFindProjectQuery,
useUpdateTaskGroupNameMutation,
useUpdateTaskNameMutation, useUpdateTaskNameMutation,
useUpdateProjectLabelMutation,
useCreateTaskMutation, useCreateTaskMutation,
useDeleteProjectLabelMutation,
useDeleteTaskMutation, useDeleteTaskMutation,
useUpdateTaskLocationMutation, useUpdateTaskLocationMutation,
useUpdateTaskGroupLocationMutation, useUpdateTaskGroupLocationMutation,
useCreateTaskGroupMutation, useCreateTaskGroupMutation,
useDeleteTaskGroupMutation,
useUpdateTaskDescriptionMutation, useUpdateTaskDescriptionMutation,
useAssignTaskMutation,
DeleteTaskDocument,
FindProjectDocument, FindProjectDocument,
useCreateProjectLabelMutation,
useUnassignTaskMutation,
useUpdateTaskDueDateMutation,
FindProjectQuery, FindProjectQuery,
useUsersQuery,
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
import TaskAssignee from 'shared/components/TaskAssignee';
import QuickCardEditor from 'shared/components/QuickCardEditor';
import ListActions from 'shared/components/ListActions';
import MemberManager from 'shared/components/MemberManager';
import { LabelsPopup } from 'shared/components/PopupMenu/PopupMenu.stories';
import KanbanBoard from 'Projects/Project/KanbanBoard';
import SimpleLists from 'shared/components/Lists';
import { mixin } from 'shared/utils/styles';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
import produce from 'immer'; import produce from 'immer';
import MiniProfile from 'shared/components/MiniProfile';
import Details from './Details';
import { useApolloClient } from '@apollo/react-hooks';
import UserIDContext from 'App/context'; import UserIDContext from 'App/context';
import DueDateManager from 'shared/components/DueDateManager';
import Input from 'shared/components/Input'; import Input from 'shared/components/Input';
import Member from 'shared/components/Member'; import Member from 'shared/components/Member';
import Board from './Board'
import Details from './Details'
const SearchInput = styled(Input)` const SearchInput = styled(Input)`
margin: 0; margin: 0;
@ -79,7 +55,7 @@ type UserManagementPopupProps = {
onAddProjectMember: (userID: string) => void; onAddProjectMember: (userID: string) => void;
}; };
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({ users, projectMembers, onAddProjectMember }) => { const UserManagementPopup: React.FC<UserManagementPopupProps> = ({users, projectMembers, onAddProjectMember}) => {
return ( return (
<Popup tab={0} title="Invite a user"> <Popup tab={0} title="Invite a user">
<SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" /> <SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" />
@ -111,140 +87,6 @@ interface QuickCardEditorState {
taskGroupID: string | null; taskGroupID: string | null;
} }
const TitleWrapper = styled.div`
margin-left: 38px;
margin-bottom: 15px;
`;
const Title = styled.span`
text-align: center;
font-size: 24px;
color: #fff;
`;
const ProjectMembers = styled.div`
display: flex;
padding-left: 4px;
padding-top: 4px;
align-items: center;
`;
type LabelManagerEditorProps = {
labels: React.RefObject<Array<ProjectLabel>>;
taskLabels: null | React.RefObject<Array<TaskLabel>>;
projectID: string;
labelColors: Array<LabelColor>;
onLabelToggle?: (labelId: string) => void;
};
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
labels: labelsRef,
projectID,
labelColors,
onLabelToggle,
taskLabels: taskLabelsRef,
}) => {
const [currentLabel, setCurrentLabel] = useState('');
const [createProjectLabel] = useCreateProjectLabelMutation({
update: (client, newLabelData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
}),
{
projectId: projectID,
},
);
},
});
const [updateProjectLabel] = useUpdateProjectLabelMutation();
const [deleteProjectLabel] = useDeleteProjectLabelMutation({
update: (client, newLabelData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.labels = cache.findProject.labels.filter(
label => label.id !== newLabelData.data.deleteProjectLabel.id,
);
}),
{ projectId: projectID },
);
},
});
const labels = labelsRef.current ? labelsRef.current : [];
const taskLabels = taskLabelsRef && taskLabelsRef.current ? taskLabelsRef.current : [];
const [currentTaskLabels, setCurrentTaskLabels] = useState(taskLabels);
console.log(taskLabels);
const { setTab, hidePopup } = usePopup();
return (
<>
<Popup title="Labels" tab={0} onClose={() => hidePopup()}>
<LabelManager
labels={labels}
taskLabels={currentTaskLabels}
onLabelCreate={() => {
setTab(2);
}}
onLabelEdit={labelId => {
setCurrentLabel(labelId);
setTab(1);
}}
onLabelToggle={labelId => {
if (onLabelToggle) {
if (currentTaskLabels.find(t => t.projectLabel.id === labelId)) {
setCurrentTaskLabels(currentTaskLabels.filter(t => t.projectLabel.id !== labelId));
} else {
const newProjectLabel = labels.find(l => l.id === labelId);
if (newProjectLabel) {
setCurrentTaskLabels([
...currentTaskLabels,
{ id: '', assignedDate: '', projectLabel: { ...newProjectLabel } },
]);
}
}
setCurrentLabel(labelId);
onLabelToggle(labelId);
} else {
setCurrentLabel(labelId);
setTab(1);
}
}}
/>
</Popup>
<Popup onClose={() => hidePopup()} title="Edit label" tab={1}>
<LabelEditor
labelColors={labelColors}
label={labels.find(label => label.id === currentLabel) ?? null}
onLabelEdit={(projectLabelID, name, color) => {
if (projectLabelID) {
updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name: name ?? '' } });
}
setTab(0);
}}
onLabelDelete={labelID => {
deleteProjectLabel({ variables: { projectLabelID: labelID } });
setTab(0);
}}
/>
</Popup>
<Popup onClose={() => hidePopup()} title="Create new label" tab={2}>
<LabelEditor
labelColors={labelColors}
label={null}
onLabelEdit={(_labelId, name, color) => {
createProjectLabel({ variables: { projectID, labelColorID: color.id, name: name ?? '' } });
setTab(0);
}}
/>
</Popup>
</>
);
};
interface ProjectParams { interface ProjectParams {
projectID: string; projectID: string;
} }
@ -256,259 +98,28 @@ const initialQuickCardEditorState: QuickCardEditorState = {
target: null, target: null,
}; };
const ProjectBar = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding: 0 12px;
`;
const ProjectActions = styled.div`
display: flex;
align-items: center;
`;
const ProjectAction = styled.div<{ disabled?: boolean }>`
cursor: pointer;
display: flex;
align-items: center;
font-size: 15px;
color: rgba(${props => props.theme.colors.text.primary});
&:not(:last-child) {
margin-right: 16px;
}
&:hover {
color: rgba(${props => props.theme.colors.text.secondary});
}
${props =>
props.disabled &&
css`
opacity: 0.5;
cursor: default;
pointer-events: none;
`}
`;
const ProjectActionText = styled.span`
padding-left: 4px;
`;
const Project = () => { const Project = () => {
const { projectID } = useParams<ProjectParams>(); const {projectID} = useParams<ProjectParams>();
const history = useHistory(); const history = useHistory();
const match = useRouteMatch(); const match = useRouteMatch();
const [updateTaskDescription] = useUpdateTaskDescriptionMutation(); const [updateTaskDescription] = useUpdateTaskDescriptionMutation();
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
const [updateTaskLocation] = useUpdateTaskLocationMutation({
update: (client, newTask) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
if (previousTaskGroupID !== task.taskGroup.id) {
const { taskGroups } = cache.findProject;
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
(t: Task) => t.id !== task.id,
);
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
...taskGroups[newTaskGroupIdx].tasks,
{ ...task },
];
}
}
}),
{ projectId: projectID },
);
},
});
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation({});
const [updateProjectMemberRole] = useUpdateProjectMemberRoleMutation();
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
onCompleted: deletedTaskGroupData => {},
update: (client, deletedTaskGroupData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
);
}),
{ projectId: projectID },
);
},
});
const [createTaskGroup] = useCreateTaskGroupMutation({
onCompleted: newTaskGroupData => {},
update: (client, newTaskGroupData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache => {
console.log(cache);
return produce(cache, draftCache => {
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
});
},
{ projectId: projectID },
);
},
});
const [createTask] = useCreateTaskMutation({
onCompleted: newTaskData => {},
update: (client, newTaskData) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const { taskGroups } = cache.findProject;
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data.createTask.taskGroup.id);
if (idx !== -1) {
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
}
}),
{ projectId: projectID },
);
},
});
const [deleteTask] = useDeleteTaskMutation({
onCompleted: deletedTask => {},
});
const [updateTaskName] = useUpdateTaskNameMutation({
onCompleted: newTaskData => {},
});
const [toggleTaskLabel] = useToggleTaskLabelMutation({ const [toggleTaskLabel] = useToggleTaskLabelMutation({
onCompleted: newTaskLabel => { onCompleted: newTaskLabel => {
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels; taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
console.log(taskLabelsRef.current); console.log(taskLabelsRef.current);
}, },
}); });
const { loading, data, refetch } = useFindProjectQuery({ const [updateProjectMemberRole] = useUpdateProjectMemberRoleMutation();
variables: { projectId: projectID },
onCompleted: newData => {}, const [deleteTask] = useDeleteTaskMutation();
const [updateTaskName] = useUpdateTaskNameMutation();
const {loading, data} = useFindProjectQuery({
variables: {projectId: projectID},
}); });
const onCardCreate = (taskGroupID: string, name: string) => {
if (data) {
const taskGroupTasks = data.findProject.taskGroups.filter(t => t.id === taskGroupID);
if (taskGroupTasks) {
let position = 65535;
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;
}
createTask({ variables: { taskGroupID, name, position } });
}
}
};
const onCreateTask = (taskGroupID: string, name: string) => {
if (data) {
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
console.log(`taskGroup ${taskGroup}`);
if (taskGroup) {
let position = 65535;
if (taskGroup.tasks.length !== 0) {
const [lastTask] = taskGroup.tasks
.slice()
.sort((a: any, b: any) => a.position - b.position)
.slice(-1);
position = Math.ceil(lastTask.position) * 2 + 1;
}
console.log(`position ${position}`);
createTask({
variables: { taskGroupID, name, position },
optimisticResponse: {
__typename: 'Mutation',
createTask: {
__typename: 'Task',
id: '' + Math.round(Math.random() * -1000000),
name,
complete: false,
taskGroup: {
__typename: 'TaskGroup',
id: taskGroup.id,
name: taskGroup.name,
position: taskGroup.position,
},
badges: {
checklist: null,
},
position,
dueDate: null,
description: null,
labels: [],
assigned: [],
},
},
});
}
}
};
const onListDrop = (droppedColumn: TaskGroup) => {
console.log(`list drop ${droppedColumn.id}`);
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const taskGroupIdx = cache.findProject.taskGroups.findIndex(t => t.id === droppedColumn.id);
if (taskGroupIdx !== -1) {
draftCache.findProject.taskGroups[taskGroupIdx].position = droppedColumn.position;
}
}),
{
projectId: projectID,
},
);
updateTaskGroupLocation({
variables: { taskGroupID: droppedColumn.id, position: droppedColumn.position },
optimisticResponse: {
updateTaskGroupLocation: {
id: droppedColumn.id,
position: droppedColumn.position,
},
},
});
};
const onCreateList = (listName: string) => {
if (data) {
const [lastColumn] = data.findProject.taskGroups.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 } });
}
};
const [assignTask] = useAssignTaskMutation();
const [unassignTask] = useUnassignTaskMutation();
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
const [updateProjectName] = useUpdateProjectNameMutation({ const [updateProjectName] = useUpdateProjectNameMutation({
update: (client, newName) => { update: (client, newName) => {
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
@ -518,12 +129,11 @@ const Project = () => {
produce(cache, draftCache => { produce(cache, draftCache => {
draftCache.findProject.name = newName.data.updateProjectName.name; draftCache.findProject.name = newName.data.updateProjectName.name;
}), }),
{ projectId: projectID }, {projectId: projectID},
); );
}, },
}); });
const [setTaskComplete] = useSetTaskCompleteMutation();
const [createProjectMember] = useCreateProjectMemberMutation({ const [createProjectMember] = useCreateProjectMemberMutation({
update: (client, response) => { update: (client, response) => {
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
@ -531,9 +141,9 @@ const Project = () => {
FindProjectDocument, FindProjectDocument,
cache => cache =>
produce(cache, draftCache => { produce(cache, draftCache => {
draftCache.findProject.members.push({ ...response.data.createProjectMember.member }); draftCache.findProject.members.push({...response.data.createProjectMember.member});
}), }),
{ projectId: projectID }, {projectId: projectID},
); );
}, },
}); });
@ -549,16 +159,15 @@ const Project = () => {
m => m.id !== response.data.deleteProjectMember.member.id, m => m.id !== response.data.deleteProjectMember.member.id,
); );
}), }),
{ projectId: projectID }, {projectId: projectID},
); );
}, },
}); });
const client = useApolloClient(); const {userID} = useContext(UserIDContext);
const { userID } = useContext(UserIDContext);
const location = useLocation(); const location = useLocation();
const { showPopup, hidePopup } = usePopup(); const {showPopup, hidePopup} = usePopup();
const $labelsRef = useRef<HTMLDivElement>(null); const $labelsRef = useRef<HTMLDivElement>(null);
const labelsRef = useRef<Array<ProjectLabel>>([]); const labelsRef = useRef<Array<ProjectLabel>>([]);
const taskLabelsRef = useRef<Array<TaskLabel>>([]); const taskLabelsRef = useRef<Array<TaskLabel>>([]);
@ -576,58 +185,32 @@ const Project = () => {
} }
if (data) { if (data) {
console.log(data.findProject); console.log(data.findProject);
const onQuickEditorOpen = ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => {
if ($target && $target.current) {
const pos = $target.current.getBoundingClientRect();
const height = 120;
if (window.innerHeight - pos.bottom < height) {
}
}
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
const currentTask = taskGroup ? taskGroup.tasks.find(t => t.id === taskID) : null;
if (currentTask) {
setQuickCardEditor({
target: $target,
isOpen: true,
taskID: currentTask.id,
taskGroupID: currentTask.taskGroup.id,
});
}
};
labelsRef.current = data.findProject.labels; labelsRef.current = data.findProject.labels;
let currentQuickTask = null;
if (quickCardEditor.taskID && quickCardEditor.taskGroupID) {
const targetGroup = data.findProject.taskGroups.find(t => t.id === quickCardEditor.taskGroupID);
if (targetGroup) {
currentQuickTask = targetGroup.tasks.find(t => t.id === quickCardEditor.taskID);
}
}
return ( return (
<> <>
<GlobalTopNavbar <GlobalTopNavbar
onChangeRole={(userID, roleCode) => { onChangeRole={(userID, roleCode) => {
updateProjectMemberRole({ variables: { userID, roleCode, projectID } }); updateProjectMemberRole({variables: {userID, roleCode, projectID}});
}} }}
onChangeProjectOwner={uid => { onChangeProjectOwner={uid => {
setProjectOwner({ variables: { ownerID: uid, projectID } }); setProjectOwner({variables: {ownerID: uid, projectID}});
hidePopup(); hidePopup();
}} }}
onRemoveFromBoard={userID => { onRemoveFromBoard={userID => {
deleteProjectMember({ variables: { userID, projectID } }); deleteProjectMember({variables: {userID, projectID}});
hidePopup(); hidePopup();
}} }}
onSaveProjectName={projectName => { onSaveProjectName={projectName => {
updateProjectName({ variables: { projectID, name: projectName } }); updateProjectName({variables: {projectID, name: projectName}});
}} }}
onInviteUser={$target => { onInviteUser={$target => {
showPopup( showPopup(
$target, $target,
<UserManagementPopup <UserManagementPopup
onAddProjectMember={userID => { onAddProjectMember={userID => {
createProjectMember({ variables: { userID, projectID } }); createProjectMember({variables: {userID, projectID}});
}} }}
users={data.users} users={data.users}
projectMembers={data.findProject.members} projectMembers={data.findProject.members}
@ -635,250 +218,41 @@ const Project = () => {
); );
}} }}
popupContent={<ProjectPopup history={history} name={data.findProject.name} projectID={projectID} />} popupContent={<ProjectPopup history={history} name={data.findProject.name} projectID={projectID} />}
menuType={[{ name: 'Board', link: location.pathname }]} menuType={[{name: 'Board', link: location.pathname}]}
currentTab={0} currentTab={0}
projectMembers={data.findProject.members} projectMembers={data.findProject.members}
projectID={projectID} projectID={projectID}
name={data.findProject.name} name={data.findProject.name}
/> />
<ProjectBar>
<ProjectActions>
<ProjectAction disabled>
<CheckCircle width={13} height={13} />
<ProjectActionText>All Tasks</ProjectActionText>
</ProjectAction>
<ProjectAction disabled>
<Filter width={13} height={13} />
<ProjectActionText>Filter</ProjectActionText>
</ProjectAction>
<ProjectAction disabled>
<Sort width={13} height={13} />
<ProjectActionText>Sort</ProjectActionText>
</ProjectAction>
</ProjectActions>
<ProjectActions>
<ProjectAction
ref={$labelsRef}
onClick={() => {
showPopup(
$labelsRef,
<LabelManagerEditor
taskLabels={null}
labelColors={data.labelColors}
labels={labelsRef}
projectID={projectID}
/>,
);
}}
>
<Tags width={13} height={13} />
<ProjectActionText>Labels</ProjectActionText>
</ProjectAction>
<ProjectAction disabled>
<ToggleOn width={13} height={13} />
<ProjectActionText>Fields</ProjectActionText>
</ProjectAction>
<ProjectAction disabled>
<Bolt width={13} height={13} />
<ProjectActionText>Rules</ProjectActionText>
</ProjectAction>
</ProjectActions>
</ProjectBar>
<SimpleLists
onTaskClick={task => {
history.push(`${match.url}/c/${task.id}`);
}}
onTaskDrop={(droppedTask, previousTaskGroupID) => {
updateTaskLocation({
variables: {
taskID: droppedTask.id,
taskGroupID: droppedTask.taskGroup.id,
position: droppedTask.position,
},
optimisticResponse: {
__typename: 'Mutation',
updateTaskLocation: {
previousTaskGroupID,
task: {
name: droppedTask.name,
id: droppedTask.id,
position: droppedTask.position,
taskGroup: {
id: droppedTask.taskGroup.id,
__typename: 'TaskGroup',
},
createdAt: '',
__typename: 'Task',
},
},
},
});
}}
onTaskGroupDrop={droppedTaskGroup => {
updateTaskGroupLocation({
variables: { taskGroupID: droppedTaskGroup.id, position: droppedTaskGroup.position },
optimisticResponse: {
__typename: 'Mutation',
updateTaskGroupLocation: {
id: droppedTaskGroup.id,
position: droppedTaskGroup.position,
__typename: 'TaskGroup',
},
},
});
}}
taskGroups={data.findProject.taskGroups}
onCreateTask={onCreateTask}
onCreateTaskGroup={onCreateList}
onCardMemberClick={($targetRef, taskID, memberID) => {
const member = data.findProject.members.find(m => m.id === memberID);
if (member) {
showPopup(
$targetRef,
<MiniProfile
user={member}
bio="None"
onRemoveFromTask={() => {
/* unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); */
}}
/>,
);
}
}}
onChangeTaskGroupName={(taskGroupID, name) => {
updateTaskGroupName({ variables: { taskGroupID, name } });
}}
onQuickEditorOpen={onQuickEditorOpen}
onExtraMenuOpen={(taskGroupID: string, $targetRef: any) => {
showPopup(
$targetRef,
<Popup title="List actions" tab={0} onClose={() => hidePopup()}>
<ListActions
taskGroupID={taskGroupID}
onArchiveTaskGroup={tgID => {
deleteTaskGroup({ variables: { taskGroupID: tgID } });
hidePopup();
}}
/>
</Popup>,
);
}}
/>
{quickCardEditor.isOpen && currentQuickTask && quickCardEditor.target && (
<QuickCardEditor
task={currentQuickTask}
onCloseEditor={() => setQuickCardEditor(initialQuickCardEditorState)}
onEditCard={(_taskGroupID: string, taskID: string, cardName: string) => {
updateTaskName({ variables: { taskID, name: cardName } });
}}
onOpenMembersPopup={($targetRef, task) => {
showPopup(
$targetRef,
<Popup title="Members" tab={0} onClose={() => hidePopup()}>
<MemberManager
availableMembers={data.findProject.members}
activeMembers={task.assigned ?? []}
onMemberChange={(member, isActive) => {
if (isActive) {
assignTask({ variables: { taskID: task.id, userID: userID ?? '' } });
} else {
unassignTask({ variables: { taskID: task.id, userID: userID ?? '' } });
}
}}
/>
</Popup>,
);
}}
onCardMemberClick={($targetRef, taskID, memberID) => {
const member = data.findProject.members.find(m => m.id === memberID);
if (member) {
showPopup(
$targetRef,
<MiniProfile
bio="None"
user={member}
onRemoveFromTask={() => {
/* unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); */
}}
/>,
);
}
}}
onOpenLabelsPopup={($targetRef, task) => {
taskLabelsRef.current = task.labels;
showPopup(
$targetRef,
<LabelManagerEditor
onLabelToggle={labelID => {
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
}}
labelColors={data.labelColors}
labels={labelsRef}
taskLabels={taskLabelsRef}
projectID={projectID}
/>,
);
}}
onArchiveCard={(_listId: string, cardId: string) =>
deleteTask({
variables: { taskID: cardId },
update: () => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups = cache.findProject.taskGroups.map(taskGroup => ({
...taskGroup,
tasks: taskGroup.tasks.filter(t => t.id !== cardId),
}));
}),
{ projectId: projectID },
);
},
})
}
onOpenDueDatePopup={($targetRef, task) => {
showPopup(
$targetRef,
<Popup title={'Change Due Date'} tab={0} onClose={() => hidePopup()}>
<DueDateManager
task={task}
onRemoveDueDate={t => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null } });
hidePopup();
}}
onDueDateChange={(t, newDueDate) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } });
hidePopup();
}}
onCancel={() => {}}
/>
</Popup>,
);
}}
onToggleComplete={task => {
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
}}
target={quickCardEditor.target}
/>
)}
<Route <Route
path={`${match.path}/c/:taskID`} path={`${match.path}`}
exact
render={() => (
<Redirect to={`${match.url}/board`} />
)}
/>
<Route
path={`${match.path}/board`}
render={() => (
<Board projectID={projectID} />
)}
/>
<Route
path={`${match.path}/board/c/:taskID`}
render={(routeProps: RouteComponentProps<TaskRouteProps>) => ( render={(routeProps: RouteComponentProps<TaskRouteProps>) => (
<Details <Details
refreshCache={() => {}} refreshCache={() => {}}
availableMembers={data.findProject.members} availableMembers={data.findProject.members}
projectURL={match.url} projectURL={`${match.url}/board`}
taskID={routeProps.match.params.taskID} taskID={routeProps.match.params.taskID}
onTaskNameChange={(updatedTask, newName) => { onTaskNameChange={(updatedTask, newName) => {
updateTaskName({ variables: { taskID: updatedTask.id, name: newName } }); updateTaskName({variables: {taskID: updatedTask.id, name: newName}});
}} }}
onTaskDescriptionChange={(updatedTask, newDescription) => { onTaskDescriptionChange={(updatedTask, newDescription) => {
updateTaskDescription({ variables: { taskID: updatedTask.id, description: newDescription } }); updateTaskDescription({variables: {taskID: updatedTask.id, description: newDescription}});
}} }}
onDeleteTask={deletedTask => { onDeleteTask={deletedTask => {
deleteTask({ variables: { taskID: deletedTask.id } }); deleteTask({variables: {taskID: deletedTask.id}});
}} }}
onOpenAddLabelPopup={(task, $targetRef) => { onOpenAddLabelPopup={(task, $targetRef) => {
taskLabelsRef.current = task.labels; taskLabelsRef.current = task.labels;
@ -886,7 +260,7 @@ const Project = () => {
$targetRef, $targetRef,
<LabelManagerEditor <LabelManagerEditor
onLabelToggle={labelID => { onLabelToggle={labelID => {
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } }); toggleTaskLabel({variables: {taskID: task.id, projectLabelID: labelID}});
}} }}
labelColors={data.labelColors} labelColors={data.labelColors}
labels={labelsRef} labels={labelsRef}

View File

@ -1,8 +1,10 @@
import React from 'react'; import React, { useState, useContext } from 'react';
import Input from 'shared/components/Input'; import Input from 'shared/components/Input';
import updateApolloCache from 'shared/utils/cache'; import updateApolloCache from 'shared/utils/cache';
import produce from 'immer'; import produce from 'immer';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import UserIDContext from 'App/context';
import Select from 'shared/components/Select';
import { import {
useGetTeamQuery, useGetTeamQuery,
RoleCode, RoleCode,
@ -155,23 +157,28 @@ export const RemoveMemberButton = styled(Button)`
width: 100%; width: 100%;
`; `;
type TeamRoleManagerPopupProps = { type TeamRoleManagerPopupProps = {
user: TaskUser; currentUserID: string;
subject: TaskUser;
members: Array<TaskUser>;
warning?: string | null; warning?: string | null;
canChangeRole: boolean; canChangeRole: boolean;
onChangeRole: (roleCode: RoleCode) => void; onChangeRole: (roleCode: RoleCode) => void;
onRemoveFromTeam?: () => void; onRemoveFromTeam?: (newOwnerID: string | null) => void;
onChangeTeamOwner?: (userID: string) => void; onChangeTeamOwner?: (userID: string) => void;
}; };
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
members,
warning, warning,
user, subject,
currentUserID,
canChangeRole, canChangeRole,
onRemoveFromTeam, onRemoveFromTeam,
onChangeTeamOwner, onChangeTeamOwner,
onChangeRole, onChangeRole,
}) => { }) => {
const { hidePopup, setTab } = usePopup(); const { hidePopup, setTab } = usePopup();
const [orphanedProjectOwner, setOrphanedProjectOwner] = useState<{ label: string; value: string } | null>(null);
return ( return (
<> <>
<Popup title={null} tab={0}> <Popup title={null} tab={0}>
@ -186,14 +193,14 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
Set as team owner... Set as team owner...
</MiniProfileActionItem> </MiniProfileActionItem>
)} )}
{user.role && ( {subject.role && (
<MiniProfileActionItem <MiniProfileActionItem
onClick={() => { onClick={() => {
setTab(1); setTab(1);
}} }}
> >
Change permissions... Change permissions...
<CurrentPermission>{`(${user.role.name})`}</CurrentPermission> <CurrentPermission>{`(${subject.role.name})`}</CurrentPermission>
</MiniProfileActionItem> </MiniProfileActionItem>
)} )}
{onRemoveFromTeam && ( {onRemoveFromTeam && (
@ -202,7 +209,7 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
setTab(2); setTab(2);
}} }}
> >
Remove from team... {currentUserID === subject.id ? 'Leave team...' : 'Remove from team...'}
</MiniProfileActionItem> </MiniProfileActionItem>
)} )}
</MiniProfileActionWrapper> </MiniProfileActionWrapper>
@ -218,13 +225,13 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
<MiniProfileActions> <MiniProfileActions>
<MiniProfileActionWrapper> <MiniProfileActionWrapper>
{permissions {permissions
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner') .filter(p => (subject.role && subject.role.code === 'owner') || p.code !== 'owner')
.map(perm => ( .map(perm => (
<MiniProfileActionItem <MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole} disabled={subject.role && perm.code !== subject.role.code && !canChangeRole}
key={perm.code} key={perm.code}
onClick={() => { onClick={() => {
if (onChangeRole && user.role && perm.code !== user.role.code) { if (onChangeRole && subject.role && perm.code !== subject.role.code) {
switch (perm.code) { switch (perm.code) {
case 'owner': case 'owner':
onChangeRole(RoleCode.Owner); onChangeRole(RoleCode.Owner);
@ -244,13 +251,13 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
> >
<RoleName> <RoleName>
{perm.name} {perm.name}
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />} {subject.role && perm.code === subject.role.code && <RoleCheckmark width={12} height={12} />}
</RoleName> </RoleName>
<RoleDescription>{perm.description}</RoleDescription> <RoleDescription>{perm.description}</RoleDescription>
</MiniProfileActionItem> </MiniProfileActionItem>
))} ))}
</MiniProfileActionWrapper> </MiniProfileActionWrapper>
{user.role && user.role.code === 'owner' && ( {subject.role && subject.role.code === 'owner' && (
<> <>
<Separator /> <Separator />
<WarningText>You can't change roles because there must be an owner.</WarningText> <WarningText>You can't change roles because there must be an owner.</WarningText>
@ -261,13 +268,28 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}> <Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}>
<Content> <Content>
<DeleteDescription> <DeleteDescription>
The member will be removed from all cards on this project. They will receive a notification. The member will be removed from all team project tasks. They will receive a notification.
</DeleteDescription> </DeleteDescription>
{subject.owned && subject.owned.projects.length !== 0 && (
<>
<DeleteDescription>
{`The member is the owner of ${subject.owned.projects.length} project${
subject.owned.projects.length > 1 ? 's' : ''
}. You can give the projects a new owner but it is not needed`}
</DeleteDescription>
<Select
label="New projects owner"
value={orphanedProjectOwner}
onChange={value => setOrphanedProjectOwner(value)}
options={members.filter(m => m.id !== subject.id).map(m => ({ label: m.fullName, value: m.id }))}
/>
</>
)}
<RemoveMemberButton <RemoveMemberButton
color="danger" color="danger"
onClick={() => { onClick={() => {
if (onRemoveFromTeam) { if (onRemoveFromTeam) {
onRemoveFromTeam(); onRemoveFromTeam(orphanedProjectOwner ? orphanedProjectOwner.value : null);
} }
}} }}
> >
@ -278,14 +300,14 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
<Popup title="Set as Team Owner?" onClose={() => hidePopup()} tab={3}> <Popup title="Set as Team Owner?" onClose={() => hidePopup()} tab={3}>
<Content> <Content>
<DeleteDescription> <DeleteDescription>
This will change the project owner from you to this user. They will be able to view and edit cards, remove This will change the project owner from you to this subject. They will be able to view and edit cards,
members, and change all settings for the project. They will also be able to delete the project. remove members, and change all settings for the project. They will also be able to delete the project.
</DeleteDescription> </DeleteDescription>
<RemoveMemberButton <RemoveMemberButton
color="warning" color="warning"
onClick={() => { onClick={() => {
if (onChangeTeamOwner) { if (onChangeTeamOwner) {
onChangeTeamOwner(user.id); onChangeTeamOwner(subject.id);
} }
}} }}
> >
@ -421,6 +443,7 @@ type MembersProps = {
const Members: React.FC<MembersProps> = ({ teamID }) => { const Members: React.FC<MembersProps> = ({ teamID }) => {
const { showPopup, hidePopup } = usePopup(); const { showPopup, hidePopup } = usePopup();
const { loading, data } = useGetTeamQuery({ variables: { teamID } }); const { loading, data } = useGetTeamQuery({ variables: { teamID } });
const { userID } = useContext(UserIDContext);
const warning = const warning =
'You cant leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.'; 'You cant leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
const [createTeamMember] = useCreateTeamMemberMutation({ const [createTeamMember] = useCreateTeamMemberMutation({
@ -508,7 +531,9 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
showPopup( showPopup(
$target, $target,
<TeamRoleManagerPopup <TeamRoleManagerPopup
user={member} currentUserID={userID ?? ''}
subject={member}
members={data.findTeam.members}
warning={member.role && member.role.code === 'owner' ? warning : null} warning={member.role && member.role.code === 'owner' ? warning : null}
onChangeTeamOwner={ onChangeTeamOwner={
member.role && member.role.code !== 'owner' ? (userID: string) => {} : undefined member.role && member.role.code !== 'owner' ? (userID: string) => {} : undefined
@ -518,8 +543,8 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
onRemoveFromTeam={ onRemoveFromTeam={
member.role && member.role.code === 'owner' member.role && member.role.code === 'owner'
? undefined ? undefined
: () => { : newOwnerID => {
deleteTeamMember({ variables: { teamID, userID: member.id } }); deleteTeamMember({ variables: { teamID, newOwnerID, userID: member.id } });
hidePopup(); hidePopup();
} }
} }

View File

@ -31,12 +31,18 @@ type User = {
profileIcon: ProfileIcon; profileIcon: ProfileIcon;
}; };
type OwnedList = {
projects: Array<string>;
teams: Array<string>;
};
type TaskUser = { type TaskUser = {
id: string; id: string;
fullName: string; fullName: string;
profileIcon: ProfileIcon; profileIcon: ProfileIcon;
username?: string; username?: string;
role?: Role; role?: Role;
owned?: OwnedList | null;
}; };
type RefreshTokenResponse = { type RefreshTokenResponse = {

View File

@ -1,38 +1,294 @@
import React, { useState, useRef } from 'react'; import React, {useState, useRef} from 'react';
import styled from 'styled-components'; import {UserPlus, Checkmark} from 'shared/icons';
import { User, Plus, Lock, Pencil, Trash } from 'shared/icons'; import styled, {css} from 'styled-components';
import { AgGridReact } from 'ag-grid-react'; import TaskAssignee from 'shared/components/TaskAssignee';
import {User, Plus, Lock, Pencil, Trash} from 'shared/icons';
import {usePopup, Popup} from 'shared/components/PopupMenu';
import {RoleCode, useUpdateUserRoleMutation} from 'shared/generated/graphql';
import {AgGridReact} from 'ag-grid-react';
import Input from 'shared/components/Input';
import Member from 'shared/components/Member';
import 'ag-grid-community/dist/styles/ag-grid.css'; import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-material.css'; import 'ag-grid-community/dist/styles/ag-theme-material.css';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
const NewUserButton = styled(Button)` export const RoleCheckmark = styled(Checkmark)`
padding-left: 4px;
`;
const permissions = [
{
code: 'owner',
name: 'Owner',
description:
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team. Can delete the team and all team projects.',
},
{
code: 'admin',
name: 'Admin',
description:
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team.',
},
{code: 'member', name: 'Member', description: 'Can view, create, and edit team projects, but not change settings.'},
];
export const RoleName = styled.div`
font-size: 14px;
font-weight: 700;
`;
export const RoleDescription = styled.div`
margin-top: 4px;
font-size: 14px;
`;
export const MiniProfileActions = styled.ul`
list-style-type: none;
`;
export const MiniProfileActionWrapper = styled.li``;
export const MiniProfileActionItem = styled.span<{disabled?: boolean}>`
color: #c2c6dc;
display: block;
font-weight: 400;
padding: 6px 12px; padding: 6px 12px;
margin-right: 12px;
`;
const InviteUserButton = styled(Button)`
padding: 6px 12px;
margin-right: 8px;
`;
const MemberActions = styled.div`
display: flex;
justify-content: flex-end;
align-items: center;
`;
const GridTable = styled.div`
height: 620px;
`;
const RootWrapper = styled.div`
height: 100%;
display: flex;
position: relative; position: relative;
text-decoration: none;
${props =>
props.disabled
? css`
user-select: none;
pointer-events: none;
color: rgba(${props.theme.colors.text.primary}, 0.4);
`
: css`
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`}
`;
export const Content = styled.div`
padding: 0 12px 12px;
`;
export const CurrentPermission = styled.span`
margin-left: 4px;
color: rgba(${props => props.theme.colors.text.secondary}, 0.4);
`;
export const Separator = styled.div`
height: 1px;
border-top: 1px solid #414561;
margin: 0.25rem !important;
`;
export const WarningText = styled.span`
display: flex;
color: rgba(${props => props.theme.colors.text.primary}, 0.4);
padding: 6px;
`;
export const DeleteDescription = styled.div`
font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary});
`;
export const RemoveMemberButton = styled(Button)`
margin-top: 16px;
padding: 6px 12px;
width: 100%;
`;
type TeamRoleManagerPopupProps = {
user: TaskUser;
warning?: string | null;
canChangeRole: boolean;
onChangeRole: (roleCode: RoleCode) => void;
onRemoveFromTeam?: () => void;
};
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
warning,
user,
canChangeRole,
onRemoveFromTeam,
onChangeRole,
}) => {
const {hidePopup, setTab} = usePopup();
return (
<>
<Popup title={null} tab={0}>
<MiniProfileActions>
<MiniProfileActionWrapper>
{user.role && (
<MiniProfileActionItem
onClick={() => {
setTab(1);
}}
>
Change permissions...
<CurrentPermission>{`(${user.role.name})`}</CurrentPermission>
</MiniProfileActionItem>
)}
<MiniProfileActionItem onClick={() => {}}>Reset password...</MiniProfileActionItem>
<MiniProfileActionItem onClick={() => {}}>Lock user...</MiniProfileActionItem>
<MiniProfileActionItem onClick={() => {}}>Remove from organzation...</MiniProfileActionItem>
</MiniProfileActionWrapper>
</MiniProfileActions>
{warning && (
<>
<Separator />
<WarningText>{warning}</WarningText>
</>
)}
</Popup>
<Popup title="Change Permissions" onClose={() => hidePopup()} tab={1}>
<MiniProfileActions>
<MiniProfileActionWrapper>
{permissions
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map(perm => (
<MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
key={perm.code}
onClick={() => {
if (onChangeRole && user.role && perm.code !== user.role.code) {
switch (perm.code) {
case 'owner':
onChangeRole(RoleCode.Owner);
break;
case 'admin':
onChangeRole(RoleCode.Admin);
break;
case 'member':
onChangeRole(RoleCode.Member);
break;
default:
break;
}
hidePopup();
}
}}
>
<RoleName>
{perm.name}
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />}
</RoleName>
<RoleDescription>{perm.description}</RoleDescription>
</MiniProfileActionItem>
))}
</MiniProfileActionWrapper>
{user.role && user.role.code === 'owner' && (
<>
<Separator />
<WarningText>You can't change roles because there must be an owner.</WarningText>
</>
)}
</MiniProfileActions>
</Popup>
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}>
<Content>
<DeleteDescription>
The member will be removed from all cards on this project. They will receive a notification.
</DeleteDescription>
<RemoveMemberButton
color="danger"
onClick={() => {
if (onRemoveFromTeam) {
onRemoveFromTeam();
}
}}
>
Remove Member
</RemoveMemberButton>
</Content>
</Popup>
</>
);
};
const InviteMemberButton = styled(Button)`
padding: 7px 12px;
`;
const MemberItemOptions = styled.div``;
const MemberItemOption = styled(Button)`
padding: 7px 9px;
margin: 4px 0 4px 8px;
float: left;
min-width: 95px;
`;
const MemberList = styled.div`
border-top: 1px solid rgba(${props => props.theme.colors.border});
`;
const MemberListItem = styled.div`
display: flex;
flex-flow: row wrap;
justify-content: space-between;
border-bottom: 1px solid rgba(${props => props.theme.colors.border});
min-height: 40px;
padding: 12px 0 12px 40px;
position: relative;
`;
const MemberListItemDetails = styled.div`
float: left;
flex: 1 0 auto;
padding-left: 8px;
`;
const InviteIcon = styled(UserPlus)`
padding-right: 4px;
`;
const MemberProfile = styled(TaskAssignee)`
position: absolute;
top: 16px;
left: 0;
margin: 0;
`;
const MemberItemName = styled.p`
color: rgba(${props => props.theme.colors.text.secondary});
`;
const MemberItemUsername = styled.p`
color: rgba(${props => props.theme.colors.text.primary});
`;
const MemberListHeader = styled.div`
display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; `;
const ListTitle = styled.h3`
font-size: 18px;
color: rgba(${props => props.theme.colors.text.secondary});
margin-bottom: 12px;
`;
const ListDesc = styled.span`
font-size: 16px;
color: rgba(${props => props.theme.colors.text.primary});
`;
const FilterSearch = styled(Input)`
margin: 0;
`;
const ListActions = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
margin-bottom: 18px;
`;
const MemberListWrapper = styled.div`
flex: 1 1;
`; `;
const Root = styled.div` const Root = styled.div`
@ -103,7 +359,7 @@ const ActionButtonWrapper = styled.div`
display: inline-flex; display: inline-flex;
`; `;
const ActionButton: React.FC<ActionButtonProps> = ({ onClick, children }) => { const ActionButton: React.FC<ActionButtonProps> = ({onClick, children}) => {
const $wrapper = useRef<HTMLDivElement>(null); const $wrapper = useRef<HTMLDivElement>(null);
return ( return (
<ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}> <ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}>
@ -133,7 +389,7 @@ type ListTableProps = {
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void; onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
}; };
const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => { const ListTable: React.FC<ListTableProps> = ({users, onDeleteUser}) => {
const data = { const data = {
defaultColDef: { defaultColDef: {
resizable: true, resizable: true,
@ -146,10 +402,10 @@ const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
headerCheckboxSelection: true, headerCheckboxSelection: true,
checkboxSelection: true, checkboxSelection: true,
}, },
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' }, {minWidth: 210, headerName: 'Username', editable: true, field: 'username'},
{ minWidth: 225, headerName: 'Email', field: 'email' }, {minWidth: 225, headerName: 'Email', field: 'email'},
{ minWidth: 200, headerName: 'Name', editable: true, field: 'fullName' }, {minWidth: 200, headerName: 'Name', editable: true, field: 'fullName'},
{ minWidth: 200, headerName: 'Role', editable: true, field: 'roleName' }, {minWidth: 200, headerName: 'Role', editable: true, field: 'roleName'},
{ {
minWidth: 200, minWidth: 200,
headerName: 'Actions', headerName: 'Actions',
@ -168,12 +424,12 @@ const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
}; };
return ( return (
<Root> <Root>
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}> <div className="ag-theme-material" style={{height: '296px', width: '100%'}}>
<AgGridReact <AgGridReact
rowSelection="multiple" rowSelection="multiple"
defaultColDef={data.defaultColDef} defaultColDef={data.defaultColDef}
columnDefs={data.columnDefs} columnDefs={data.columnDefs}
rowData={users.map(u => ({ ...u, roleName: u.role.name }))} rowData={users.map(u => ({...u, roleName: u.role.name}))}
frameworkComponents={data.frameworkComponents} frameworkComponents={data.frameworkComponents}
onFirstDataRendered={params => { onFirstDataRendered={params => {
params.api.sizeColumnsToFit(); params.api.sizeColumnsToFit();
@ -199,7 +455,9 @@ const Container = styled.div`
padding: 2.2rem; padding: 2.2rem;
display: flex; display: flex;
width: 100%; width: 100%;
max-width: 1400px;
position: relative; position: relative;
margin: 0 auto;
`; `;
const TabNav = styled.div` const TabNav = styled.div`
@ -223,7 +481,7 @@ const TabNavItem = styled.li`
display: block; display: block;
position: relative; position: relative;
`; `;
const TabNavItemButton = styled.button<{ active: boolean }>` const TabNavItemButton = styled.button<{active: boolean}>`
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@ -250,7 +508,7 @@ const TabNavItemSpan = styled.span`
font-size: 14px; font-size: 14px;
`; `;
const TabNavLine = styled.span<{ top: number }>` const TabNavLine = styled.span<{top: number}>`
left: auto; left: auto;
right: 0; right: 0;
width: 2px; width: 2px;
@ -270,6 +528,7 @@ const TabContentWrapper = styled.div`
display: block; display: block;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
margin-left: 1rem;
`; `;
const TabContent = styled.div` const TabContent = styled.div`
@ -279,17 +538,10 @@ const TabContent = styled.div`
padding: 0; padding: 0;
padding: 1.5rem; padding: 1.5rem;
background-color: #10163a; background-color: #10163a;
margin-left: 1rem !important;
border-radius: 0.5rem; border-radius: 0.5rem;
`; `;
const items = [ const items = [{name: 'Members'}, {name: 'Settings'}];
{ name: 'Insights' },
{ name: 'Members' },
{ name: 'Teams' },
{ name: 'Security' },
{ name: 'Settings' },
];
type NavItemProps = { type NavItemProps = {
active: boolean; active: boolean;
@ -297,7 +549,7 @@ type NavItemProps = {
tab: number; tab: number;
onClick: (tab: number, top: number) => void; onClick: (tab: number, top: number) => void;
}; };
const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => { const NavItem: React.FC<NavItemProps> = ({active, name, tab, onClick}) => {
const $item = useRef<HTMLLIElement>(null); const $item = useRef<HTMLLIElement>(null);
return ( return (
<TabNavItem <TabNavItem
@ -326,10 +578,15 @@ type AdminProps = {
users: Array<User>; users: Array<User>;
}; };
const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onDeleteUser, onInviteUser, users }) => { const Admin: React.FC<AdminProps> = ({initialTab, onAddUser, onDeleteUser, onInviteUser, users}) => {
const warning =
'You cant leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
const [currentTop, setTop] = useState(initialTab * 48); const [currentTop, setTop] = useState(initialTab * 48);
const [currentTab, setTab] = useState(initialTab); const [currentTab, setTab] = useState(initialTab);
const {showPopup, hidePopup} = usePopup();
const $tabNav = useRef<HTMLDivElement>(null); const $tabNav = useRef<HTMLDivElement>(null);
const [updateUserRole] = useUpdateUserRoleMutation()
return ( return (
<Container> <Container>
<TabNav ref={$tabNav}> <TabNav ref={$tabNav}>
@ -353,17 +610,64 @@ const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onDeleteUser, onIn
</TabNav> </TabNav>
<TabContentWrapper> <TabContentWrapper>
<TabContent> <TabContent>
<MemberActions> <MemberListWrapper>
<NewUserButton variant="outline" onClick={onAddUser}> <MemberListHeader>
<Plus color="rgba(115, 103, 240)" size={10} /> <ListTitle>{`Users (${users.length})`}</ListTitle>
<span style={{ paddingLeft: '5px' }}>Create member</span> <ListDesc>
</NewUserButton> Team members can view and join all Team Visible boards and create new boards in the team.
<InviteUserButton variant="outline" onClick={onInviteUser}> </ListDesc>
<Plus color="rgba(115, 103, 240)" size={10} /> <ListActions>
<span style={{ paddingLeft: '5px' }}>Invite member</span> <FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
</InviteUserButton> <InviteMemberButton
</MemberActions> onClick={$target => {
<ListTable onDeleteUser={onDeleteUser} users={users} /> onAddUser($target);
}}
>
<InviteIcon width={16} height={16} />
New Member
</InviteMemberButton>
</ListActions>
</MemberListHeader>
<MemberList>
{users.map(member => (
<MemberListItem>
<MemberProfile showRoleIcons size={32} onMemberProfile={() => {}} member={member} />
<MemberListItemDetails>
<MemberItemName>{member.fullName}</MemberItemName>
<MemberItemUsername>{`@${member.username}`}</MemberItemUsername>
</MemberListItemDetails>
<MemberItemOptions>
<MemberItemOption variant="flat">On 6 projects</MemberItemOption>
<MemberItemOption
variant="outline"
onClick={$target => {
showPopup(
$target,
<TeamRoleManagerPopup
user={member}
warning={member.role && member.role.code === 'owner' ? warning : null}
canChangeRole={member.role && member.role.code !== 'owner'}
onChangeRole={roleCode => {
updateUserRole({variables: {userID: member.id, roleCode}})
}}
onRemoveFromTeam={
member.role && member.role.code === 'owner'
? undefined
: () => {
hidePopup();
}
}
/>,
);
}}
>
Manage
</MemberItemOption>
</MemberItemOptions>
</MemberListItem>
))}
</MemberList>
</MemberListWrapper>
</TabContent> </TabContent>
</TabContentWrapper> </TabContentWrapper>
</Container> </Container>

View File

@ -1,9 +1,9 @@
import styled, { css } from 'styled-components'; import styled, {css} from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import { mixin } from 'shared/utils/styles'; import {mixin} from 'shared/utils/styles';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
import { CheckCircle } from 'shared/icons'; import {CheckCircle} from 'shared/icons';
import { RefObject } from 'react'; import {RefObject} from 'react';
export const ClockIcon = styled(FontAwesomeIcon)``; export const ClockIcon = styled(FontAwesomeIcon)``;
@ -22,7 +22,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
min-height: 54px; min-height: 54px;
padding: 0; padding: 0;
font-size: 14px; font-size: 14px;
line-height: 16px; line-height: 18px;
color: rgba(${props => props.theme.colors.text.primary}); color: rgba(${props => props.theme.colors.text.primary});
&:focus { &:focus {
border: none; border: none;
@ -57,7 +57,7 @@ export const DescriptionBadge = styled(ListCardBadge)`
padding-right: 6px; padding-right: 6px;
`; `;
export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>` export const DueDateCardBadge = styled(ListCardBadge) <{isPastDue: boolean}>`
font-size: 12px; font-size: 12px;
${props => ${props =>
props.isPastDue && props.isPastDue &&
@ -76,7 +76,7 @@ export const ListCardBadgeText = styled.span`
white-space: nowrap; white-space: nowrap;
`; `;
export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>` export const ListCardContainer = styled.div<{isActive: boolean; editable: boolean}>`
max-width: 256px; max-width: 256px;
margin-bottom: 8px; margin-bottom: 8px;
border-radius: 3px; border-radius: 3px;
@ -93,7 +93,7 @@ export const ListCardInnerContainer = styled.div`
height: 100%; height: 100%;
`; `;
export const ListCardDetails = styled.div<{ complete: boolean }>` export const ListCardDetails = styled.div<{complete: boolean}>`
overflow: hidden; overflow: hidden;
padding: 6px 8px 2px; padding: 6px 8px 2px;
position: relative; position: relative;
@ -147,7 +147,7 @@ export const CardTitle = styled.span`
overflow: hidden; overflow: hidden;
text-decoration: none; text-decoration: none;
word-wrap: break-word; word-wrap: break-word;
line-height: 16px; line-height: 18px;
font-size: 14px; font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary}); color: rgba(${props => props.theme.colors.text.primary});

View File

@ -5,7 +5,7 @@ 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 from '.'; import Checklist, { ChecklistItem } from '.';
export default { export default {
component: Checklist, component: Checklist,
@ -86,6 +86,8 @@ export const Default = () => {
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Container> <Container>
<Checklist <Checklist
wrapperProps={{}}
handleProps={{}}
name={checklistName} name={checklistName}
checklistID="checklist-one" checklistID="checklist-one"
items={items} items={items}
@ -130,7 +132,21 @@ export const Default = () => {
); );
}} }}
onToggleItem={onToggleItem} onToggleItem={onToggleItem}
/> >
{items.map((item, idx) => (
<ChecklistItem
key={item.id}
wrapperProps={{}}
handleProps={{}}
itemID={item.id}
name={item.name}
complete={item.complete}
onDeleteItem={() => {}}
onChangeName={() => {}}
onToggleItem={() => {}}
/>
))}
</Checklist>
</Container> </Container>
</ThemeProvider> </ThemeProvider>
</> </>

View File

@ -1,6 +1,13 @@
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 {
isPositionChanged,
getSortedDraggables,
getNewDraggablePosition,
getAfterDropDraggableList,
} from 'shared/utils/draggables';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
import Control from 'react-select/src/components/Control'; import Control from 'react-select/src/components/Control';
@ -84,7 +91,7 @@ const ChecklistProgressBarCurrent = styled.div<{ width: number }>`
transition: width 0.14s ease-in, background 0.14s ease-in; transition: width 0.14s ease-in, background 0.14s ease-in;
`; `;
const ChecklistItems = styled.div` export const ChecklistItems = styled.div`
min-height: 8px; min-height: 8px;
`; `;
@ -199,7 +206,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` const ChecklistItemWrapper = styled.div<{ ref: any }>`
user-select: none; user-select: none;
clear: both; clear: both;
padding-left: 40px; padding-left: 40px;
@ -270,122 +277,121 @@ type ChecklistItemProps = {
complete: boolean; complete: boolean;
name: string; name: string;
onChangeName: (itemID: string, currentName: string) => void; onChangeName: (itemID: string, currentName: string) => void;
wrapperProps: any;
handleProps: any;
onToggleItem: (itemID: string, complete: boolean) => void; onToggleItem: (itemID: string, complete: boolean) => void;
onDeleteItem: (itemID: string) => void; onDeleteItem: (itemID: string) => void;
}; };
const ChecklistItem: React.FC<ChecklistItemProps> = ({ export const ChecklistItem = React.forwardRef(
itemID, (
complete, { itemID, complete, name, wrapperProps, handleProps, onChangeName, onToggleItem, onDeleteItem }: ChecklistItemProps,
name, $item,
onChangeName, ) => {
onToggleItem, const $editor = useRef<HTMLTextAreaElement>(null);
onDeleteItem, const [editting, setEditting] = useState(false);
}) => { const [currentName, setCurrentName] = useState(name);
const $item = useRef<HTMLDivElement>(null); useEffect(() => {
const $editor = useRef<HTMLTextAreaElement>(null); if (editting && $editor && $editor.current) {
const [editting, setEditting] = useState(false); $editor.current.focus();
const [currentName, setCurrentName] = useState(name); $editor.current.select();
useEffect(() => { }
if (editting && $editor && $editor.current) { }, [editting]);
$editor.current.focus(); // useOnOutsideClick($item, true, () => setEditting(false), null);
$editor.current.select(); return (
} <ChecklistItemWrapper ref={$item} {...wrapperProps} {...handleProps}>
}, [editting]); <ChecklistIcon
useOnOutsideClick($item, true, () => setEditting(false), null); onClick={e => {
return ( e.stopPropagation();
<ChecklistItemWrapper ref={$item}> onToggleItem(itemID, !complete);
<ChecklistIcon
onClick={e => {
e.stopPropagation();
onToggleItem(itemID, !complete);
}}
>
{complete ? (
<ChecklistItemCheckedIcon width={20} height={20} />
) : (
<ChecklistItemUncheckedIcon width={20} height={20} />
)}
</ChecklistIcon>
{editting ? (
<>
<ChecklistNameEditorWrapper>
<ChecklistNameEditor
ref={$editor}
onKeyDown={e => {
if (e.key === 'Enter') {
onChangeName(itemID, currentName);
setEditting(false);
}
}}
onChange={e => {
setCurrentName(e.currentTarget.value);
}}
value={currentName}
/>
</ChecklistNameEditorWrapper>
<EditControls>
<SaveButton
onClick={() => {
onChangeName(itemID, currentName);
setEditting(false);
}}
variant="relief"
>
Save
</SaveButton>
<CancelButton
onClick={e => {
e.stopPropagation();
setEditting(false);
}}
>
<Cross width={20} height={20} />
</CancelButton>
<Spacer />
<EditableDeleteButton
onClick={e => {
e.stopPropagation();
setEditting(false);
onDeleteItem(itemID);
}}
>
<Trash width={16} height={16} />
</EditableDeleteButton>
</EditControls>
</>
) : (
<ChecklistItemDetails
onClick={() => {
setEditting(true);
}} }}
> >
<ChecklistItemRow> {complete ? (
<ChecklistItemTextControls> <ChecklistItemCheckedIcon width={20} height={20} />
<ChecklistItemText complete={complete}>{name}</ChecklistItemText> ) : (
<ChecklistControls> <ChecklistItemUncheckedIcon width={20} height={20} />
<ControlButton> )}
<AssignUserButton width={14} height={14} /> </ChecklistIcon>
</ControlButton> {editting ? (
<ControlButton> <>
<ClockButton width={14} height={14} /> <ChecklistNameEditorWrapper>
</ControlButton> <ChecklistNameEditor
<ControlButton ref={$editor}
onClick={e => { onKeyDown={e => {
e.stopPropagation(); if (e.key === 'Enter') {
onDeleteItem(itemID); onChangeName(itemID, currentName);
}} setEditting(false);
> }
<TrashButton width={14} height={14} /> }}
</ControlButton> onChange={e => {
</ChecklistControls> setCurrentName(e.currentTarget.value);
</ChecklistItemTextControls> }}
</ChecklistItemRow> value={currentName}
</ChecklistItemDetails> />
)} </ChecklistNameEditorWrapper>
</ChecklistItemWrapper> <EditControls>
); <SaveButton
}; onClick={() => {
onChangeName(itemID, currentName);
setEditting(false);
}}
variant="relief"
>
Save
</SaveButton>
<CancelButton
onClick={e => {
e.stopPropagation();
setEditting(false);
}}
>
<Cross width={20} height={20} />
</CancelButton>
<Spacer />
<EditableDeleteButton
onClick={e => {
e.stopPropagation();
setEditting(false);
onDeleteItem(itemID);
}}
>
<Trash width={16} height={16} />
</EditableDeleteButton>
</EditControls>
</>
) : (
<ChecklistItemDetails
onClick={() => {
setEditting(true);
}}
>
<ChecklistItemRow>
<ChecklistItemTextControls>
<ChecklistItemText complete={complete}>{name}</ChecklistItemText>
<ChecklistControls>
<ControlButton>
<AssignUserButton width={14} height={14} />
</ControlButton>
<ControlButton>
<ClockButton width={14} height={14} />
</ControlButton>
<ControlButton
onClick={e => {
e.stopPropagation();
onDeleteItem(itemID);
}}
>
<TrashButton width={14} height={14} />
</ControlButton>
</ChecklistControls>
</ChecklistItemTextControls>
</ChecklistItemRow>
</ChecklistItemDetails>
)}
</ChecklistItemWrapper>
);
},
);
type AddNewItemProps = { type AddNewItemProps = {
onAddItem: (name: string) => void; onAddItem: (name: string) => void;
@ -503,94 +509,112 @@ type ChecklistProps = {
checklistID: string; checklistID: string;
onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void; onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
name: string; name: string;
children: React.ReactNode;
onChangeName: (item: string) => void; onChangeName: (item: string) => void;
onToggleItem: (taskID: string, complete: boolean) => void; onToggleItem: (taskID: string, complete: boolean) => void;
onChangeItemName: (itemID: string, currentName: string) => void; onChangeItemName: (itemID: string, currentName: string) => void;
wrapperProps: any;
handleProps: any;
onDeleteItem: (itemID: string) => void; onDeleteItem: (itemID: string) => void;
onAddItem: (itemName: string) => void; onAddItem: (itemName: string) => void;
items: Array<TaskChecklistItem>; items: Array<TaskChecklistItem>;
}; };
const Checklist: React.FC<ChecklistProps> = ({ const Checklist = React.forwardRef(
checklistID, (
onDeleteChecklist, {
name, checklistID,
items, children,
onToggleItem, onDeleteChecklist,
onAddItem, name,
onChangeItemName, items,
onChangeName, wrapperProps,
onDeleteItem, handleProps,
}) => { onToggleItem,
const $name = useRef<HTMLTextAreaElement>(null); onAddItem,
const complete = items.reduce((prev, item) => prev + (item.complete ? 1 : 0), 0); onChangeItemName,
const percent = items.length === 0 ? 0 : Math.floor((complete / items.length) * 100); onChangeName,
const [editting, setEditting] = useState(false); onDeleteItem,
// useOnOutsideClick($name, true, () => setEditting(false), null); }: ChecklistProps,
useEffect(() => { $container,
if (editting && $name && $name.current) { ) => {
$name.current.focus(); const $name = useRef<HTMLTextAreaElement>(null);
$name.current.select(); const complete = items.reduce((prev, item) => prev + (item.complete ? 1 : 0), 0);
} const percent = items.length === 0 ? 0 : Math.floor((complete / items.length) * 100);
}, [editting]); const [editting, setEditting] = useState(false);
return ( // useOnOutsideClick($name, true, () => setEditting(false), null);
<Wrapper> useEffect(() => {
<WindowTitle> if (editting && $name && $name.current) {
<WindowTitleIcon width={24} height={24} /> $name.current.focus();
{editting ? ( $name.current.select();
<ChecklistTitleEditor }
ref={$name} }, [editting]);
name={name} useEffect(() => {
onChangeName={currentName => { console.log($container);
onChangeName(currentName); }, [$container]);
setEditting(false); return (
}} <Wrapper ref={$container} {...wrapperProps}>
onCancel={() => { <WindowTitle>
setEditting(false); <WindowTitleIcon width={24} height={24} />
}} {editting ? (
/> <ChecklistTitleEditor
) : ( ref={$name}
<WindowChecklistTitle> name={name}
<WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText> onChangeName={currentName => {
<WindowOptions> onChangeName(currentName);
<DeleteButton setEditting(false);
onClick={$target => { }}
onDeleteChecklist($target, checklistID); onCancel={() => {
}} setEditting(false);
color="danger" }}
variant="outline"
>
Delete
</DeleteButton>
</WindowOptions>
</WindowChecklistTitle>
)}
</WindowTitle>
<ChecklistProgress>
<ChecklistProgressPercent>{`${percent}%`}</ChecklistProgressPercent>
<ChecklistProgressBar>
<ChecklistProgressBarCurrent width={percent} />
</ChecklistProgressBar>
</ChecklistProgress>
<ChecklistItems>
{items
.slice()
.sort((a, b) => a.position - b.position)
.map(item => (
<ChecklistItem
key={item.id}
itemID={item.id}
name={item.name}
complete={item.complete}
onDeleteItem={onDeleteItem}
onChangeName={onChangeItemName}
onToggleItem={onToggleItem}
/> />
))} ) : (
</ChecklistItems> <WindowChecklistTitle {...handleProps}>
<AddNewItem onAddItem={onAddItem} /> <WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText>
</Wrapper> <WindowOptions>
); <DeleteButton
}; onClick={$target => {
onDeleteChecklist($target, checklistID);
}}
color="danger"
variant="outline"
>
Delete
</DeleteButton>
</WindowOptions>
</WindowChecklistTitle>
)}
</WindowTitle>
<ChecklistProgress>
<ChecklistProgressPercent>{`${percent}%`}</ChecklistProgressPercent>
<ChecklistProgressBar>
<ChecklistProgressBarCurrent width={percent} />
</ChecklistProgressBar>
</ChecklistProgress>
{children}
<AddNewItem onAddItem={onAddItem} />
</Wrapper>
);
},
);
/*
<ChecklistItems>
{items
.slice()
.sort((a, b) => a.position - b.position)
.map((item, idx) => (
<ChecklistItem
index={idx}
key={item.id}
itemID={item.id}
name={item.name}
complete={item.complete}
onDeleteItem={onDeleteItem}
onChangeName={onChangeItemName}
onToggleItem={onToggleItem}
/>
))}
</ChecklistItems>
*/
export default Checklist; export default Checklist;

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, {useState, useEffect} from 'react';
import styled, { css } from 'styled-components/macro'; import styled, {css} from 'styled-components/macro';
const InputWrapper = styled.div<{ width: string }>` const InputWrapper = styled.div<{width: string}>`
position: relative; position: relative;
width: ${props => props.width}; width: ${props => props.width};
display: flex; display: flex;
@ -14,7 +14,7 @@ const InputWrapper = styled.div<{ width: string }>`
margin-top: 24px; margin-top: 24px;
`; `;
const InputLabel = styled.span<{ width: string }>` const InputLabel = styled.span<{width: string}>`
width: ${props => props.width}; width: ${props => props.width};
padding: 0.7rem !important; padding: 0.7rem !important;
color: #c2c6dc; color: #c2c6dc;

View File

@ -76,9 +76,8 @@ export const HeaderTitle = styled.span`
export const Content = styled.div` export const Content = styled.div`
max-height: 632px; max-height: 632px;
overflow-x: hidden;
overflow-y: auto;
`; `;
export const LabelSearch = styled.input` export const LabelSearch = styled.input`
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;

View File

@ -63,7 +63,6 @@ export const TaskDetailsSidebar = styled.div`
`; `;
export const TaskDetailsTitleWrapper = styled.div` export const TaskDetailsTitleWrapper = styled.div`
height: 44px;
width: 100%; width: 100%;
margin: 0 0 0 -8px; margin: 0 0 0 -8px;
display: inline-block; display: inline-block;

View File

@ -75,6 +75,8 @@ export const Default = () => {
onDeleteChecklist={action('delete checklist')} onDeleteChecklist={action('delete checklist')}
onOpenAddChecklistPopup={action(' open checklist')} onOpenAddChecklistPopup={action(' open checklist')}
onOpenDueDatePopop={action('open due date popup')} onOpenDueDatePopop={action('open due date popup')}
onChecklistDrop={action('on checklist drop')}
onChecklistItemDrop={action('on checklist item drop')}
/> />
); );
}} }}

View File

@ -2,6 +2,14 @@ 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';
import {
isPositionChanged,
getSortedDraggables,
getNewDraggablePosition,
getAfterDropDraggableList,
} from 'shared/utils/draggables';
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';
@ -46,7 +54,10 @@ import {
MetaDetailTitle, MetaDetailTitle,
MetaDetailContent, MetaDetailContent,
} from './Styles'; } from './Styles';
import Checklist from '../Checklist'; import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import styled from 'styled-components';
const ChecklistContainer = styled.div``;
type TaskContentProps = { type TaskContentProps = {
onEditContent: () => void; onEditContent: () => void;
@ -145,6 +156,8 @@ type TaskDetailsProps = {
onChangeChecklistName: (checklistID: string, name: string) => void; onChangeChecklistName: (checklistID: string, name: string) => void;
onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void; onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
onCloseModal: () => void; onCloseModal: () => void;
onChecklistDrop: (checklist: TaskChecklist) => void;
onChecklistItemDrop: (prevChecklistID: string, checklistID: string, checklistItem: TaskChecklistItem) => void;
}; };
const TaskDetails: React.FC<TaskDetailsProps> = ({ const TaskDetails: React.FC<TaskDetailsProps> = ({
@ -153,6 +166,8 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onTaskNameChange, onTaskNameChange,
onOpenAddChecklistPopup, onOpenAddChecklistPopup,
onChangeChecklistName, onChangeChecklistName,
onChecklistDrop,
onChecklistItemDrop,
onToggleTaskComplete, onToggleTaskComplete,
onTaskDescriptionChange, onTaskDescriptionChange,
onChangeItemName, onChangeItemName,
@ -190,14 +205,91 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onOpenAddMemberPopup(task, $target); onOpenAddMemberPopup(task, $target);
}; };
const onAddChecklist = ($target: React.RefObject<HTMLElement>) => { const onAddChecklist = ($target: React.RefObject<HTMLElement>) => {
onOpenAddChecklistPopup(task, $target) onOpenAddChecklistPopup(task, $target);
} };
const $dueDateLabel = useRef<HTMLDivElement>(null); const $dueDateLabel = useRef<HTMLDivElement>(null);
const $addLabelRef = useRef<HTMLDivElement>(null); const $addLabelRef = useRef<HTMLDivElement>(null);
const onAddLabel = ($target: React.RefObject<HTMLElement>) => { const onAddLabel = ($target: React.RefObject<HTMLElement>) => {
onOpenAddLabelPopup(task, $target); onOpenAddLabelPopup(task, $target);
}; };
const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => {
if (typeof destination === 'undefined') return;
if (!isPositionChanged(source, destination)) return;
const isChecklist = type === 'checklist';
const isSameChecklist = destination.droppableId === source.droppableId;
let droppedDraggable: DraggableElement | null = null;
let beforeDropDraggables: Array<DraggableElement> | null = null;
if (!task.checklists) return;
if (isChecklist) {
const droppedGroup = task.checklists.find(taskGroup => taskGroup.id === draggableId);
if (droppedGroup) {
droppedDraggable = {
id: draggableId,
position: droppedGroup.position,
};
beforeDropDraggables = getSortedDraggables(
task.checklists.map(checklist => {
return { id: checklist.id, position: checklist.position };
}),
);
if (droppedDraggable === null || beforeDropDraggables === null) {
throw new Error('before drop draggables is null');
}
const afterDropDraggables = getAfterDropDraggableList(
beforeDropDraggables,
droppedDraggable,
isChecklist,
isSameChecklist,
destination,
);
const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
console.log(droppedGroup);
console.log(`positiion: ${newPosition}`);
onChecklistDrop({ ...droppedGroup, position: newPosition });
} else {
throw { error: 'task group can not be found' };
}
} else {
const targetChecklist = task.checklists.findIndex(
checklist => checklist.items.findIndex(item => item.id === draggableId) !== -1,
);
const droppedChecklistItem = task.checklists[targetChecklist].items.find(item => item.id === draggableId);
if (droppedChecklistItem) {
droppedDraggable = {
id: draggableId,
position: droppedChecklistItem.position,
};
beforeDropDraggables = getSortedDraggables(
task.checklists[targetChecklist].items.map(item => {
return { id: item.id, position: item.position };
}),
);
if (droppedDraggable === null || beforeDropDraggables === null) {
throw new Error('before drop draggables is null');
}
const afterDropDraggables = getAfterDropDraggableList(
beforeDropDraggables,
droppedDraggable,
isChecklist,
isSameChecklist,
destination,
);
const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
const newItem = {
...droppedChecklistItem,
position: newPosition,
};
onChecklistItemDrop(droppedChecklistItem.taskChecklistID, destination.droppableId, newItem);
console.log(newItem);
}
}
};
return ( return (
<> <>
<TaskActions> <TaskActions>
@ -289,33 +381,80 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
) : ( ) : (
<TaskContent description={description} onEditContent={handleClick} /> <TaskContent description={description} onEditContent={handleClick} />
)} )}
{task.checklists && <DragDropContext onDragEnd={onDragEnd}>
task.checklists <Droppable direction="vertical" type="checklist" droppableId="root">
.slice() {dropProvided => (
.sort((a, b) => a.position - b.position) <ChecklistContainer {...dropProvided.droppableProps} ref={dropProvided.innerRef}>
.map(checklist => ( {task.checklists &&
<Checklist task.checklists
key={checklist.id} .slice()
name={checklist.name} .sort((a, b) => a.position - b.position)
checklistID={checklist.id} .map((checklist, idx) => (
items={checklist.items} <Draggable key={checklist.id} draggableId={checklist.id} index={idx}>
onDeleteChecklist={onDeleteChecklist} {provided => (
onChangeName={newName => onChangeChecklistName(checklist.id, newName)} <Checklist
onToggleItem={onToggleChecklistItem} ref={provided.innerRef}
onDeleteItem={onDeleteItem} wrapperProps={provided.draggableProps}
onAddItem={n => { handleProps={provided.dragHandleProps}
if (task.checklists) { key={checklist.id}
let position = 65535; name={checklist.name}
const [lastItem] = checklist.items.sort((a, b) => a.position - b.position).slice(-1); checklistID={checklist.id}
if (lastItem) { items={checklist.items}
position = lastItem.position * 2 + 1; onDeleteChecklist={onDeleteChecklist}
} onChangeName={newName => onChangeChecklistName(checklist.id, newName)}
onAddItem(checklist.id, n, position); onToggleItem={onToggleChecklistItem}
} onDeleteItem={onDeleteItem}
}} onAddItem={n => {
onChangeItemName={onChangeItemName} if (task.checklists) {
/> let position = 65535;
))} const [lastItem] = checklist.items
.sort((a, b) => a.position - b.position)
.slice(-1);
if (lastItem) {
position = lastItem.position * 2 + 1;
}
onAddItem(checklist.id, n, position);
}
}}
onChangeItemName={onChangeItemName}
>
<Droppable direction="vertical" type="checklistItem" droppableId={checklist.id}>
{checklistDrop => (
<ChecklistItems ref={checklistDrop.innerRef} {...checklistDrop.droppableProps}>
{checklist.items
.slice()
.sort((a, b) => a.position - b.position)
.map((item, itemIdx) => (
<Draggable key={item.id} draggableId={item.id} index={itemIdx}>
{itemDrop => (
<ChecklistItem
key={item.id}
itemID={item.id}
ref={itemDrop.innerRef}
wrapperProps={itemDrop.draggableProps}
handleProps={itemDrop.dragHandleProps}
name={item.name}
complete={item.complete}
onDeleteItem={onDeleteItem}
onChangeName={onChangeItemName}
onToggleItem={() => {}}
/>
)}
</Draggable>
))}
{checklistDrop.placeholder}
</ChecklistItems>
)}
</Droppable>
</Checklist>
)}
</Draggable>
))}
{dropProvided.placeholder}
</ChecklistContainer>
)}
</Droppable>
</DragDropContext>
</TaskDetailsSection> </TaskDetailsSection>
</TaskDetailsContent> </TaskDetailsContent>
<TaskDetailsSidebar> <TaskDetailsSidebar>

View File

@ -54,6 +54,12 @@ export type ProfileIcon = {
bgColor?: Maybe<Scalars['String']>; bgColor?: Maybe<Scalars['String']>;
}; };
export type OwnersList = {
__typename?: 'OwnersList';
projects: Array<Scalars['UUID']>;
teams: Array<Scalars['UUID']>;
};
export type Member = { export type Member = {
__typename?: 'Member'; __typename?: 'Member';
id: Scalars['ID']; id: Scalars['ID'];
@ -61,6 +67,7 @@ export type Member = {
fullName: Scalars['String']; fullName: Scalars['String'];
username: Scalars['String']; username: Scalars['String'];
profileIcon: ProfileIcon; profileIcon: ProfileIcon;
owned?: Maybe<OwnersList>;
}; };
export type RefreshToken = { export type RefreshToken = {
@ -249,7 +256,9 @@ export type Mutation = {
updateProjectLabelName: ProjectLabel; updateProjectLabelName: ProjectLabel;
updateProjectMemberRole: UpdateProjectMemberRolePayload; updateProjectMemberRole: UpdateProjectMemberRolePayload;
updateProjectName: Project; updateProjectName: Project;
updateTaskChecklistItemLocation: UpdateTaskChecklistItemLocationPayload;
updateTaskChecklistItemName: TaskChecklistItem; updateTaskChecklistItemName: TaskChecklistItem;
updateTaskChecklistLocation: UpdateTaskChecklistLocationPayload;
updateTaskChecklistName: TaskChecklist; updateTaskChecklistName: TaskChecklist;
updateTaskDescription: Task; updateTaskDescription: Task;
updateTaskDueDate: Task; updateTaskDueDate: Task;
@ -258,6 +267,7 @@ export type Mutation = {
updateTaskLocation: UpdateTaskLocationPayload; updateTaskLocation: UpdateTaskLocationPayload;
updateTaskName: Task; updateTaskName: Task;
updateTeamMemberRole: UpdateTeamMemberRolePayload; updateTeamMemberRole: UpdateTeamMemberRolePayload;
updateUserRole: UpdateUserRolePayload;
}; };
@ -441,11 +451,21 @@ export type MutationUpdateProjectNameArgs = {
}; };
export type MutationUpdateTaskChecklistItemLocationArgs = {
input: UpdateTaskChecklistItemLocation;
};
export type MutationUpdateTaskChecklistItemNameArgs = { export type MutationUpdateTaskChecklistItemNameArgs = {
input: UpdateTaskChecklistItemName; input: UpdateTaskChecklistItemName;
}; };
export type MutationUpdateTaskChecklistLocationArgs = {
input: UpdateTaskChecklistLocation;
};
export type MutationUpdateTaskChecklistNameArgs = { export type MutationUpdateTaskChecklistNameArgs = {
input: UpdateTaskChecklistName; input: UpdateTaskChecklistName;
}; };
@ -485,6 +505,11 @@ export type MutationUpdateTeamMemberRoleArgs = {
input: UpdateTeamMemberRole; input: UpdateTeamMemberRole;
}; };
export type MutationUpdateUserRoleArgs = {
input: UpdateUserRole;
};
export type ProjectsFilter = { export type ProjectsFilter = {
teamID?: Maybe<Scalars['UUID']>; teamID?: Maybe<Scalars['UUID']>;
}; };
@ -656,6 +681,29 @@ export type UpdateTaskName = {
name: Scalars['String']; name: Scalars['String'];
}; };
export type UpdateTaskChecklistItemLocation = {
checklistID: Scalars['UUID'];
checklistItemID: Scalars['UUID'];
position: Scalars['Float'];
};
export type UpdateTaskChecklistItemLocationPayload = {
__typename?: 'UpdateTaskChecklistItemLocationPayload';
checklistID: Scalars['UUID'];
prevChecklistID: Scalars['UUID'];
checklistItem: TaskChecklistItem;
};
export type UpdateTaskChecklistLocation = {
checklistID: Scalars['UUID'];
position: Scalars['Float'];
};
export type UpdateTaskChecklistLocationPayload = {
__typename?: 'UpdateTaskChecklistLocationPayload';
checklist: TaskChecklist;
};
export type CreateTaskChecklist = { export type CreateTaskChecklist = {
taskID: Scalars['UUID']; taskID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
@ -769,12 +817,14 @@ export type DeleteTeamPayload = {
export type DeleteTeamMember = { export type DeleteTeamMember = {
teamID: Scalars['UUID']; teamID: Scalars['UUID'];
userID: Scalars['UUID']; userID: Scalars['UUID'];
newOwnerID?: Maybe<Scalars['UUID']>;
}; };
export type DeleteTeamMemberPayload = { export type DeleteTeamMemberPayload = {
__typename?: 'DeleteTeamMemberPayload'; __typename?: 'DeleteTeamMemberPayload';
teamID: Scalars['UUID']; teamID: Scalars['UUID'];
userID: Scalars['UUID']; userID: Scalars['UUID'];
affectedProjects: Array<Project>;
}; };
export type CreateTeamMember = { export type CreateTeamMember = {
@ -812,6 +862,16 @@ export type SetTeamOwnerPayload = {
newOwner: Member; newOwner: Member;
}; };
export type UpdateUserRole = {
userID: Scalars['UUID'];
roleCode: RoleCode;
};
export type UpdateUserRolePayload = {
__typename?: 'UpdateUserRolePayload';
user: UserAccount;
};
export type NewRefreshToken = { export type NewRefreshToken = {
userId: Scalars['String']; userId: Scalars['String'];
}; };
@ -1363,6 +1423,25 @@ export type SetTaskCompleteMutation = (
) } ) }
); );
export type UpdateTaskChecklistItemLocationMutationVariables = {
checklistID: Scalars['UUID'];
checklistItemID: Scalars['UUID'];
position: Scalars['Float'];
};
export type UpdateTaskChecklistItemLocationMutation = (
{ __typename?: 'Mutation' }
& { updateTaskChecklistItemLocation: (
{ __typename?: 'UpdateTaskChecklistItemLocationPayload' }
& Pick<UpdateTaskChecklistItemLocationPayload, 'checklistID' | 'prevChecklistID'>
& { checklistItem: (
{ __typename?: 'TaskChecklistItem' }
& Pick<TaskChecklistItem, 'id' | 'taskChecklistID' | 'position'>
) }
) }
);
export type UpdateTaskChecklistItemNameMutationVariables = { export type UpdateTaskChecklistItemNameMutationVariables = {
taskChecklistItemID: Scalars['UUID']; taskChecklistItemID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
@ -1377,6 +1456,23 @@ export type UpdateTaskChecklistItemNameMutation = (
) } ) }
); );
export type UpdateTaskChecklistLocationMutationVariables = {
checklistID: Scalars['UUID'];
position: Scalars['Float'];
};
export type UpdateTaskChecklistLocationMutation = (
{ __typename?: 'Mutation' }
& { updateTaskChecklistLocation: (
{ __typename?: 'UpdateTaskChecklistLocationPayload' }
& { checklist: (
{ __typename?: 'TaskChecklist' }
& Pick<TaskChecklist, 'id' | 'position'>
) }
) }
);
export type UpdateTaskChecklistNameMutationVariables = { export type UpdateTaskChecklistNameMutationVariables = {
taskChecklistID: Scalars['UUID']; taskChecklistID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
@ -1470,6 +1566,7 @@ export type DeleteTeamMutation = (
export type DeleteTeamMemberMutationVariables = { export type DeleteTeamMemberMutationVariables = {
teamID: Scalars['UUID']; teamID: Scalars['UUID'];
userID: Scalars['UUID']; userID: Scalars['UUID'];
newOwnerID?: Maybe<Scalars['UUID']>;
}; };
@ -1497,7 +1594,10 @@ export type GetTeamQuery = (
& { role: ( & { role: (
{ __typename?: 'Role' } { __typename?: 'Role' }
& Pick<Role, 'code' | 'name'> & Pick<Role, 'code' | 'name'>
), profileIcon: ( ), owned?: Maybe<(
{ __typename?: 'OwnersList' }
& Pick<OwnersList, 'projects' | 'teams'>
)>, profileIcon: (
{ __typename?: 'ProfileIcon' } { __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'> & Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) } ) }
@ -1724,6 +1824,27 @@ export type DeleteUserAccountMutation = (
) } ) }
); );
export type UpdateUserRoleMutationVariables = {
userID: Scalars['UUID'];
roleCode: RoleCode;
};
export type UpdateUserRoleMutation = (
{ __typename?: 'Mutation' }
& { updateUserRole: (
{ __typename?: 'UpdateUserRolePayload' }
& { user: (
{ __typename?: 'UserAccount' }
& Pick<UserAccount, 'id'>
& { role: (
{ __typename?: 'Role' }
& Pick<Role, 'code' | 'name'>
) }
) }
) }
);
export type UsersQueryVariables = {}; export type UsersQueryVariables = {};
@ -2796,6 +2917,46 @@ export function useSetTaskCompleteMutation(baseOptions?: ApolloReactHooks.Mutati
export type SetTaskCompleteMutationHookResult = ReturnType<typeof useSetTaskCompleteMutation>; export type SetTaskCompleteMutationHookResult = ReturnType<typeof useSetTaskCompleteMutation>;
export type SetTaskCompleteMutationResult = ApolloReactCommon.MutationResult<SetTaskCompleteMutation>; export type SetTaskCompleteMutationResult = ApolloReactCommon.MutationResult<SetTaskCompleteMutation>;
export type SetTaskCompleteMutationOptions = ApolloReactCommon.BaseMutationOptions<SetTaskCompleteMutation, SetTaskCompleteMutationVariables>; export type SetTaskCompleteMutationOptions = ApolloReactCommon.BaseMutationOptions<SetTaskCompleteMutation, SetTaskCompleteMutationVariables>;
export const UpdateTaskChecklistItemLocationDocument = gql`
mutation updateTaskChecklistItemLocation($checklistID: UUID!, $checklistItemID: UUID!, $position: Float!) {
updateTaskChecklistItemLocation(input: {checklistID: $checklistID, checklistItemID: $checklistItemID, position: $position}) {
checklistID
prevChecklistID
checklistItem {
id
taskChecklistID
position
}
}
}
`;
export type UpdateTaskChecklistItemLocationMutationFn = ApolloReactCommon.MutationFunction<UpdateTaskChecklistItemLocationMutation, UpdateTaskChecklistItemLocationMutationVariables>;
/**
* __useUpdateTaskChecklistItemLocationMutation__
*
* To run a mutation, you first call `useUpdateTaskChecklistItemLocationMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateTaskChecklistItemLocationMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateTaskChecklistItemLocationMutation, { data, loading, error }] = useUpdateTaskChecklistItemLocationMutation({
* variables: {
* checklistID: // value for 'checklistID'
* checklistItemID: // value for 'checklistItemID'
* position: // value for 'position'
* },
* });
*/
export function useUpdateTaskChecklistItemLocationMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTaskChecklistItemLocationMutation, UpdateTaskChecklistItemLocationMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateTaskChecklistItemLocationMutation, UpdateTaskChecklistItemLocationMutationVariables>(UpdateTaskChecklistItemLocationDocument, baseOptions);
}
export type UpdateTaskChecklistItemLocationMutationHookResult = ReturnType<typeof useUpdateTaskChecklistItemLocationMutation>;
export type UpdateTaskChecklistItemLocationMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistItemLocationMutation>;
export type UpdateTaskChecklistItemLocationMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskChecklistItemLocationMutation, UpdateTaskChecklistItemLocationMutationVariables>;
export const UpdateTaskChecklistItemNameDocument = gql` export const UpdateTaskChecklistItemNameDocument = gql`
mutation updateTaskChecklistItemName($taskChecklistItemID: UUID!, $name: String!) { mutation updateTaskChecklistItemName($taskChecklistItemID: UUID!, $name: String!) {
updateTaskChecklistItemName(input: {taskChecklistItemID: $taskChecklistItemID, name: $name}) { updateTaskChecklistItemName(input: {taskChecklistItemID: $taskChecklistItemID, name: $name}) {
@ -2830,6 +2991,42 @@ export function useUpdateTaskChecklistItemNameMutation(baseOptions?: ApolloReact
export type UpdateTaskChecklistItemNameMutationHookResult = ReturnType<typeof useUpdateTaskChecklistItemNameMutation>; export type UpdateTaskChecklistItemNameMutationHookResult = ReturnType<typeof useUpdateTaskChecklistItemNameMutation>;
export type UpdateTaskChecklistItemNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistItemNameMutation>; export type UpdateTaskChecklistItemNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistItemNameMutation>;
export type UpdateTaskChecklistItemNameMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskChecklistItemNameMutation, UpdateTaskChecklistItemNameMutationVariables>; export type UpdateTaskChecklistItemNameMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskChecklistItemNameMutation, UpdateTaskChecklistItemNameMutationVariables>;
export const UpdateTaskChecklistLocationDocument = gql`
mutation updateTaskChecklistLocation($checklistID: UUID!, $position: Float!) {
updateTaskChecklistLocation(input: {checklistID: $checklistID, position: $position}) {
checklist {
id
position
}
}
}
`;
export type UpdateTaskChecklistLocationMutationFn = ApolloReactCommon.MutationFunction<UpdateTaskChecklistLocationMutation, UpdateTaskChecklistLocationMutationVariables>;
/**
* __useUpdateTaskChecklistLocationMutation__
*
* To run a mutation, you first call `useUpdateTaskChecklistLocationMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateTaskChecklistLocationMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateTaskChecklistLocationMutation, { data, loading, error }] = useUpdateTaskChecklistLocationMutation({
* variables: {
* checklistID: // value for 'checklistID'
* position: // value for 'position'
* },
* });
*/
export function useUpdateTaskChecklistLocationMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTaskChecklistLocationMutation, UpdateTaskChecklistLocationMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateTaskChecklistLocationMutation, UpdateTaskChecklistLocationMutationVariables>(UpdateTaskChecklistLocationDocument, baseOptions);
}
export type UpdateTaskChecklistLocationMutationHookResult = ReturnType<typeof useUpdateTaskChecklistLocationMutation>;
export type UpdateTaskChecklistLocationMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistLocationMutation>;
export type UpdateTaskChecklistLocationMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskChecklistLocationMutation, UpdateTaskChecklistLocationMutationVariables>;
export const UpdateTaskChecklistNameDocument = gql` export const UpdateTaskChecklistNameDocument = gql`
mutation updateTaskChecklistName($taskChecklistID: UUID!, $name: String!) { mutation updateTaskChecklistName($taskChecklistID: UUID!, $name: String!) {
updateTaskChecklistName(input: {taskChecklistID: $taskChecklistID, name: $name}) { updateTaskChecklistName(input: {taskChecklistID: $taskChecklistID, name: $name}) {
@ -3026,8 +3223,8 @@ export type DeleteTeamMutationHookResult = ReturnType<typeof useDeleteTeamMutati
export type DeleteTeamMutationResult = ApolloReactCommon.MutationResult<DeleteTeamMutation>; export type DeleteTeamMutationResult = ApolloReactCommon.MutationResult<DeleteTeamMutation>;
export type DeleteTeamMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTeamMutation, DeleteTeamMutationVariables>; export type DeleteTeamMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTeamMutation, DeleteTeamMutationVariables>;
export const DeleteTeamMemberDocument = gql` export const DeleteTeamMemberDocument = gql`
mutation deleteTeamMember($teamID: UUID!, $userID: UUID!) { mutation deleteTeamMember($teamID: UUID!, $userID: UUID!, $newOwnerID: UUID) {
deleteTeamMember(input: {teamID: $teamID, userID: $userID}) { deleteTeamMember(input: {teamID: $teamID, userID: $userID, newOwnerID: $newOwnerID}) {
teamID teamID
userID userID
} }
@ -3050,6 +3247,7 @@ export type DeleteTeamMemberMutationFn = ApolloReactCommon.MutationFunction<Dele
* variables: { * variables: {
* teamID: // value for 'teamID' * teamID: // value for 'teamID'
* userID: // value for 'userID' * userID: // value for 'userID'
* newOwnerID: // value for 'newOwnerID'
* }, * },
* }); * });
*/ */
@ -3073,6 +3271,10 @@ export const GetTeamDocument = gql`
code code
name name
} }
owned {
projects
teams
}
profileIcon { profileIcon {
url url
initials initials
@ -3560,6 +3762,45 @@ export function useDeleteUserAccountMutation(baseOptions?: ApolloReactHooks.Muta
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>; export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
export type DeleteUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteUserAccountMutation>; export type DeleteUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteUserAccountMutation>;
export type DeleteUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>; export type DeleteUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
export const UpdateUserRoleDocument = gql`
mutation updateUserRole($userID: UUID!, $roleCode: RoleCode!) {
updateUserRole(input: {userID: $userID, roleCode: $roleCode}) {
user {
id
role {
code
name
}
}
}
}
`;
export type UpdateUserRoleMutationFn = ApolloReactCommon.MutationFunction<UpdateUserRoleMutation, UpdateUserRoleMutationVariables>;
/**
* __useUpdateUserRoleMutation__
*
* To run a mutation, you first call `useUpdateUserRoleMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateUserRoleMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateUserRoleMutation, { data, loading, error }] = useUpdateUserRoleMutation({
* variables: {
* userID: // value for 'userID'
* roleCode: // value for 'roleCode'
* },
* });
*/
export function useUpdateUserRoleMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateUserRoleMutation, UpdateUserRoleMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateUserRoleMutation, UpdateUserRoleMutationVariables>(UpdateUserRoleDocument, baseOptions);
}
export type UpdateUserRoleMutationHookResult = ReturnType<typeof useUpdateUserRoleMutation>;
export type UpdateUserRoleMutationResult = ApolloReactCommon.MutationResult<UpdateUserRoleMutation>;
export type UpdateUserRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateUserRoleMutation, UpdateUserRoleMutationVariables>;
export const UsersDocument = gql` export const UsersDocument = gql`
query users { query users {
users { users {

View File

@ -0,0 +1,19 @@
import gql from 'graphql-tag';
const UPDATE_TASK_CHECKLIST_ITEM_LOCATION_MUTATION = gql`
mutation updateTaskChecklistItemLocation($checklistID: UUID!, $checklistItemID: UUID!, $position: Float!) {
updateTaskChecklistItemLocation(
input: { checklistID: $checklistID, checklistItemID: $checklistItemID, position: $position }
) {
checklistID
prevChecklistID
checklistItem {
id
taskChecklistID
position
}
}
}
`;
export default UPDATE_TASK_CHECKLIST_ITEM_LOCATION_MUTATION;

View File

@ -0,0 +1,14 @@
import gql from 'graphql-tag';
const UPDATE_TASK_CHECKLIST_LOCATION_MUTATION = gql`
mutation updateTaskChecklistLocation($checklistID: UUID!, $position: Float!) {
updateTaskChecklistLocation(input: { checklistID: $checklistID, position: $position }) {
checklist {
id
position
}
}
}
`;
export default UPDATE_TASK_CHECKLIST_LOCATION_MUTATION;

View File

@ -1,8 +1,8 @@
import gql from 'graphql-tag'; import gql from 'graphql-tag';
export const DELETE_TEAM_MEMBER_MUTATION = gql` export const DELETE_TEAM_MEMBER_MUTATION = gql`
mutation deleteTeamMember($teamID: UUID!, $userID: UUID!) { mutation deleteTeamMember($teamID: UUID!, $userID: UUID!, $newOwnerID: UUID) {
deleteTeamMember(input: { teamID: $teamID, userID: $userID }) { deleteTeamMember(input: { teamID: $teamID, userID: $userID, newOwnerID: $newOwnerID }) {
teamID teamID
userID userID
} }

View File

@ -14,6 +14,10 @@ export const GET_TEAM_QUERY = gql`
code code
name name
} }
owned {
projects
teams
}
profileIcon { profileIcon {
url url
initials initials

View File

@ -0,0 +1,17 @@
import gql from 'graphql-tag';
export const UPDATE_USER_ROLE_MUTATION = gql`
mutation updateUserRole($userID: UUID!, $roleCode: RoleCode!) {
updateUserRole(input:{userID: $userID, roleCode:$roleCode}) {
user {
id
role {
code
name
}
}
}
}
`;
export default UPDATE_USER_ROLE_MUTATION;

View File

@ -20,7 +20,7 @@ model:
# Where should the resolver implementations go? # Where should the resolver implementations go?
resolver: resolver:
layout: follow-schema layout: follow-schema
dir: graph dir: internal/graph
package: graph package: graph
# Optional: turn on to use []Thing instead of []*Thing # Optional: turn on to use []Thing instead of []*Thing

View File

@ -158,6 +158,38 @@ func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) (
return items, nil return items, nil
} }
const getOwnedTeamProjectsForUserID = `-- name: GetOwnedTeamProjectsForUserID :many
SELECT project_id FROM project WHERE owner = $1 AND team_id = $2
`
type GetOwnedTeamProjectsForUserIDParams struct {
Owner uuid.UUID `json:"owner"`
TeamID uuid.UUID `json:"team_id"`
}
func (q *Queries) GetOwnedTeamProjectsForUserID(ctx context.Context, arg GetOwnedTeamProjectsForUserIDParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, getOwnedTeamProjectsForUserID, arg.Owner, arg.TeamID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var project_id uuid.UUID
if err := rows.Scan(&project_id); err != nil {
return nil, err
}
items = append(items, project_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProjectByID = `-- name: GetProjectByID :one const getProjectByID = `-- name: GetProjectByID :one
SELECT project_id, team_id, created_at, name, owner FROM project WHERE project_id = $1 SELECT project_id, team_id, created_at, name, owner FROM project WHERE project_id = $1
` `

View File

@ -51,6 +51,7 @@ type Querier interface {
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error) GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error) GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
GetLabelColors(ctx context.Context) ([]LabelColor, error) GetLabelColors(ctx context.Context) ([]LabelColor, error)
GetOwnedTeamProjectsForUserID(ctx context.Context, arg GetOwnedTeamProjectsForUserIDParams) ([]uuid.UUID, error)
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error) GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error)
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error) GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
@ -87,8 +88,10 @@ type Querier interface {
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error) UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)
UpdateProjectMemberRole(ctx context.Context, arg UpdateProjectMemberRoleParams) (ProjectMember, error) UpdateProjectMemberRole(ctx context.Context, arg UpdateProjectMemberRoleParams) (ProjectMember, error)
UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, error) UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, error)
UpdateTaskChecklistItemLocation(ctx context.Context, arg UpdateTaskChecklistItemLocationParams) (TaskChecklistItem, error)
UpdateTaskChecklistItemName(ctx context.Context, arg UpdateTaskChecklistItemNameParams) (TaskChecklistItem, error) UpdateTaskChecklistItemName(ctx context.Context, arg UpdateTaskChecklistItemNameParams) (TaskChecklistItem, error)
UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (TaskChecklist, error) UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (TaskChecklist, error)
UpdateTaskChecklistPosition(ctx context.Context, arg UpdateTaskChecklistPositionParams) (TaskChecklist, error)
UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescriptionParams) (Task, error) UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescriptionParams) (Task, error)
UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDateParams) (Task, error) UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDateParams) (Task, error)
UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error) UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
@ -96,6 +99,7 @@ type Querier interface {
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error) UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error)
UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
} }
var _ Querier = (*Queries)(nil) var _ Querier = (*Queries)(nil)

View File

@ -37,5 +37,5 @@ DELETE FROM project_member WHERE user_id = $1 AND project_id = $2;
UPDATE project_member SET role_code = $3 WHERE project_id = $1 AND user_id = $2 UPDATE project_member SET role_code = $3 WHERE project_id = $1 AND user_id = $2
RETURNING *; RETURNING *;
-- name: GetOwnedTeamProjectsForUserID :many
SELECT project_id FROM project WHERE owner = $1 AND team_id = $2;

View File

@ -35,3 +35,9 @@ SELECT * FROM task_checklist_item WHERE task_checklist_item_id = $1;
-- name: UpdateTaskChecklistItemName :one -- name: UpdateTaskChecklistItemName :one
UPDATE task_checklist_item SET name = $2 WHERE task_checklist_item_id = $1 UPDATE task_checklist_item SET name = $2 WHERE task_checklist_item_id = $1
RETURNING *; RETURNING *;
-- name: UpdateTaskChecklistPosition :one
UPDATE task_checklist SET position = $2 WHERE task_checklist_id = $1 RETURNING *;
-- name: UpdateTaskChecklistItemLocation :one
UPDATE task_checklist_item SET position = $2, task_checklist_id = $3 WHERE task_checklist_item_id = $1 RETURNING *;

View File

@ -22,3 +22,6 @@ DELETE FROM user_account WHERE user_id = $1;
SELECT username, role.code, role.name FROM user_account SELECT username, role.code, role.name FROM user_account
INNER JOIN role ON role.code = user_account.role_code INNER JOIN role ON role.code = user_account.role_code
WHERE user_id = $1; WHERE user_id = $1;
-- name: UpdateUserRole :one
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING *;

View File

@ -219,6 +219,31 @@ func (q *Queries) SetTaskChecklistItemComplete(ctx context.Context, arg SetTaskC
return i, err return i, err
} }
const updateTaskChecklistItemLocation = `-- name: UpdateTaskChecklistItemLocation :one
UPDATE task_checklist_item SET position = $2, task_checklist_id = $3 WHERE task_checklist_item_id = $1 RETURNING task_checklist_item_id, task_checklist_id, created_at, complete, name, position, due_date
`
type UpdateTaskChecklistItemLocationParams struct {
TaskChecklistItemID uuid.UUID `json:"task_checklist_item_id"`
Position float64 `json:"position"`
TaskChecklistID uuid.UUID `json:"task_checklist_id"`
}
func (q *Queries) UpdateTaskChecklistItemLocation(ctx context.Context, arg UpdateTaskChecklistItemLocationParams) (TaskChecklistItem, error) {
row := q.db.QueryRowContext(ctx, updateTaskChecklistItemLocation, arg.TaskChecklistItemID, arg.Position, arg.TaskChecklistID)
var i TaskChecklistItem
err := row.Scan(
&i.TaskChecklistItemID,
&i.TaskChecklistID,
&i.CreatedAt,
&i.Complete,
&i.Name,
&i.Position,
&i.DueDate,
)
return i, err
}
const updateTaskChecklistItemName = `-- name: UpdateTaskChecklistItemName :one const updateTaskChecklistItemName = `-- name: UpdateTaskChecklistItemName :one
UPDATE task_checklist_item SET name = $2 WHERE task_checklist_item_id = $1 UPDATE task_checklist_item SET name = $2 WHERE task_checklist_item_id = $1
RETURNING task_checklist_item_id, task_checklist_id, created_at, complete, name, position, due_date RETURNING task_checklist_item_id, task_checklist_id, created_at, complete, name, position, due_date
@ -266,3 +291,25 @@ func (q *Queries) UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChe
) )
return i, err return i, err
} }
const updateTaskChecklistPosition = `-- name: UpdateTaskChecklistPosition :one
UPDATE task_checklist SET position = $2 WHERE task_checklist_id = $1 RETURNING task_checklist_id, task_id, created_at, name, position
`
type UpdateTaskChecklistPositionParams struct {
TaskChecklistID uuid.UUID `json:"task_checklist_id"`
Position float64 `json:"position"`
}
func (q *Queries) UpdateTaskChecklistPosition(ctx context.Context, arg UpdateTaskChecklistPositionParams) (TaskChecklist, error) {
row := q.db.QueryRowContext(ctx, updateTaskChecklistPosition, arg.TaskChecklistID, arg.Position)
var i TaskChecklist
err := row.Scan(
&i.TaskChecklistID,
&i.TaskID,
&i.CreatedAt,
&i.Name,
&i.Position,
)
return i, err
}

View File

@ -189,3 +189,30 @@ func (q *Queries) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg Upd
) )
return i, err return i, err
} }
const updateUserRole = `-- name: UpdateUserRole :one
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
`
type UpdateUserRoleParams struct {
UserID uuid.UUID `json:"user_id"`
RoleCode string `json:"role_code"`
}
func (q *Queries) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error) {
row := q.db.QueryRowContext(ctx, updateUserRole, arg.UserID, arg.RoleCode)
var i UserAccount
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.Email,
&i.Username,
&i.PasswordHash,
&i.ProfileBgColor,
&i.FullName,
&i.Initials,
&i.ProfileAvatarUrl,
&i.RoleCode,
)
return i, err
}

File diff suppressed because it is too large Load Diff

View File

@ -124,13 +124,15 @@ type DeleteTeam struct {
} }
type DeleteTeamMember struct { type DeleteTeamMember struct {
TeamID uuid.UUID `json:"teamID"` TeamID uuid.UUID `json:"teamID"`
UserID uuid.UUID `json:"userID"` UserID uuid.UUID `json:"userID"`
NewOwnerID *uuid.UUID `json:"newOwnerID"`
} }
type DeleteTeamMemberPayload struct { type DeleteTeamMemberPayload struct {
TeamID uuid.UUID `json:"teamID"` TeamID uuid.UUID `json:"teamID"`
UserID uuid.UUID `json:"userID"` UserID uuid.UUID `json:"userID"`
AffectedProjects []db.Project `json:"affectedProjects"`
} }
type DeleteTeamPayload struct { type DeleteTeamPayload struct {
@ -174,6 +176,7 @@ type Member struct {
FullName string `json:"fullName"` FullName string `json:"fullName"`
Username string `json:"username"` Username string `json:"username"`
ProfileIcon *ProfileIcon `json:"profileIcon"` ProfileIcon *ProfileIcon `json:"profileIcon"`
Owned *OwnersList `json:"owned"`
} }
type NewProject struct { type NewProject struct {
@ -229,6 +232,11 @@ type NewUserAccount struct {
RoleCode string `json:"roleCode"` RoleCode string `json:"roleCode"`
} }
type OwnersList struct {
Projects []uuid.UUID `json:"projects"`
Teams []uuid.UUID `json:"teams"`
}
type ProfileIcon struct { type ProfileIcon struct {
URL *string `json:"url"` URL *string `json:"url"`
Initials *string `json:"initials"` Initials *string `json:"initials"`
@ -326,11 +334,32 @@ type UpdateProjectName struct {
Name string `json:"name"` Name string `json:"name"`
} }
type UpdateTaskChecklistItemLocation struct {
ChecklistID uuid.UUID `json:"checklistID"`
ChecklistItemID uuid.UUID `json:"checklistItemID"`
Position float64 `json:"position"`
}
type UpdateTaskChecklistItemLocationPayload struct {
ChecklistID uuid.UUID `json:"checklistID"`
PrevChecklistID uuid.UUID `json:"prevChecklistID"`
ChecklistItem *db.TaskChecklistItem `json:"checklistItem"`
}
type UpdateTaskChecklistItemName struct { type UpdateTaskChecklistItemName struct {
TaskChecklistItemID uuid.UUID `json:"taskChecklistItemID"` TaskChecklistItemID uuid.UUID `json:"taskChecklistItemID"`
Name string `json:"name"` Name string `json:"name"`
} }
type UpdateTaskChecklistLocation struct {
ChecklistID uuid.UUID `json:"checklistID"`
Position float64 `json:"position"`
}
type UpdateTaskChecklistLocationPayload struct {
Checklist *db.TaskChecklist `json:"checklist"`
}
type UpdateTaskChecklistName struct { type UpdateTaskChecklistName struct {
TaskChecklistID uuid.UUID `json:"taskChecklistID"` TaskChecklistID uuid.UUID `json:"taskChecklistID"`
Name string `json:"name"` Name string `json:"name"`
@ -372,6 +401,15 @@ type UpdateTeamMemberRolePayload struct {
Member *Member `json:"member"` Member *Member `json:"member"`
} }
type UpdateUserRole struct {
UserID uuid.UUID `json:"userID"`
RoleCode RoleCode `json:"roleCode"`
}
type UpdateUserRolePayload struct {
User *db.UserAccount `json:"user"`
}
type RoleCode string type RoleCode string
const ( const (

View File

@ -35,12 +35,18 @@ type ProfileIcon {
bgColor: String bgColor: String
} }
type OwnersList {
projects: [UUID!]!
teams: [UUID!]!
}
type Member { type Member {
id: ID! id: ID!
role: Role! role: Role!
fullName: String! fullName: String!
username: String! username: String!
profileIcon: ProfileIcon! profileIcon: ProfileIcon!
owned: OwnersList
} }
type RefreshToken { type RefreshToken {
@ -361,6 +367,30 @@ extend type Mutation {
updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): TaskChecklistItem! updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): TaskChecklistItem!
setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): TaskChecklistItem! setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): TaskChecklistItem!
deleteTaskChecklistItem(input: DeleteTaskChecklistItem!): DeleteTaskChecklistItemPayload! deleteTaskChecklistItem(input: DeleteTaskChecklistItem!): DeleteTaskChecklistItemPayload!
updateTaskChecklistLocation(input: UpdateTaskChecklistLocation!): UpdateTaskChecklistLocationPayload!
updateTaskChecklistItemLocation(input: UpdateTaskChecklistItemLocation!): UpdateTaskChecklistItemLocationPayload!
}
input UpdateTaskChecklistItemLocation {
checklistID: UUID!
checklistItemID: UUID!
position: Float!
}
type UpdateTaskChecklistItemLocationPayload {
checklistID: UUID!
prevChecklistID: UUID!
checklistItem: TaskChecklistItem!
}
input UpdateTaskChecklistLocation {
checklistID: UUID!
position: Float!
}
type UpdateTaskChecklistLocationPayload {
checklist: TaskChecklist!
} }
input CreateTaskChecklist { input CreateTaskChecklist {
@ -492,11 +522,13 @@ extend type Mutation {
input DeleteTeamMember { input DeleteTeamMember {
teamID: UUID! teamID: UUID!
userID: UUID! userID: UUID!
newOwnerID: UUID
} }
type DeleteTeamMemberPayload { type DeleteTeamMemberPayload {
teamID: UUID! teamID: UUID!
userID: UUID! userID: UUID!
affectedProjects: [Project!]!
} }
input CreateTeamMember { input CreateTeamMember {
@ -537,6 +569,17 @@ extend type Mutation {
deleteUserAccount(input: DeleteUserAccount!): DeleteUserAccountPayload! deleteUserAccount(input: DeleteUserAccount!): DeleteUserAccountPayload!
logoutUser(input: LogoutUser!): Boolean! logoutUser(input: LogoutUser!): Boolean!
clearProfileAvatar: UserAccount! clearProfileAvatar: UserAccount!
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
}
input UpdateUserRole {
userID: UUID!
roleCode: RoleCode!
}
type UpdateUserRolePayload {
user: UserAccount!
} }
input NewRefreshToken { input NewRefreshToken {

View File

@ -426,6 +426,26 @@ func (r *mutationResolver) DeleteTaskChecklistItem(ctx context.Context, input De
}, err }, err
} }
func (r *mutationResolver) UpdateTaskChecklistLocation(ctx context.Context, input UpdateTaskChecklistLocation) (*UpdateTaskChecklistLocationPayload, error) {
checklist, err := r.Repository.UpdateTaskChecklistPosition(ctx, db.UpdateTaskChecklistPositionParams{Position: input.Position, TaskChecklistID: input.ChecklistID})
if err != nil {
return &UpdateTaskChecklistLocationPayload{}, err
}
return &UpdateTaskChecklistLocationPayload{Checklist: &checklist}, nil
}
func (r *mutationResolver) UpdateTaskChecklistItemLocation(ctx context.Context, input UpdateTaskChecklistItemLocation) (*UpdateTaskChecklistItemLocationPayload, error) {
currentChecklistItem, err := r.Repository.GetTaskChecklistItemByID(ctx, input.ChecklistItemID)
checklistItem, err := r.Repository.UpdateTaskChecklistItemLocation(ctx, db.UpdateTaskChecklistItemLocationParams{TaskChecklistID: input.ChecklistID, TaskChecklistItemID: input.ChecklistItemID, Position: input.Position})
if err != nil {
return &UpdateTaskChecklistItemLocationPayload{}, err
}
return &UpdateTaskChecklistItemLocationPayload{PrevChecklistID: currentChecklistItem.TaskChecklistID, ChecklistID: input.ChecklistID, ChecklistItem: &checklistItem}, err
}
func (r *mutationResolver) CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*db.TaskGroup, error) { func (r *mutationResolver) CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*db.TaskGroup, error) {
createdAt := time.Now().UTC() createdAt := time.Now().UTC()
projectID, err := uuid.Parse(input.ProjectID) projectID, err := uuid.Parse(input.ProjectID)
@ -682,7 +702,11 @@ func (r *mutationResolver) UpdateTeamMemberRole(ctx context.Context, input Updat
} }
func (r *mutationResolver) DeleteTeamMember(ctx context.Context, input DeleteTeamMember) (*DeleteTeamMemberPayload, error) { func (r *mutationResolver) DeleteTeamMember(ctx context.Context, input DeleteTeamMember) (*DeleteTeamMemberPayload, error) {
_, err := r.Repository.GetTeamMemberByID(ctx, db.GetTeamMemberByIDParams{TeamID: input.TeamID, UserID: input.UserID}) ownedProjects, err := r.Repository.GetOwnedTeamProjectsForUserID(ctx, db.GetOwnedTeamProjectsForUserIDParams{TeamID: input.TeamID, Owner: input.UserID})
if err != nil {
return &DeleteTeamMemberPayload{}, err
}
_, err = r.Repository.GetTeamMemberByID(ctx, db.GetTeamMemberByIDParams{TeamID: input.TeamID, UserID: input.UserID})
if err != nil { if err != nil {
return &DeleteTeamMemberPayload{}, err return &DeleteTeamMemberPayload{}, err
} }
@ -690,6 +714,11 @@ func (r *mutationResolver) DeleteTeamMember(ctx context.Context, input DeleteTea
if err != nil { if err != nil {
return &DeleteTeamMemberPayload{}, err return &DeleteTeamMemberPayload{}, err
} }
if input.NewOwnerID != nil {
for _, projectID := range ownedProjects {
_, err = r.Repository.SetProjectOwner(ctx, db.SetProjectOwnerParams{ProjectID: projectID, Owner: *input.NewOwnerID})
}
}
return &DeleteTeamMemberPayload{TeamID: input.TeamID, UserID: input.UserID}, nil return &DeleteTeamMemberPayload{TeamID: input.TeamID, UserID: input.UserID}, nil
} }
@ -754,6 +783,15 @@ func (r *mutationResolver) ClearProfileAvatar(ctx context.Context) (*db.UserAcco
return &user, nil return &user, nil
} }
func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) {
user, err := r.Repository.UpdateUserRole(ctx, db.UpdateUserRoleParams{RoleCode: input.RoleCode.String(), UserID: input.UserID})
if err != nil {
return &UpdateUserRolePayload{}, err
}
return &UpdateUserRolePayload{User: &user}, nil
}
func (r *organizationResolver) ID(ctx context.Context, obj *db.Organization) (uuid.UUID, error) { func (r *organizationResolver) ID(ctx context.Context, obj *db.Organization) (uuid.UUID, error) {
return obj.OrganizationID, nil return obj.OrganizationID, nil
} }
@ -1095,6 +1133,7 @@ func (r *teamResolver) ID(ctx context.Context, obj *db.Team) (uuid.UUID, error)
func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, error) { func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, error) {
user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner) user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
members := []Member{} members := []Member{}
log.WithFields(log.Fields{"teamID": obj.TeamID}).Info("getting members")
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return members, nil return members, nil
} }
@ -1102,6 +1141,20 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
log.WithError(err).Error("get user account by ID") log.WithError(err).Error("get user account by ID")
return members, err return members, err
} }
ownedProjects, err := r.Repository.GetOwnedTeamProjectsForUserID(ctx, db.GetOwnedTeamProjectsForUserIDParams{TeamID: obj.TeamID, Owner: user.UserID})
log.WithFields(log.Fields{"projects": ownedProjects}).Info("retrieved owned project list")
if err == sql.ErrNoRows {
ownedProjects = []uuid.UUID{}
} else if err != nil {
log.WithError(err).Error("get owned team projects for user id")
return members, err
}
ownedTeams := []uuid.UUID{}
var ownerList *OwnersList
if len(ownedTeams) != 0 || len(ownedProjects) != 0 {
log.Info("owned list is not empty")
ownerList = &OwnersList{Projects: ownedProjects, Teams: ownedTeams}
}
var url *string var url *string
if user.ProfileAvatarUrl.Valid { if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String url = &user.ProfileAvatarUrl.String
@ -1109,7 +1162,7 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor} profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
members = append(members, Member{ members = append(members, Member{
ID: obj.Owner, FullName: user.FullName, ProfileIcon: profileIcon, Username: user.Username, ID: obj.Owner, FullName: user.FullName, ProfileIcon: profileIcon, Username: user.Username,
Role: &db.Role{Code: "owner", Name: "Owner"}, Owned: ownerList, Role: &db.Role{Code: "owner", Name: "Owner"},
}) })
teamMembers, err := r.Repository.GetTeamMembersForTeamID(ctx, obj.TeamID) teamMembers, err := r.Repository.GetTeamMembersForTeamID(ctx, obj.TeamID)
if err != nil { if err != nil {
@ -1132,9 +1185,24 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
log.WithError(err).Error("get role for projet member by user ID") log.WithError(err).Error("get role for projet member by user ID")
return members, err return members, err
} }
ownedProjects, err := r.Repository.GetOwnedTeamProjectsForUserID(ctx, db.GetOwnedTeamProjectsForUserIDParams{TeamID: obj.TeamID, Owner: user.UserID})
log.WithFields(log.Fields{"projects": ownedProjects}).Info("retrieved owned project list")
if err == sql.ErrNoRows {
ownedProjects = []uuid.UUID{}
} else if err != nil {
log.WithError(err).Error("get owned team projects for user id")
return members, err
}
ownedTeams := []uuid.UUID{}
var ownerList *OwnersList
if len(ownedTeams) != 0 || len(ownedProjects) != 0 {
log.Info("owned list is not empty")
ownerList = &OwnersList{Projects: ownedProjects, Teams: ownedTeams}
}
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor} profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
members = append(members, Member{ID: user.UserID, FullName: user.FullName, ProfileIcon: profileIcon, members = append(members, Member{ID: user.UserID, FullName: user.FullName, ProfileIcon: profileIcon,
Username: user.Username, Role: &db.Role{Code: role.Code, Name: role.Name}, Username: user.Username, Owned: ownerList, Role: &db.Role{Code: role.Code, Name: role.Name},
}) })
} }
return members, nil return members, nil

View File

@ -35,12 +35,18 @@ type ProfileIcon {
bgColor: String bgColor: String
} }
type OwnersList {
projects: [UUID!]!
teams: [UUID!]!
}
type Member { type Member {
id: ID! id: ID!
role: Role! role: Role!
fullName: String! fullName: String!
username: String! username: String!
profileIcon: ProfileIcon! profileIcon: ProfileIcon!
owned: OwnersList
} }
type RefreshToken { type RefreshToken {

View File

@ -6,6 +6,30 @@ extend type Mutation {
updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): TaskChecklistItem! updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): TaskChecklistItem!
setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): TaskChecklistItem! setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): TaskChecklistItem!
deleteTaskChecklistItem(input: DeleteTaskChecklistItem!): DeleteTaskChecklistItemPayload! deleteTaskChecklistItem(input: DeleteTaskChecklistItem!): DeleteTaskChecklistItemPayload!
updateTaskChecklistLocation(input: UpdateTaskChecklistLocation!): UpdateTaskChecklistLocationPayload!
updateTaskChecklistItemLocation(input: UpdateTaskChecklistItemLocation!): UpdateTaskChecklistItemLocationPayload!
}
input UpdateTaskChecklistItemLocation {
checklistID: UUID!
checklistItemID: UUID!
position: Float!
}
type UpdateTaskChecklistItemLocationPayload {
checklistID: UUID!
prevChecklistID: UUID!
checklistItem: TaskChecklistItem!
}
input UpdateTaskChecklistLocation {
checklistID: UUID!
position: Float!
}
type UpdateTaskChecklistLocationPayload {
checklist: TaskChecklist!
} }
input CreateTaskChecklist { input CreateTaskChecklist {

View File

@ -8,11 +8,13 @@ extend type Mutation {
input DeleteTeamMember { input DeleteTeamMember {
teamID: UUID! teamID: UUID!
userID: UUID! userID: UUID!
newOwnerID: UUID
} }
type DeleteTeamMemberPayload { type DeleteTeamMemberPayload {
teamID: UUID! teamID: UUID!
userID: UUID! userID: UUID!
affectedProjects: [Project!]!
} }
input CreateTeamMember { input CreateTeamMember {

View File

@ -4,6 +4,17 @@ extend type Mutation {
deleteUserAccount(input: DeleteUserAccount!): DeleteUserAccountPayload! deleteUserAccount(input: DeleteUserAccount!): DeleteUserAccountPayload!
logoutUser(input: LogoutUser!): Boolean! logoutUser(input: LogoutUser!): Boolean!
clearProfileAvatar: UserAccount! clearProfileAvatar: UserAccount!
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
}
input UpdateUserRole {
userID: UUID!
roleCode: RoleCode!
}
type UpdateUserRolePayload {
user: UserAccount!
} }
input NewRefreshToken { input NewRefreshToken {

View File

@ -17,13 +17,13 @@ var Aliases = map[string]interface{}{
// Runs go mod download and then installs the binary. // Runs go mod download and then installs the binary.
func Generate() error { func Generate() error {
files, err := ioutil.ReadDir("graph/schema/") files, err := ioutil.ReadDir("internal/graph/schema/")
if err != nil { if err != nil {
panic(err) panic(err)
} }
var schema strings.Builder var schema strings.Builder
for _, file := range files { for _, file := range files {
filename := "graph/schema/" + file.Name() filename := "internal/graph/schema/" + file.Name()
fmt.Println(filename) fmt.Println(filename)
f, err := os.Open(filename) f, err := os.Open(filename)
if err != nil { if err != nil {
@ -37,7 +37,7 @@ func Generate() error {
} }
// return sh.Run("go", "install", "./...") // return sh.Run("go", "install", "./...")
// fmt.Println(schema.String()) // fmt.Println(schema.String())
err = ioutil.WriteFile("graph/schema.graphqls", []byte(schema.String()), os.FileMode(0755)) err = ioutil.WriteFile("internal/graph/schema.graphqls", []byte(schema.String()), os.FileMode(0755))
if err != nil { if err != nil {
panic(err) panic(err)
} }