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"
],
"rules": {
"prettier/prettier": "error",
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"prettier/prettier": "warning",
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",

View File

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

View File

@ -1,6 +1,6 @@
overwrite: true
schema:
- '../api/graph/schema.graphqls'
- '../internal/graph/schema.graphqls'
documents:
- 'src/shared/graphql/*.graphqls'
- '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 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 { useRouteMatch, useHistory } from 'react-router';
import {useRouteMatch, useHistory} from 'react-router';
import {
useDeleteTaskChecklistMutation,
useUpdateTaskChecklistNameMutation,
useUpdateTaskChecklistItemLocationMutation,
useCreateTaskChecklistMutation,
useFindTaskQuery,
useUpdateTaskDueDateMutation,
@ -14,6 +15,7 @@ import {
useAssignTaskMutation,
useUnassignTaskMutation,
useSetTaskChecklistItemCompleteMutation,
useUpdateTaskChecklistLocationMutation,
useDeleteTaskChecklistItemMutation,
useUpdateTaskChecklistItemNameMutation,
useCreateTaskChecklistItemMutation,
@ -27,7 +29,7 @@ import produce from 'immer';
import styled from 'styled-components';
import Button from 'shared/components/Button';
import Input from 'shared/components/Input';
import { useForm } from 'react-hook-form';
import {useForm} from 'react-hook-form';
import updateApolloCache from 'shared/utils/cache';
const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => {
@ -47,7 +49,7 @@ const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => {
}, 0),
0,
);
return { total, complete };
return {total, complete};
};
const DeleteChecklistButton = styled(Button)`
@ -80,8 +82,8 @@ const InputError = styled.span`
type CreateChecklistPopupProps = {
onCreateChecklist: (data: CreateChecklistData) => void;
};
const CreateChecklistPopup: React.FC<CreateChecklistPopupProps> = ({ onCreateChecklist }) => {
const { register, handleSubmit, errors } = useForm<CreateChecklistData>();
const CreateChecklistPopup: React.FC<CreateChecklistPopupProps> = ({onCreateChecklist}) => {
const {register, handleSubmit, errors} = useForm<CreateChecklistData>();
const createUser = (data: CreateChecklistData) => {
onCreateChecklist(data);
};
@ -90,12 +92,13 @@ const CreateChecklistPopup: React.FC<CreateChecklistPopupProps> = ({ onCreateChe
<CreateChecklistForm onSubmit={handleSubmit(createUser)}>
<CreateChecklistInput
floatingLabel
value="Checklist"
width="100%"
label="Name"
id="name"
name="name"
variant="alternate"
ref={register({ required: 'Checklist name is required' })}
ref={register({required: 'Checklist name is required'})}
/>
<CreateChecklistButton type="submit">Create</CreateChecklistButton>
</CreateChecklistForm>
@ -113,7 +116,7 @@ type DetailsProps = {
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> = ({
projectURL,
@ -125,12 +128,46 @@ const Details: React.FC<DetailsProps> = ({
availableMembers,
refreshCache,
}) => {
const { userID } = useContext(UserIDContext);
const { showPopup, hidePopup } = usePopup();
const {userID} = useContext(UserIDContext);
const {showPopup, hidePopup} = usePopup();
const history = useHistory();
const match = useRouteMatch();
const [currentMemberTask, setCurrentMemberTask] = useState('');
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({
update: client => {
updateApolloCache<FindTaskQuery>(
@ -138,14 +175,14 @@ const Details: React.FC<DetailsProps> = ({
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
const {complete, total} = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
total,
};
}),
{ taskID },
{taskID},
);
},
});
@ -156,10 +193,10 @@ const Details: React.FC<DetailsProps> = ({
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const { checklists } = cache.findTask;
const item = deleteData.deleteTaskChecklist;
draftCache.findTask.checklists = checklists.filter(c => c.id !== item.taskChecklist.id);
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
const {checklists} = cache.findTask;
console.log(deleteData)
draftCache.findTask.checklists = checklists.filter(c => c.id !== deleteData.data.deleteTaskChecklist.taskChecklist.id);
const {complete, total} = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
@ -169,7 +206,7 @@ const Details: React.FC<DetailsProps> = ({
draftCache.findTask.badges.checklist = null;
}
}),
{ taskID },
{taskID},
);
},
});
@ -182,9 +219,9 @@ const Details: React.FC<DetailsProps> = ({
cache =>
produce(cache, draftCache => {
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 =>
produce(cache, draftCache => {
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
draftCache.findTask.checklists = cache.findTask.checklists.filter(c => item.id !== c.id);
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID)
if (targetIdx > -1) {
draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(c => item.id !== c.id);
}
const {complete, total} = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
total,
};
}),
{ taskID },
{taskID},
);
},
});
@ -217,11 +257,11 @@ const Details: React.FC<DetailsProps> = ({
cache =>
produce(cache, draftCache => {
const item = newTaskItem.data.createTaskChecklistItem;
const { checklists } = cache.findTask;
const {checklists} = cache.findTask;
const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
if (idx !== -1) {
draftCache.findTask.checklists[idx].items.push({ ...item });
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.checklists[idx].items.push({...item});
const {complete, total} = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
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 [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => {
@ -259,7 +299,6 @@ const Details: React.FC<DetailsProps> = ({
if (!data) {
return <div>loading</div>;
}
console.log(data.findTask);
return (
<>
<Modal
@ -271,25 +310,62 @@ const Details: React.FC<DetailsProps> = ({
return (
<TaskDetails
task={data.findTask}
onChecklistDrop={checklist => {
updateTaskChecklistLocation({
variables: {checklistID: checklist.id, position: checklist.position},
optimisticResponse: {
__typename: 'Mutation',
updateTaskChecklistLocation: {
__typename: 'UpdateTaskChecklistLocationPayload',
checklist: {
__typename: 'TaskChecklist',
position: checklist.position,
id: checklist.id,
},
},
},
});
}}
onChecklistItemDrop={(prevChecklistID, checklistID, checklistItem) => {
updateTaskChecklistItemLocation({
variables: {checklistID, checklistItemID: checklistItem.id, position: checklistItem.position},
optimisticResponse: {
__typename: 'Mutation',
updateTaskChecklistItemLocation: {
__typename: 'UpdateTaskChecklistItemLocationPayload',
prevChecklistID,
checklistID,
checklistItem: {
__typename: 'TaskChecklistItem',
position: checklistItem.position,
id: checklistItem.id,
taskChecklistID: checklistID,
},
},
},
});
}}
onTaskNameChange={onTaskNameChange}
onTaskDescriptionChange={onTaskDescriptionChange}
onToggleTaskComplete={task => {
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
setTaskComplete({variables: {taskID: task.id, complete: !task.complete}});
}}
onDeleteTask={onDeleteTask}
onChangeItemName={(itemID, itemName) => {
updateTaskChecklistItemName({ variables: { taskChecklistItemID: itemID, name: itemName } });
updateTaskChecklistItemName({variables: {taskChecklistItemID: itemID, name: itemName}});
}}
onCloseModal={() => history.push(projectURL)}
onChangeChecklistName={(checklistID, newName) => {
updateTaskChecklistName({ variables: { taskChecklistID: checklistID, name: newName } });
updateTaskChecklistName({variables: {taskChecklistID: checklistID, name: newName}});
}}
onDeleteItem={itemID => {
deleteTaskChecklistItem({ variables: { taskChecklistItemID: itemID } });
deleteTaskChecklistItem({variables: {taskChecklistItemID: itemID}});
}}
onToggleChecklistItem={(itemID, complete) => {
setTaskChecklistItemComplete({
variables: { taskChecklistItemID: itemID, complete },
variables: {taskChecklistItemID: itemID, complete},
optimisticResponse: {
__typename: 'Mutation',
setTaskChecklistItemComplete: {
@ -301,7 +377,7 @@ const Details: React.FC<DetailsProps> = ({
});
}}
onAddItem={(taskChecklistID, name, position) => {
createTaskChecklistItem({ variables: { taskChecklistID, name, position } });
createTaskChecklistItem({variables: {taskChecklistID, name, position}});
}}
onMemberProfile={($targetRef, memberID) => {
const member = data.findTask.assigned.find(m => m.id === memberID);
@ -313,7 +389,7 @@ const Details: React.FC<DetailsProps> = ({
user={member}
bio="None"
onRemoveFromTask={() => {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
unassignTask({variables: {taskID: data.findTask.id, userID: userID ?? ''}});
}}
/>
</Popup>,
@ -329,9 +405,9 @@ const Details: React.FC<DetailsProps> = ({
activeMembers={data.findTask.assigned}
onMemberChange={(member, isActive) => {
if (isActive) {
assignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
assignTask({variables: {taskID: data.findTask.id, userID: userID ?? ''}});
} 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
color="danger"
onClick={() => {
deleteTaskChecklist({ variables: { taskChecklistID: checklistID } });
deleteTaskChecklist({variables: {taskChecklistID: checklistID}});
hidePopup();
}}
>
@ -403,11 +479,11 @@ const Details: React.FC<DetailsProps> = ({
<DueDateManager
task={task}
onRemoveDueDate={t => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null } });
updateTaskDueDate({variables: {taskID: t.id, dueDate: null}});
hidePopup();
}}
onDueDateChange={(t, newDueDate) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } });
updateTaskDueDate({variables: {taskID: t.id, dueDate: newDueDate}});
hidePopup();
}}
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
import React, { useState, useRef, useContext, useEffect } from 'react';
import { MENU_TYPES } from 'shared/components/TopNavbar';
import React, {useState, useRef, useEffect, useContext} from 'react';
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 { usePopup, Popup } from 'shared/components/PopupMenu';
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation } from 'react-router-dom';
import GlobalTopNavbar, {ProjectPopup} from 'App/TopNavbar';
import styled from 'styled-components/macro';
import {usePopup, Popup} from 'shared/components/PopupMenu';
import LabelManagerEditor from './LabelManagerEditor'
import {useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation, Redirect} 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 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 MiniProfile from 'shared/components/MiniProfile';
import Details from './Details';
import { useApolloClient } from '@apollo/react-hooks';
import UserIDContext from 'App/context';
import DueDateManager from 'shared/components/DueDateManager';
import Input from 'shared/components/Input';
import Member from 'shared/components/Member';
import Board from './Board'
import Details from './Details'
const SearchInput = styled(Input)`
margin: 0;
@ -79,7 +55,7 @@ type UserManagementPopupProps = {
onAddProjectMember: (userID: string) => void;
};
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({ users, projectMembers, onAddProjectMember }) => {
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({users, projectMembers, onAddProjectMember}) => {
return (
<Popup tab={0} title="Invite a user">
<SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" />
@ -111,140 +87,6 @@ interface QuickCardEditorState {
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 {
projectID: string;
}
@ -256,259 +98,28 @@ const initialQuickCardEditorState: QuickCardEditorState = {
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 { projectID } = useParams<ProjectParams>();
const {projectID} = useParams<ProjectParams>();
const history = useHistory();
const match = useRouteMatch();
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({
onCompleted: newTaskLabel => {
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
console.log(taskLabelsRef.current);
},
});
const { loading, data, refetch } = useFindProjectQuery({
variables: { projectId: projectID },
onCompleted: newData => {},
const [updateProjectMemberRole] = useUpdateProjectMemberRoleMutation();
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({
update: (client, newName) => {
updateApolloCache<FindProjectQuery>(
@ -518,12 +129,11 @@ const Project = () => {
produce(cache, draftCache => {
draftCache.findProject.name = newName.data.updateProjectName.name;
}),
{ projectId: projectID },
{projectId: projectID},
);
},
});
const [setTaskComplete] = useSetTaskCompleteMutation();
const [createProjectMember] = useCreateProjectMemberMutation({
update: (client, response) => {
updateApolloCache<FindProjectQuery>(
@ -531,9 +141,9 @@ const Project = () => {
FindProjectDocument,
cache =>
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,
);
}),
{ projectId: projectID },
{projectId: projectID},
);
},
});
const client = useApolloClient();
const { userID } = useContext(UserIDContext);
const {userID} = useContext(UserIDContext);
const location = useLocation();
const { showPopup, hidePopup } = usePopup();
const {showPopup, hidePopup} = usePopup();
const $labelsRef = useRef<HTMLDivElement>(null);
const labelsRef = useRef<Array<ProjectLabel>>([]);
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
@ -576,58 +185,32 @@ const Project = () => {
}
if (data) {
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;
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 (
<>
<GlobalTopNavbar
onChangeRole={(userID, roleCode) => {
updateProjectMemberRole({ variables: { userID, roleCode, projectID } });
updateProjectMemberRole({variables: {userID, roleCode, projectID}});
}}
onChangeProjectOwner={uid => {
setProjectOwner({ variables: { ownerID: uid, projectID } });
setProjectOwner({variables: {ownerID: uid, projectID}});
hidePopup();
}}
onRemoveFromBoard={userID => {
deleteProjectMember({ variables: { userID, projectID } });
deleteProjectMember({variables: {userID, projectID}});
hidePopup();
}}
onSaveProjectName={projectName => {
updateProjectName({ variables: { projectID, name: projectName } });
updateProjectName({variables: {projectID, name: projectName}});
}}
onInviteUser={$target => {
showPopup(
$target,
<UserManagementPopup
onAddProjectMember={userID => {
createProjectMember({ variables: { userID, projectID } });
createProjectMember({variables: {userID, projectID}});
}}
users={data.users}
projectMembers={data.findProject.members}
@ -635,250 +218,41 @@ const Project = () => {
);
}}
popupContent={<ProjectPopup history={history} name={data.findProject.name} projectID={projectID} />}
menuType={[{ name: 'Board', link: location.pathname }]}
menuType={[{name: 'Board', link: location.pathname}]}
currentTab={0}
projectMembers={data.findProject.members}
projectID={projectID}
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
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>) => (
<Details
refreshCache={() => {}}
availableMembers={data.findProject.members}
projectURL={match.url}
projectURL={`${match.url}/board`}
taskID={routeProps.match.params.taskID}
onTaskNameChange={(updatedTask, newName) => {
updateTaskName({ variables: { taskID: updatedTask.id, name: newName } });
updateTaskName({variables: {taskID: updatedTask.id, name: newName}});
}}
onTaskDescriptionChange={(updatedTask, newDescription) => {
updateTaskDescription({ variables: { taskID: updatedTask.id, description: newDescription } });
updateTaskDescription({variables: {taskID: updatedTask.id, description: newDescription}});
}}
onDeleteTask={deletedTask => {
deleteTask({ variables: { taskID: deletedTask.id } });
deleteTask({variables: {taskID: deletedTask.id}});
}}
onOpenAddLabelPopup={(task, $targetRef) => {
taskLabelsRef.current = task.labels;
@ -886,7 +260,7 @@ const Project = () => {
$targetRef,
<LabelManagerEditor
onLabelToggle={labelID => {
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
toggleTaskLabel({variables: {taskID: task.id, projectLabelID: labelID}});
}}
labelColors={data.labelColors}
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 updateApolloCache from 'shared/utils/cache';
import produce from 'immer';
import Button from 'shared/components/Button';
import UserIDContext from 'App/context';
import Select from 'shared/components/Select';
import {
useGetTeamQuery,
RoleCode,
@ -155,23 +157,28 @@ export const RemoveMemberButton = styled(Button)`
width: 100%;
`;
type TeamRoleManagerPopupProps = {
user: TaskUser;
currentUserID: string;
subject: TaskUser;
members: Array<TaskUser>;
warning?: string | null;
canChangeRole: boolean;
onChangeRole: (roleCode: RoleCode) => void;
onRemoveFromTeam?: () => void;
onRemoveFromTeam?: (newOwnerID: string | null) => void;
onChangeTeamOwner?: (userID: string) => void;
};
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
members,
warning,
user,
subject,
currentUserID,
canChangeRole,
onRemoveFromTeam,
onChangeTeamOwner,
onChangeRole,
}) => {
const { hidePopup, setTab } = usePopup();
const [orphanedProjectOwner, setOrphanedProjectOwner] = useState<{ label: string; value: string } | null>(null);
return (
<>
<Popup title={null} tab={0}>
@ -186,14 +193,14 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
Set as team owner...
</MiniProfileActionItem>
)}
{user.role && (
{subject.role && (
<MiniProfileActionItem
onClick={() => {
setTab(1);
}}
>
Change permissions...
<CurrentPermission>{`(${user.role.name})`}</CurrentPermission>
<CurrentPermission>{`(${subject.role.name})`}</CurrentPermission>
</MiniProfileActionItem>
)}
{onRemoveFromTeam && (
@ -202,7 +209,7 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
setTab(2);
}}
>
Remove from team...
{currentUserID === subject.id ? 'Leave team...' : 'Remove from team...'}
</MiniProfileActionItem>
)}
</MiniProfileActionWrapper>
@ -218,13 +225,13 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
<MiniProfileActions>
<MiniProfileActionWrapper>
{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 => (
<MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
disabled={subject.role && perm.code !== subject.role.code && !canChangeRole}
key={perm.code}
onClick={() => {
if (onChangeRole && user.role && perm.code !== user.role.code) {
if (onChangeRole && subject.role && perm.code !== subject.role.code) {
switch (perm.code) {
case 'owner':
onChangeRole(RoleCode.Owner);
@ -244,13 +251,13 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
>
<RoleName>
{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>
<RoleDescription>{perm.description}</RoleDescription>
</MiniProfileActionItem>
))}
</MiniProfileActionWrapper>
{user.role && user.role.code === 'owner' && (
{subject.role && subject.role.code === 'owner' && (
<>
<Separator />
<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}>
<Content>
<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>
{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
color="danger"
onClick={() => {
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}>
<Content>
<DeleteDescription>
This will change the project owner from you to this user. They will be able to view and edit cards, remove
members, and change all settings for the project. They will also be able to delete the project.
This will change the project owner from you to this subject. They will be able to view and edit cards,
remove members, and change all settings for the project. They will also be able to delete the project.
</DeleteDescription>
<RemoveMemberButton
color="warning"
onClick={() => {
if (onChangeTeamOwner) {
onChangeTeamOwner(user.id);
onChangeTeamOwner(subject.id);
}
}}
>
@ -421,6 +443,7 @@ type MembersProps = {
const Members: React.FC<MembersProps> = ({ teamID }) => {
const { showPopup, hidePopup } = usePopup();
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
const { userID } = useContext(UserIDContext);
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 [createTeamMember] = useCreateTeamMemberMutation({
@ -508,7 +531,9 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
showPopup(
$target,
<TeamRoleManagerPopup
user={member}
currentUserID={userID ?? ''}
subject={member}
members={data.findTeam.members}
warning={member.role && member.role.code === 'owner' ? warning : null}
onChangeTeamOwner={
member.role && member.role.code !== 'owner' ? (userID: string) => {} : undefined
@ -518,8 +543,8 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
onRemoveFromTeam={
member.role && member.role.code === 'owner'
? undefined
: () => {
deleteTeamMember({ variables: { teamID, userID: member.id } });
: newOwnerID => {
deleteTeamMember({ variables: { teamID, newOwnerID, userID: member.id } });
hidePopup();
}
}

View File

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

View File

@ -1,38 +1,294 @@
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
import { User, Plus, Lock, Pencil, Trash } from 'shared/icons';
import { AgGridReact } from 'ag-grid-react';
import React, {useState, useRef} from 'react';
import {UserPlus, Checkmark} from 'shared/icons';
import styled, {css} from 'styled-components';
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-theme-material.css';
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;
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;
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;
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`
@ -103,7 +359,7 @@ const ActionButtonWrapper = styled.div`
display: inline-flex;
`;
const ActionButton: React.FC<ActionButtonProps> = ({ onClick, children }) => {
const ActionButton: React.FC<ActionButtonProps> = ({onClick, children}) => {
const $wrapper = useRef<HTMLDivElement>(null);
return (
<ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}>
@ -133,7 +389,7 @@ type ListTableProps = {
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
};
const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
const ListTable: React.FC<ListTableProps> = ({users, onDeleteUser}) => {
const data = {
defaultColDef: {
resizable: true,
@ -146,10 +402,10 @@ const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
headerCheckboxSelection: true,
checkboxSelection: true,
},
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
{ minWidth: 225, headerName: 'Email', field: 'email' },
{ minWidth: 200, headerName: 'Name', editable: true, field: 'fullName' },
{ minWidth: 200, headerName: 'Role', editable: true, field: 'roleName' },
{minWidth: 210, headerName: 'Username', editable: true, field: 'username'},
{minWidth: 225, headerName: 'Email', field: 'email'},
{minWidth: 200, headerName: 'Name', editable: true, field: 'fullName'},
{minWidth: 200, headerName: 'Role', editable: true, field: 'roleName'},
{
minWidth: 200,
headerName: 'Actions',
@ -168,12 +424,12 @@ const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
};
return (
<Root>
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}>
<div className="ag-theme-material" style={{height: '296px', width: '100%'}}>
<AgGridReact
rowSelection="multiple"
defaultColDef={data.defaultColDef}
columnDefs={data.columnDefs}
rowData={users.map(u => ({ ...u, roleName: u.role.name }))}
rowData={users.map(u => ({...u, roleName: u.role.name}))}
frameworkComponents={data.frameworkComponents}
onFirstDataRendered={params => {
params.api.sizeColumnsToFit();
@ -199,7 +455,9 @@ const Container = styled.div`
padding: 2.2rem;
display: flex;
width: 100%;
max-width: 1400px;
position: relative;
margin: 0 auto;
`;
const TabNav = styled.div`
@ -223,7 +481,7 @@ const TabNavItem = styled.li`
display: block;
position: relative;
`;
const TabNavItemButton = styled.button<{ active: boolean }>`
const TabNavItemButton = styled.button<{active: boolean}>`
cursor: pointer;
display: flex;
align-items: center;
@ -250,7 +508,7 @@ const TabNavItemSpan = styled.span`
font-size: 14px;
`;
const TabNavLine = styled.span<{ top: number }>`
const TabNavLine = styled.span<{top: number}>`
left: auto;
right: 0;
width: 2px;
@ -270,6 +528,7 @@ const TabContentWrapper = styled.div`
display: block;
overflow: hidden;
width: 100%;
margin-left: 1rem;
`;
const TabContent = styled.div`
@ -279,17 +538,10 @@ const TabContent = styled.div`
padding: 0;
padding: 1.5rem;
background-color: #10163a;
margin-left: 1rem !important;
border-radius: 0.5rem;
`;
const items = [
{ name: 'Insights' },
{ name: 'Members' },
{ name: 'Teams' },
{ name: 'Security' },
{ name: 'Settings' },
];
const items = [{name: 'Members'}, {name: 'Settings'}];
type NavItemProps = {
active: boolean;
@ -297,7 +549,7 @@ type NavItemProps = {
tab: number;
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);
return (
<TabNavItem
@ -326,10 +578,15 @@ type AdminProps = {
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 [currentTab, setTab] = useState(initialTab);
const {showPopup, hidePopup} = usePopup();
const $tabNav = useRef<HTMLDivElement>(null);
const [updateUserRole] = useUpdateUserRoleMutation()
return (
<Container>
<TabNav ref={$tabNav}>
@ -353,17 +610,64 @@ const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onDeleteUser, onIn
</TabNav>
<TabContentWrapper>
<TabContent>
<MemberActions>
<NewUserButton variant="outline" onClick={onAddUser}>
<Plus color="rgba(115, 103, 240)" size={10} />
<span style={{ paddingLeft: '5px' }}>Create member</span>
</NewUserButton>
<InviteUserButton variant="outline" onClick={onInviteUser}>
<Plus color="rgba(115, 103, 240)" size={10} />
<span style={{ paddingLeft: '5px' }}>Invite member</span>
</InviteUserButton>
</MemberActions>
<ListTable onDeleteUser={onDeleteUser} users={users} />
<MemberListWrapper>
<MemberListHeader>
<ListTitle>{`Users (${users.length})`}</ListTitle>
<ListDesc>
Team members can view and join all Team Visible boards and create new boards in the team.
</ListDesc>
<ListActions>
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
<InviteMemberButton
onClick={$target => {
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>
</TabContentWrapper>
</Container>

View File

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

View File

@ -1,6 +1,13 @@
import React, { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
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 TextareaAutosize from 'react-autosize-textarea';
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;
`;
const ChecklistItems = styled.div`
export const ChecklistItems = styled.div`
min-height: 8px;
`;
@ -199,7 +206,7 @@ const TrashButton = styled(Trash)`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const ChecklistItemWrapper = styled.div`
const ChecklistItemWrapper = styled.div<{ ref: any }>`
user-select: none;
clear: both;
padding-left: 40px;
@ -270,122 +277,121 @@ type ChecklistItemProps = {
complete: boolean;
name: string;
onChangeName: (itemID: string, currentName: string) => void;
wrapperProps: any;
handleProps: any;
onToggleItem: (itemID: string, complete: boolean) => void;
onDeleteItem: (itemID: string) => void;
};
const ChecklistItem: React.FC<ChecklistItemProps> = ({
itemID,
complete,
name,
onChangeName,
onToggleItem,
onDeleteItem,
}) => {
const $item = useRef<HTMLDivElement>(null);
const $editor = useRef<HTMLTextAreaElement>(null);
const [editting, setEditting] = useState(false);
const [currentName, setCurrentName] = useState(name);
useEffect(() => {
if (editting && $editor && $editor.current) {
$editor.current.focus();
$editor.current.select();
}
}, [editting]);
useOnOutsideClick($item, true, () => setEditting(false), null);
return (
<ChecklistItemWrapper ref={$item}>
<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);
export const ChecklistItem = React.forwardRef(
(
{ itemID, complete, name, wrapperProps, handleProps, onChangeName, onToggleItem, onDeleteItem }: ChecklistItemProps,
$item,
) => {
const $editor = useRef<HTMLTextAreaElement>(null);
const [editting, setEditting] = useState(false);
const [currentName, setCurrentName] = useState(name);
useEffect(() => {
if (editting && $editor && $editor.current) {
$editor.current.focus();
$editor.current.select();
}
}, [editting]);
// useOnOutsideClick($item, true, () => setEditting(false), null);
return (
<ChecklistItemWrapper ref={$item} {...wrapperProps} {...handleProps}>
<ChecklistIcon
onClick={e => {
e.stopPropagation();
onToggleItem(itemID, !complete);
}}
>
<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>
);
};
{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>
<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 = {
onAddItem: (name: string) => void;
@ -503,94 +509,112 @@ type ChecklistProps = {
checklistID: string;
onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
name: string;
children: React.ReactNode;
onChangeName: (item: string) => void;
onToggleItem: (taskID: string, complete: boolean) => void;
onChangeItemName: (itemID: string, currentName: string) => void;
wrapperProps: any;
handleProps: any;
onDeleteItem: (itemID: string) => void;
onAddItem: (itemName: string) => void;
items: Array<TaskChecklistItem>;
};
const Checklist: React.FC<ChecklistProps> = ({
checklistID,
onDeleteChecklist,
name,
items,
onToggleItem,
onAddItem,
onChangeItemName,
onChangeName,
onDeleteItem,
}) => {
const $name = useRef<HTMLTextAreaElement>(null);
const complete = items.reduce((prev, item) => prev + (item.complete ? 1 : 0), 0);
const percent = items.length === 0 ? 0 : Math.floor((complete / items.length) * 100);
const [editting, setEditting] = useState(false);
// useOnOutsideClick($name, true, () => setEditting(false), null);
useEffect(() => {
if (editting && $name && $name.current) {
$name.current.focus();
$name.current.select();
}
}, [editting]);
return (
<Wrapper>
<WindowTitle>
<WindowTitleIcon width={24} height={24} />
{editting ? (
<ChecklistTitleEditor
ref={$name}
name={name}
onChangeName={currentName => {
onChangeName(currentName);
setEditting(false);
}}
onCancel={() => {
setEditting(false);
}}
/>
) : (
<WindowChecklistTitle>
<WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText>
<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>
<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}
const Checklist = React.forwardRef(
(
{
checklistID,
children,
onDeleteChecklist,
name,
items,
wrapperProps,
handleProps,
onToggleItem,
onAddItem,
onChangeItemName,
onChangeName,
onDeleteItem,
}: ChecklistProps,
$container,
) => {
const $name = useRef<HTMLTextAreaElement>(null);
const complete = items.reduce((prev, item) => prev + (item.complete ? 1 : 0), 0);
const percent = items.length === 0 ? 0 : Math.floor((complete / items.length) * 100);
const [editting, setEditting] = useState(false);
// useOnOutsideClick($name, true, () => setEditting(false), null);
useEffect(() => {
if (editting && $name && $name.current) {
$name.current.focus();
$name.current.select();
}
}, [editting]);
useEffect(() => {
console.log($container);
}, [$container]);
return (
<Wrapper ref={$container} {...wrapperProps}>
<WindowTitle>
<WindowTitleIcon width={24} height={24} />
{editting ? (
<ChecklistTitleEditor
ref={$name}
name={name}
onChangeName={currentName => {
onChangeName(currentName);
setEditting(false);
}}
onCancel={() => {
setEditting(false);
}}
/>
))}
</ChecklistItems>
<AddNewItem onAddItem={onAddItem} />
</Wrapper>
);
};
) : (
<WindowChecklistTitle {...handleProps}>
<WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText>
<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;

View File

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

View File

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

View File

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

View File

@ -75,6 +75,8 @@ export const Default = () => {
onDeleteChecklist={action('delete checklist')}
onOpenAddChecklistPopup={action(' open checklist')}
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 useOnOutsideClick from 'shared/hooks/onOutsideClick';
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 moment from 'moment';
@ -46,7 +54,10 @@ import {
MetaDetailTitle,
MetaDetailContent,
} from './Styles';
import Checklist from '../Checklist';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import styled from 'styled-components';
const ChecklistContainer = styled.div``;
type TaskContentProps = {
onEditContent: () => void;
@ -145,6 +156,8 @@ type TaskDetailsProps = {
onChangeChecklistName: (checklistID: string, name: string) => void;
onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
onCloseModal: () => void;
onChecklistDrop: (checklist: TaskChecklist) => void;
onChecklistItemDrop: (prevChecklistID: string, checklistID: string, checklistItem: TaskChecklistItem) => void;
};
const TaskDetails: React.FC<TaskDetailsProps> = ({
@ -153,6 +166,8 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onTaskNameChange,
onOpenAddChecklistPopup,
onChangeChecklistName,
onChecklistDrop,
onChecklistItemDrop,
onToggleTaskComplete,
onTaskDescriptionChange,
onChangeItemName,
@ -190,14 +205,91 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onOpenAddMemberPopup(task, $target);
};
const onAddChecklist = ($target: React.RefObject<HTMLElement>) => {
onOpenAddChecklistPopup(task, $target)
}
onOpenAddChecklistPopup(task, $target);
};
const $dueDateLabel = useRef<HTMLDivElement>(null);
const $addLabelRef = useRef<HTMLDivElement>(null);
const onAddLabel = ($target: React.RefObject<HTMLElement>) => {
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 (
<>
<TaskActions>
@ -289,33 +381,80 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
) : (
<TaskContent description={description} onEditContent={handleClick} />
)}
{task.checklists &&
task.checklists
.slice()
.sort((a, b) => a.position - b.position)
.map(checklist => (
<Checklist
key={checklist.id}
name={checklist.name}
checklistID={checklist.id}
items={checklist.items}
onDeleteChecklist={onDeleteChecklist}
onChangeName={newName => onChangeChecklistName(checklist.id, newName)}
onToggleItem={onToggleChecklistItem}
onDeleteItem={onDeleteItem}
onAddItem={n => {
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}
/>
))}
<DragDropContext onDragEnd={onDragEnd}>
<Droppable direction="vertical" type="checklist" droppableId="root">
{dropProvided => (
<ChecklistContainer {...dropProvided.droppableProps} ref={dropProvided.innerRef}>
{task.checklists &&
task.checklists
.slice()
.sort((a, b) => a.position - b.position)
.map((checklist, idx) => (
<Draggable key={checklist.id} draggableId={checklist.id} index={idx}>
{provided => (
<Checklist
ref={provided.innerRef}
wrapperProps={provided.draggableProps}
handleProps={provided.dragHandleProps}
key={checklist.id}
name={checklist.name}
checklistID={checklist.id}
items={checklist.items}
onDeleteChecklist={onDeleteChecklist}
onChangeName={newName => onChangeChecklistName(checklist.id, newName)}
onToggleItem={onToggleChecklistItem}
onDeleteItem={onDeleteItem}
onAddItem={n => {
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>
</TaskDetailsContent>
<TaskDetailsSidebar>

View File

@ -54,6 +54,12 @@ export type ProfileIcon = {
bgColor?: Maybe<Scalars['String']>;
};
export type OwnersList = {
__typename?: 'OwnersList';
projects: Array<Scalars['UUID']>;
teams: Array<Scalars['UUID']>;
};
export type Member = {
__typename?: 'Member';
id: Scalars['ID'];
@ -61,6 +67,7 @@ export type Member = {
fullName: Scalars['String'];
username: Scalars['String'];
profileIcon: ProfileIcon;
owned?: Maybe<OwnersList>;
};
export type RefreshToken = {
@ -249,7 +256,9 @@ export type Mutation = {
updateProjectLabelName: ProjectLabel;
updateProjectMemberRole: UpdateProjectMemberRolePayload;
updateProjectName: Project;
updateTaskChecklistItemLocation: UpdateTaskChecklistItemLocationPayload;
updateTaskChecklistItemName: TaskChecklistItem;
updateTaskChecklistLocation: UpdateTaskChecklistLocationPayload;
updateTaskChecklistName: TaskChecklist;
updateTaskDescription: Task;
updateTaskDueDate: Task;
@ -258,6 +267,7 @@ export type Mutation = {
updateTaskLocation: UpdateTaskLocationPayload;
updateTaskName: Task;
updateTeamMemberRole: UpdateTeamMemberRolePayload;
updateUserRole: UpdateUserRolePayload;
};
@ -441,11 +451,21 @@ export type MutationUpdateProjectNameArgs = {
};
export type MutationUpdateTaskChecklistItemLocationArgs = {
input: UpdateTaskChecklistItemLocation;
};
export type MutationUpdateTaskChecklistItemNameArgs = {
input: UpdateTaskChecklistItemName;
};
export type MutationUpdateTaskChecklistLocationArgs = {
input: UpdateTaskChecklistLocation;
};
export type MutationUpdateTaskChecklistNameArgs = {
input: UpdateTaskChecklistName;
};
@ -485,6 +505,11 @@ export type MutationUpdateTeamMemberRoleArgs = {
input: UpdateTeamMemberRole;
};
export type MutationUpdateUserRoleArgs = {
input: UpdateUserRole;
};
export type ProjectsFilter = {
teamID?: Maybe<Scalars['UUID']>;
};
@ -656,6 +681,29 @@ export type UpdateTaskName = {
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 = {
taskID: Scalars['UUID'];
name: Scalars['String'];
@ -769,12 +817,14 @@ export type DeleteTeamPayload = {
export type DeleteTeamMember = {
teamID: Scalars['UUID'];
userID: Scalars['UUID'];
newOwnerID?: Maybe<Scalars['UUID']>;
};
export type DeleteTeamMemberPayload = {
__typename?: 'DeleteTeamMemberPayload';
teamID: Scalars['UUID'];
userID: Scalars['UUID'];
affectedProjects: Array<Project>;
};
export type CreateTeamMember = {
@ -812,6 +862,16 @@ export type SetTeamOwnerPayload = {
newOwner: Member;
};
export type UpdateUserRole = {
userID: Scalars['UUID'];
roleCode: RoleCode;
};
export type UpdateUserRolePayload = {
__typename?: 'UpdateUserRolePayload';
user: UserAccount;
};
export type NewRefreshToken = {
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 = {
taskChecklistItemID: Scalars['UUID'];
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 = {
taskChecklistID: Scalars['UUID'];
name: Scalars['String'];
@ -1470,6 +1566,7 @@ export type DeleteTeamMutation = (
export type DeleteTeamMemberMutationVariables = {
teamID: Scalars['UUID'];
userID: Scalars['UUID'];
newOwnerID?: Maybe<Scalars['UUID']>;
};
@ -1497,7 +1594,10 @@ export type GetTeamQuery = (
& { role: (
{ __typename?: 'Role' }
& Pick<Role, 'code' | 'name'>
), profileIcon: (
), owned?: Maybe<(
{ __typename?: 'OwnersList' }
& Pick<OwnersList, 'projects' | 'teams'>
)>, profileIcon: (
{ __typename?: 'ProfileIcon' }
& 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 = {};
@ -2796,6 +2917,46 @@ export function useSetTaskCompleteMutation(baseOptions?: ApolloReactHooks.Mutati
export type SetTaskCompleteMutationHookResult = ReturnType<typeof useSetTaskCompleteMutation>;
export type SetTaskCompleteMutationResult = ApolloReactCommon.MutationResult<SetTaskCompleteMutation>;
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`
mutation updateTaskChecklistItemName($taskChecklistItemID: UUID!, $name: String!) {
updateTaskChecklistItemName(input: {taskChecklistItemID: $taskChecklistItemID, name: $name}) {
@ -2830,6 +2991,42 @@ export function useUpdateTaskChecklistItemNameMutation(baseOptions?: ApolloReact
export type UpdateTaskChecklistItemNameMutationHookResult = ReturnType<typeof useUpdateTaskChecklistItemNameMutation>;
export type UpdateTaskChecklistItemNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistItemNameMutation>;
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`
mutation updateTaskChecklistName($taskChecklistID: UUID!, $name: String!) {
updateTaskChecklistName(input: {taskChecklistID: $taskChecklistID, name: $name}) {
@ -3026,8 +3223,8 @@ export type DeleteTeamMutationHookResult = ReturnType<typeof useDeleteTeamMutati
export type DeleteTeamMutationResult = ApolloReactCommon.MutationResult<DeleteTeamMutation>;
export type DeleteTeamMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTeamMutation, DeleteTeamMutationVariables>;
export const DeleteTeamMemberDocument = gql`
mutation deleteTeamMember($teamID: UUID!, $userID: UUID!) {
deleteTeamMember(input: {teamID: $teamID, userID: $userID}) {
mutation deleteTeamMember($teamID: UUID!, $userID: UUID!, $newOwnerID: UUID) {
deleteTeamMember(input: {teamID: $teamID, userID: $userID, newOwnerID: $newOwnerID}) {
teamID
userID
}
@ -3050,6 +3247,7 @@ export type DeleteTeamMemberMutationFn = ApolloReactCommon.MutationFunction<Dele
* variables: {
* teamID: // value for 'teamID'
* userID: // value for 'userID'
* newOwnerID: // value for 'newOwnerID'
* },
* });
*/
@ -3073,6 +3271,10 @@ export const GetTeamDocument = gql`
code
name
}
owned {
projects
teams
}
profileIcon {
url
initials
@ -3560,6 +3762,45 @@ export function useDeleteUserAccountMutation(baseOptions?: ApolloReactHooks.Muta
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
export type DeleteUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteUserAccountMutation>;
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`
query 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';
export const DELETE_TEAM_MEMBER_MUTATION = gql`
mutation deleteTeamMember($teamID: UUID!, $userID: UUID!) {
deleteTeamMember(input: { teamID: $teamID, userID: $userID }) {
mutation deleteTeamMember($teamID: UUID!, $userID: UUID!, $newOwnerID: UUID) {
deleteTeamMember(input: { teamID: $teamID, userID: $userID, newOwnerID: $newOwnerID }) {
teamID
userID
}

View File

@ -14,6 +14,10 @@ export const GET_TEAM_QUERY = gql`
code
name
}
owned {
projects
teams
}
profileIcon {
url
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?
resolver:
layout: follow-schema
dir: graph
dir: internal/graph
package: graph
# 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
}
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
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)
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (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)
GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, 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)
UpdateProjectMemberRole(ctx context.Context, arg UpdateProjectMemberRoleParams) (ProjectMember, 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)
UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (TaskChecklist, error)
UpdateTaskChecklistPosition(ctx context.Context, arg UpdateTaskChecklistPositionParams) (TaskChecklist, error)
UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescriptionParams) (Task, error)
UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDateParams) (Task, error)
UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
@ -96,6 +99,7 @@ type Querier interface {
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error)
UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
}
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
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
UPDATE task_checklist_item SET name = $2 WHERE task_checklist_item_id = $1
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
INNER JOIN role ON role.code = user_account.role_code
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
}
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
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
@ -266,3 +291,25 @@ func (q *Queries) UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChe
)
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
}
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 {
TeamID uuid.UUID `json:"teamID"`
UserID uuid.UUID `json:"userID"`
TeamID uuid.UUID `json:"teamID"`
UserID uuid.UUID `json:"userID"`
NewOwnerID *uuid.UUID `json:"newOwnerID"`
}
type DeleteTeamMemberPayload struct {
TeamID uuid.UUID `json:"teamID"`
UserID uuid.UUID `json:"userID"`
TeamID uuid.UUID `json:"teamID"`
UserID uuid.UUID `json:"userID"`
AffectedProjects []db.Project `json:"affectedProjects"`
}
type DeleteTeamPayload struct {
@ -174,6 +176,7 @@ type Member struct {
FullName string `json:"fullName"`
Username string `json:"username"`
ProfileIcon *ProfileIcon `json:"profileIcon"`
Owned *OwnersList `json:"owned"`
}
type NewProject struct {
@ -229,6 +232,11 @@ type NewUserAccount struct {
RoleCode string `json:"roleCode"`
}
type OwnersList struct {
Projects []uuid.UUID `json:"projects"`
Teams []uuid.UUID `json:"teams"`
}
type ProfileIcon struct {
URL *string `json:"url"`
Initials *string `json:"initials"`
@ -326,11 +334,32 @@ type UpdateProjectName struct {
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 {
TaskChecklistItemID uuid.UUID `json:"taskChecklistItemID"`
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 {
TaskChecklistID uuid.UUID `json:"taskChecklistID"`
Name string `json:"name"`
@ -372,6 +401,15 @@ type UpdateTeamMemberRolePayload struct {
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
const (

View File

@ -35,12 +35,18 @@ type ProfileIcon {
bgColor: String
}
type OwnersList {
projects: [UUID!]!
teams: [UUID!]!
}
type Member {
id: ID!
role: Role!
fullName: String!
username: String!
profileIcon: ProfileIcon!
owned: OwnersList
}
type RefreshToken {
@ -361,6 +367,30 @@ extend type Mutation {
updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): TaskChecklistItem!
setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): TaskChecklistItem!
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 {
@ -492,11 +522,13 @@ extend type Mutation {
input DeleteTeamMember {
teamID: UUID!
userID: UUID!
newOwnerID: UUID
}
type DeleteTeamMemberPayload {
teamID: UUID!
userID: UUID!
affectedProjects: [Project!]!
}
input CreateTeamMember {
@ -537,6 +569,17 @@ extend type Mutation {
deleteUserAccount(input: DeleteUserAccount!): DeleteUserAccountPayload!
logoutUser(input: LogoutUser!): Boolean!
clearProfileAvatar: UserAccount!
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
}
input UpdateUserRole {
userID: UUID!
roleCode: RoleCode!
}
type UpdateUserRolePayload {
user: UserAccount!
}
input NewRefreshToken {

View File

@ -426,6 +426,26 @@ func (r *mutationResolver) DeleteTaskChecklistItem(ctx context.Context, input De
}, 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) {
createdAt := time.Now().UTC()
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) {
_, 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 {
return &DeleteTeamMemberPayload{}, err
}
@ -690,6 +714,11 @@ func (r *mutationResolver) DeleteTeamMember(ctx context.Context, input DeleteTea
if err != nil {
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
}
@ -754,6 +783,15 @@ func (r *mutationResolver) ClearProfileAvatar(ctx context.Context) (*db.UserAcco
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) {
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) {
user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
members := []Member{}
log.WithFields(log.Fields{"teamID": obj.TeamID}).Info("getting members")
if err == sql.ErrNoRows {
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")
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
if user.ProfileAvatarUrl.Valid {
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}
members = append(members, Member{
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)
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")
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}
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

View File

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

View File

@ -6,6 +6,30 @@ extend type Mutation {
updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): TaskChecklistItem!
setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): TaskChecklistItem!
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 {

View File

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

View File

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

View File

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