chore: project cleanup and bugfixes
This commit is contained in:
parent
e7e6fdc24c
commit
a20ff90106
@ -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",
|
||||
|
@ -3,5 +3,6 @@ module.exports = {
|
||||
trailingComma: "all",
|
||||
singleQuote: true,
|
||||
printWidth: 120,
|
||||
tabWidth: 2
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true,
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
overwrite: true
|
||||
schema:
|
||||
- '../api/graph/schema.graphqls'
|
||||
- '../internal/graph/schema.graphqls'
|
||||
documents:
|
||||
- 'src/shared/graphql/*.graphqls'
|
||||
- 'src/shared/graphql/**/*.ts'
|
||||
|
527
frontend/src/Projects/Project/Board/index.tsx
Normal file
527
frontend/src/Projects/Project/Board/index.tsx
Normal 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;
|
@ -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={() => {}}
|
||||
|
@ -1,6 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Board = styled.div`
|
||||
margin-top: 12px;
|
||||
margin-left: 8px;
|
||||
`;
|
@ -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;
|
154
frontend/src/Projects/Project/LabelManagerEditor/index.tsx
Normal file
154
frontend/src/Projects/Project/LabelManagerEditor/index.tsx
Normal 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
|
@ -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}
|
||||
|
@ -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 can’t 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();
|
||||
}
|
||||
}
|
||||
|
6
frontend/src/citadel.d.ts
vendored
6
frontend/src/citadel.d.ts
vendored
@ -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 = {
|
||||
|
@ -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 can’t 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>
|
||||
|
@ -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});
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
@ -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;
|
@ -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
|
||||
}
|
||||
|
@ -14,6 +14,10 @@ export const GET_TEAM_QUERY = gql`
|
||||
code
|
||||
name
|
||||
}
|
||||
owned {
|
||||
projects
|
||||
teams
|
||||
}
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
|
17
frontend/src/shared/graphql/user/updateUserRole.ts
Normal file
17
frontend/src/shared/graphql/user/updateUserRole.ts
Normal 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;
|
@ -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
|
||||
|
@ -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
|
||||
`
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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 *;
|
||||
|
@ -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 *;
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
@ -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 (
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user