feat: add task activity
This commit is contained in:
parent
f732b211c9
commit
19deab0515
@ -21,4 +21,4 @@ windows:
|
|||||||
- database:
|
- database:
|
||||||
root: ./
|
root: ./
|
||||||
panes:
|
panes:
|
||||||
- pgcli postgres://taskcafe:taskcafe_test@localhost:5432/taskcafe
|
- pgcli postgres://taskcafe:taskcafe_test@localhost:8855/taskcafe
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
"@types/axios": "^0.14.0",
|
"@types/axios": "^0.14.0",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
"@types/date-fns": "^2.6.0",
|
"@types/date-fns": "^2.6.0",
|
||||||
|
"@types/dompurify": "^2.0.4",
|
||||||
|
"@types/emoji-mart": "^3.0.4",
|
||||||
"@types/jest": "^24.0.0",
|
"@types/jest": "^24.0.0",
|
||||||
"@types/jwt-decode": "^2.2.1",
|
"@types/jwt-decode": "^2.2.1",
|
||||||
"@types/lodash": "^4.14.149",
|
"@types/lodash": "^4.14.149",
|
||||||
@ -35,12 +37,16 @@
|
|||||||
"color": "^3.1.2",
|
"color": "^3.1.2",
|
||||||
"date-fns": "^2.14.0",
|
"date-fns": "^2.14.0",
|
||||||
"dayjs": "^1.9.1",
|
"dayjs": "^1.9.1",
|
||||||
|
"dompurify": "^2.2.6",
|
||||||
|
"emoji-mart": "^3.0.0",
|
||||||
|
"emoticon": "^3.2.0",
|
||||||
"graphql": "^15.0.0",
|
"graphql": "^15.0.0",
|
||||||
"graphql-tag": "^2.10.3",
|
"graphql-tag": "^2.10.3",
|
||||||
"history": "^4.10.1",
|
"history": "^4.10.1",
|
||||||
"immer": "^6.0.3",
|
"immer": "^6.0.3",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
"node-emoji": "^1.10.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"query-string": "^6.13.7",
|
"query-string": "^6.13.7",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
@ -48,6 +54,7 @@
|
|||||||
"react-beautiful-dnd": "^13.0.0",
|
"react-beautiful-dnd": "^13.0.0",
|
||||||
"react-datepicker": "^2.14.1",
|
"react-datepicker": "^2.14.1",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
|
"react-emoji-render": "^1.2.4",
|
||||||
"react-hook-form": "^6.0.6",
|
"react-hook-form": "^6.0.6",
|
||||||
"react-markdown": "^4.3.1",
|
"react-markdown": "^4.3.1",
|
||||||
"react-router": "^5.1.2",
|
"react-router": "^5.1.2",
|
||||||
|
@ -182,7 +182,7 @@ const AdminRoute = () => {
|
|||||||
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.invitedUsers = cache.invitedUsers.filter(
|
draftCache.invitedUsers = cache.invitedUsers.filter(
|
||||||
u => u.id !== response.data.deleteInvitedUserAccount.invitedUser.id,
|
u => u.id !== response.data?.deleteInvitedUserAccount.invitedUser.id,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -192,7 +192,7 @@ const AdminRoute = () => {
|
|||||||
update: (client, response) => {
|
update: (client, response) => {
|
||||||
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.users = cache.users.filter(u => u.id !== response.data.deleteUserAccount.userAccount.id);
|
draftCache.users = cache.users.filter(u => u.id !== response.data?.deleteUserAccount.userAccount.id);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -203,7 +203,7 @@ const AdminRoute = () => {
|
|||||||
query: UsersDocument,
|
query: UsersDocument,
|
||||||
});
|
});
|
||||||
const newData = produce(cacheData, (draftState: any) => {
|
const newData = produce(cacheData, (draftState: any) => {
|
||||||
draftState.users = [...draftState.users, { ...createData.data.createUserAccount }];
|
draftState.users = [...draftState.users, { ...createData.data?.createUserAccount }];
|
||||||
});
|
});
|
||||||
|
|
||||||
client.writeQuery({
|
client.writeQuery({
|
||||||
|
@ -25,6 +25,11 @@ const MainContent = styled.div`
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
type RefreshTokenResponse = {
|
||||||
|
accessToken: string;
|
||||||
|
setup?: null | { confirmToken: string };
|
||||||
|
};
|
||||||
|
|
||||||
const AuthorizedRoutes = () => {
|
const AuthorizedRoutes = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
@ -167,7 +167,7 @@ const ProjectFinder = () => {
|
|||||||
return <span>error</span>;
|
return <span>error</span>;
|
||||||
};
|
};
|
||||||
type ProjectPopupProps = {
|
type ProjectPopupProps = {
|
||||||
history: History<History.PoorMansUnknown>;
|
history: any;
|
||||||
name: string;
|
name: string;
|
||||||
projectID: string;
|
projectID: string;
|
||||||
};
|
};
|
||||||
@ -182,7 +182,7 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
|
|||||||
|
|
||||||
const newData = produce(cacheData, (draftState: any) => {
|
const newData = produce(cacheData, (draftState: any) => {
|
||||||
draftState.projects = draftState.projects.filter(
|
draftState.projects = draftState.projects.filter(
|
||||||
(project: any) => project.id !== deleteData.data.deleteProject.project.id,
|
(project: any) => project.id !== deleteData.data?.deleteProject.project.id,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -46,10 +46,6 @@ const StyledContainer = styled(ToastContainer).attrs({
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
type RefreshTokenResponse = {
|
|
||||||
accessToken: string;
|
|
||||||
isInstalled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [user, setUser] = useState<CurrentUserRaw | null>(null);
|
const [user, setUser] = useState<CurrentUserRaw | null>(null);
|
||||||
|
@ -280,7 +280,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
|
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
|
||||||
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
|
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data?.deleteTaskGroup.taskGroup.id,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
@ -296,10 +296,12 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
const { taskGroups } = cache.findProject;
|
const { taskGroups } = cache.findProject;
|
||||||
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data.createTask.taskGroup.id);
|
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data?.createTask.taskGroup.id);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
|
if (newTaskData.data) {
|
||||||
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
|
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
@ -313,7 +315,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
|
if (newTaskGroupData.data) {
|
||||||
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
|
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
@ -332,7 +336,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
const idx = cache.findProject.taskGroups.findIndex(
|
const idx = cache.findProject.taskGroups.findIndex(
|
||||||
t => t.id === resp.data.deleteTaskGroupTasks.taskGroupID,
|
t => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
|
||||||
);
|
);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
draftCache.findProject.taskGroups[idx].tasks = [];
|
draftCache.findProject.taskGroups[idx].tasks = [];
|
||||||
@ -348,7 +352,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
|
if (resp.data) {
|
||||||
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
|
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
@ -364,21 +370,26 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
|
if (newTask.data) {
|
||||||
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
|
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
|
||||||
if (previousTaskGroupID !== task.taskGroup.id) {
|
if (previousTaskGroupID !== task.taskGroup.id) {
|
||||||
const { taskGroups } = cache.findProject;
|
const { taskGroups } = cache.findProject;
|
||||||
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
|
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
|
||||||
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
|
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
|
||||||
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
|
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
|
||||||
|
const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(t => t.id === task.id);
|
||||||
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
|
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
|
||||||
(t: Task) => t.id !== task.id,
|
(t: Task) => t.id !== task.id,
|
||||||
);
|
);
|
||||||
|
if (previousTask) {
|
||||||
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
|
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
|
||||||
...taskGroups[newTaskGroupIdx].tasks,
|
...taskGroups[newTaskGroupIdx].tasks,
|
||||||
{ ...task },
|
{ ...previousTask },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
|
@ -21,6 +21,9 @@ import {
|
|||||||
useCreateTaskChecklistItemMutation,
|
useCreateTaskChecklistItemMutation,
|
||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
FindTaskQuery,
|
FindTaskQuery,
|
||||||
|
useCreateTaskCommentMutation,
|
||||||
|
useDeleteTaskCommentMutation,
|
||||||
|
useUpdateTaskCommentMutation,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
import MiniProfile from 'shared/components/MiniProfile';
|
import MiniProfile from 'shared/components/MiniProfile';
|
||||||
@ -33,6 +36,73 @@ import { useForm } from 'react-hook-form';
|
|||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
|
|
||||||
|
export const ActionsList = styled.ul`
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ActionItem = styled.li`
|
||||||
|
position: relative;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
&:hover {
|
||||||
|
background: ${props => props.theme.colors.primary};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ActionTitle = styled.span`
|
||||||
|
margin-left: 20px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WarningLabel = styled.p`
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 8px 12px;
|
||||||
|
`;
|
||||||
|
const DeleteConfirm = styled(Button)`
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type TaskCommentActionsProps = {
|
||||||
|
onDeleteComment: () => void;
|
||||||
|
onEditComment: () => void;
|
||||||
|
};
|
||||||
|
const TaskCommentActions: React.FC<TaskCommentActionsProps> = ({ onDeleteComment, onEditComment }) => {
|
||||||
|
const { setTab } = usePopup();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popup tab={0} title={null}>
|
||||||
|
<ActionsList>
|
||||||
|
<ActionItem>
|
||||||
|
<ActionTitle>Pin to top</ActionTitle>
|
||||||
|
</ActionItem>
|
||||||
|
<ActionItem onClick={() => onEditComment()}>
|
||||||
|
<ActionTitle>Edit comment</ActionTitle>
|
||||||
|
</ActionItem>
|
||||||
|
<ActionItem onClick={() => setTab(1)}>
|
||||||
|
<ActionTitle>Delete comment</ActionTitle>
|
||||||
|
</ActionItem>
|
||||||
|
</ActionsList>
|
||||||
|
</Popup>
|
||||||
|
<Popup tab={1} title="Delete comment?">
|
||||||
|
<WarningLabel>Deleting a comment can not be undone.</WarningLabel>
|
||||||
|
<DeleteConfirm onClick={() => onDeleteComment()} color="danger">
|
||||||
|
Delete comment
|
||||||
|
</DeleteConfirm>
|
||||||
|
</Popup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => {
|
const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => {
|
||||||
const total = checklists.reduce((prev: any, next: any) => {
|
const total = checklists.reduce((prev: any, next: any) => {
|
||||||
return (
|
return (
|
||||||
@ -130,6 +200,40 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const [deleteTaskComment] = useDeleteTaskCommentMutation({
|
||||||
|
update: (client, response) => {
|
||||||
|
updateApolloCache<FindTaskQuery>(
|
||||||
|
client,
|
||||||
|
FindTaskDocument,
|
||||||
|
cache =>
|
||||||
|
produce(cache, draftCache => {
|
||||||
|
if (response.data) {
|
||||||
|
draftCache.findTask.comments = cache.findTask.comments.filter(
|
||||||
|
c => c.id !== response.data?.deleteTaskComment.commentID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ taskID },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [createTaskComment] = useCreateTaskCommentMutation({
|
||||||
|
update: (client, response) => {
|
||||||
|
updateApolloCache<FindTaskQuery>(
|
||||||
|
client,
|
||||||
|
FindTaskDocument,
|
||||||
|
cache =>
|
||||||
|
produce(cache, draftCache => {
|
||||||
|
if (response.data) {
|
||||||
|
draftCache.findTask.comments.push({
|
||||||
|
...response.data.createTaskComment.comment,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ taskID },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
const [updateTaskChecklistLocation] = useUpdateTaskChecklistLocationMutation();
|
const [updateTaskChecklistLocation] = useUpdateTaskChecklistLocationMutation();
|
||||||
const [updateTaskChecklistItemLocation] = useUpdateTaskChecklistItemLocationMutation({
|
const [updateTaskChecklistItemLocation] = useUpdateTaskChecklistItemLocationMutation({
|
||||||
update: (client, response) => {
|
update: (client, response) => {
|
||||||
@ -138,10 +242,11 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
const { prevChecklistID, checklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
|
if (response.data) {
|
||||||
if (checklistID !== prevChecklistID) {
|
const { prevChecklistID, taskChecklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
|
||||||
|
if (taskChecklistID !== prevChecklistID) {
|
||||||
const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID);
|
const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID);
|
||||||
const newIdx = cache.findTask.checklists.findIndex(c => c.id === checklistID);
|
const newIdx = cache.findTask.checklists.findIndex(c => c.id === taskChecklistID);
|
||||||
if (oldIdx > -1 && newIdx > -1) {
|
if (oldIdx > -1 && newIdx > -1) {
|
||||||
const item = cache.findTask.checklists[oldIdx].items.find(i => i.id === checklistItem.id);
|
const item = cache.findTask.checklists[oldIdx].items.find(i => i.id === checklistItem.id);
|
||||||
if (item) {
|
if (item) {
|
||||||
@ -151,11 +256,12 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
draftCache.findTask.checklists[newIdx].items.push({
|
draftCache.findTask.checklists[newIdx].items.push({
|
||||||
...item,
|
...item,
|
||||||
position: checklistItem.position,
|
position: checklistItem.position,
|
||||||
taskChecklistID: checklistID,
|
taskChecklistID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{ taskID },
|
{ taskID },
|
||||||
);
|
);
|
||||||
@ -188,7 +294,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
const { checklists } = cache.findTask;
|
const { checklists } = cache.findTask;
|
||||||
draftCache.findTask.checklists = checklists.filter(
|
draftCache.findTask.checklists = checklists.filter(
|
||||||
c => c.id !== deleteData.data.deleteTaskChecklist.taskChecklist.id,
|
c => c.id !== deleteData.data?.deleteTaskChecklist.taskChecklist.id,
|
||||||
);
|
);
|
||||||
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
||||||
draftCache.findTask.badges.checklist = {
|
draftCache.findTask.badges.checklist = {
|
||||||
@ -212,8 +318,10 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
|
if (createData.data) {
|
||||||
const item = createData.data.createTaskChecklist;
|
const item = createData.data.createTaskChecklist;
|
||||||
draftCache.findTask.checklists.push({ ...item });
|
draftCache.findTask.checklists.push({ ...item });
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{ taskID },
|
{ taskID },
|
||||||
);
|
);
|
||||||
@ -227,6 +335,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
|
if (deleteData.data) {
|
||||||
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
|
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
|
||||||
const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID);
|
const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID);
|
||||||
if (targetIdx > -1) {
|
if (targetIdx > -1) {
|
||||||
@ -240,6 +349,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
complete,
|
complete,
|
||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{ taskID },
|
{ taskID },
|
||||||
);
|
);
|
||||||
@ -252,6 +362,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
|
if (newTaskItem.data) {
|
||||||
const item = newTaskItem.data.createTaskChecklistItem;
|
const item = newTaskItem.data.createTaskChecklistItem;
|
||||||
const { checklists } = cache.findTask;
|
const { checklists } = cache.findTask;
|
||||||
const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
|
const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
|
||||||
@ -264,12 +375,13 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{ taskID },
|
{ taskID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
|
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID }, fetchPolicy: 'cache-and-network' });
|
||||||
const [setTaskComplete] = useSetTaskCompleteMutation();
|
const [setTaskComplete] = useSetTaskCompleteMutation();
|
||||||
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
|
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
@ -289,6 +401,8 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
refreshCache();
|
refreshCache();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const [updateTaskComment] = useUpdateTaskCommentMutation();
|
||||||
|
const [editableComment, setEditableComment] = useState<null | string>(null);
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -305,8 +419,31 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
renderContent={() => {
|
renderContent={() => {
|
||||||
return (
|
return (
|
||||||
<TaskDetails
|
<TaskDetails
|
||||||
|
onCancelCommentEdit={() => setEditableComment(null)}
|
||||||
|
onUpdateComment={(commentID, message) => {
|
||||||
|
updateTaskComment({ variables: { commentID, message } });
|
||||||
|
}}
|
||||||
|
editableComment={editableComment}
|
||||||
me={data.me.user}
|
me={data.me.user}
|
||||||
|
onCommentShowActions={(commentID, $targetRef) => {
|
||||||
|
showPopup(
|
||||||
|
$targetRef,
|
||||||
|
<TaskCommentActions
|
||||||
|
onDeleteComment={() => {
|
||||||
|
deleteTaskComment({ variables: { commentID } });
|
||||||
|
hidePopup();
|
||||||
|
}}
|
||||||
|
onEditComment={() => {
|
||||||
|
setEditableComment(commentID);
|
||||||
|
hidePopup();
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}}
|
||||||
task={data.findTask}
|
task={data.findTask}
|
||||||
|
onCreateComment={(task, message) => {
|
||||||
|
createTaskComment({ variables: { taskID: task.id, message } });
|
||||||
|
}}
|
||||||
onChecklistDrop={checklist => {
|
onChecklistDrop={checklist => {
|
||||||
updateTaskChecklistLocation({
|
updateTaskChecklistLocation({
|
||||||
variables: { taskChecklistID: checklist.id, position: checklist.position },
|
variables: { taskChecklistID: checklist.id, position: checklist.position },
|
||||||
|
@ -36,7 +36,9 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
|
if (newLabelData.data) {
|
||||||
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
|
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
projectID,
|
projectID,
|
||||||
@ -53,7 +55,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.labels = cache.findProject.labels.filter(
|
draftCache.findProject.labels = cache.findProject.labels.filter(
|
||||||
label => label.id !== newLabelData.data.deleteProjectLabel.id,
|
label => label.id !== newLabelData.data?.deleteProjectLabel.id,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
|
@ -32,7 +32,6 @@ import {
|
|||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
FindProjectQuery,
|
FindProjectQuery,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
|
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import UserContext, { useCurrentUser } from 'App/context';
|
import UserContext, { useCurrentUser } from 'App/context';
|
||||||
import Input from 'shared/components/Input';
|
import Input from 'shared/components/Input';
|
||||||
@ -135,7 +134,6 @@ type MemberFilterOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchMembers = async (client: any, projectID: string, options: MemberFilterOptions, input: string, cb: any) => {
|
const fetchMembers = async (client: any, projectID: string, options: MemberFilterOptions, input: string, cb: any) => {
|
||||||
console.log(input.trim().length < 3);
|
|
||||||
if (input && input.trim().length < 3) {
|
if (input && input.trim().length < 3) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -163,12 +161,10 @@ const fetchMembers = async (client: any, projectID: string, options: MemberFilte
|
|||||||
|
|
||||||
let results: any = [];
|
let results: any = [];
|
||||||
const emails: Array<string> = [];
|
const emails: Array<string> = [];
|
||||||
console.log(res.data && res.data.searchMembers);
|
|
||||||
if (res.data && res.data.searchMembers) {
|
if (res.data && res.data.searchMembers) {
|
||||||
results = [
|
results = [
|
||||||
...res.data.searchMembers.map((m: any) => {
|
...res.data.searchMembers.map((m: any) => {
|
||||||
if (m.status === 'INVITED') {
|
if (m.status === 'INVITED') {
|
||||||
console.log(`${m.id} is added`);
|
|
||||||
return {
|
return {
|
||||||
label: m.id,
|
label: m.id,
|
||||||
value: {
|
value: {
|
||||||
@ -180,17 +176,15 @@ const fetchMembers = async (client: any, projectID: string, options: MemberFilte
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
}
|
||||||
console.log(`${m.user.email} is added`);
|
|
||||||
emails.push(m.user.email);
|
emails.push(m.user.email);
|
||||||
return {
|
return {
|
||||||
label: m.user.fullName,
|
label: m.user.fullName,
|
||||||
value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
|
value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
console.log(results);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RFC2822_EMAIL.test(input) && !emails.find(e => e === input)) {
|
if (RFC2822_EMAIL.test(input) && !emails.find(e => e === input)) {
|
||||||
@ -243,7 +237,6 @@ const OptionLabel = styled.span<{ fontSize: number; quiet: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const UserOption: React.FC<UserOptionProps> = ({ isDisabled, isFocused, innerProps, label, data }) => {
|
const UserOption: React.FC<UserOptionProps> = ({ isDisabled, isFocused, innerProps, label, data }) => {
|
||||||
console.log(data);
|
|
||||||
return !isDisabled ? (
|
return !isDisabled ? (
|
||||||
<OptionWrapper {...innerProps} isFocused={isFocused}>
|
<OptionWrapper {...innerProps} isFocused={isFocused}>
|
||||||
<TaskAssignee
|
<TaskAssignee
|
||||||
@ -423,14 +416,16 @@ const Project = () => {
|
|||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
|
if (resp.data) {
|
||||||
const taskGroupIdx = draftCache.findProject.taskGroups.findIndex(
|
const taskGroupIdx = draftCache.findProject.taskGroups.findIndex(
|
||||||
tg => tg.tasks.findIndex(t => t.id === resp.data.deleteTask.taskID) !== -1,
|
tg => tg.tasks.findIndex(t => t.id === resp.data?.deleteTask.taskID) !== -1,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (taskGroupIdx !== -1) {
|
if (taskGroupIdx !== -1) {
|
||||||
draftCache.findProject.taskGroups[taskGroupIdx].tasks = cache.findProject.taskGroups[
|
draftCache.findProject.taskGroups[taskGroupIdx].tasks = cache.findProject.taskGroups[
|
||||||
taskGroupIdx
|
taskGroupIdx
|
||||||
].tasks.filter(t => t.id !== resp.data.deleteTask.taskID);
|
].tasks.filter(t => t.id !== resp.data?.deleteTask.taskID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
@ -450,7 +445,7 @@ const Project = () => {
|
|||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.name = newName.data.updateProjectName.name;
|
draftCache.findProject.name = newName.data?.updateProjectName.name ?? '';
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
@ -464,6 +459,7 @@ const Project = () => {
|
|||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
|
if (response.data) {
|
||||||
draftCache.findProject.members = [
|
draftCache.findProject.members = [
|
||||||
...cache.findProject.members,
|
...cache.findProject.members,
|
||||||
...response.data.inviteProjectMembers.members,
|
...response.data.inviteProjectMembers.members,
|
||||||
@ -472,6 +468,7 @@ const Project = () => {
|
|||||||
...cache.findProject.invitedMembers,
|
...cache.findProject.invitedMembers,
|
||||||
...response.data.inviteProjectMembers.invitedMembers,
|
...response.data.inviteProjectMembers.invitedMembers,
|
||||||
];
|
];
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
@ -485,7 +482,7 @@ const Project = () => {
|
|||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.invitedMembers = cache.findProject.invitedMembers.filter(
|
draftCache.findProject.invitedMembers = cache.findProject.invitedMembers.filter(
|
||||||
m => m.email !== response.data.deleteInvitedProjectMember.invitedMember.email,
|
m => m.email !== response.data?.deleteInvitedProjectMember.invitedMember.email ?? '',
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
@ -500,7 +497,7 @@ const Project = () => {
|
|||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.members = cache.findProject.members.filter(
|
draftCache.findProject.members = cache.findProject.members.filter(
|
||||||
m => m.id !== response.data.deleteProjectMember.member.id,
|
m => m.id !== response.data?.deleteProjectMember.member.id,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
|
@ -210,7 +210,9 @@ const Projects = () => {
|
|||||||
update: (client, newProject) => {
|
update: (client, newProject) => {
|
||||||
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
|
if (newProject.data) {
|
||||||
draftCache.projects.push({ ...newProject.data.createProject });
|
draftCache.projects.push({ ...newProject.data.createProject });
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -222,7 +224,9 @@ const Projects = () => {
|
|||||||
update: (client, createData) => {
|
update: (client, createData) => {
|
||||||
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.teams.push({ ...createData.data.createTeam });
|
if (createData.data) {
|
||||||
|
draftCache.teams.push({ ...createData.data?.createTeam });
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -430,11 +430,13 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
GetTeamDocument,
|
GetTeamDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
|
if (response.data) {
|
||||||
draftCache.findTeam.members.push({
|
draftCache.findTeam.members.push({
|
||||||
...response.data.createTeamMember.teamMember,
|
...response.data.createTeamMember.teamMember,
|
||||||
member: { __typename: 'MemberList', projects: [], teams: [] },
|
member: { __typename: 'MemberList', projects: [], teams: [] },
|
||||||
owned: { __typename: 'OwnedList', projects: [], teams: [] },
|
owned: { __typename: 'OwnedList', projects: [], teams: [] },
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{ teamID },
|
{ teamID },
|
||||||
);
|
);
|
||||||
@ -459,7 +461,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findTeam.members = cache.findTeam.members.filter(
|
draftCache.findTeam.members = cache.findTeam.members.filter(
|
||||||
member => member.id !== response.data.deleteTeamMember.userID,
|
member => member.id !== response.data?.deleteTeamMember.userID,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ teamID },
|
{ teamID },
|
||||||
|
@ -33,7 +33,7 @@ const Wrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type TeamPopupProps = {
|
type TeamPopupProps = {
|
||||||
history: History<History.PoorMansUnknown>;
|
history: History<any>;
|
||||||
name: string;
|
name: string;
|
||||||
teamID: string;
|
teamID: string;
|
||||||
};
|
};
|
||||||
@ -44,9 +44,9 @@ export const TeamPopup: React.FC<TeamPopupProps> = ({ history, name, teamID }) =
|
|||||||
update: (client, deleteData) => {
|
update: (client, deleteData) => {
|
||||||
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
draftCache.teams = cache.teams.filter((team: any) => team.id !== deleteData.data.deleteTeam.team.id);
|
draftCache.teams = cache.teams.filter((team: any) => team.id !== deleteData.data?.deleteTeam.team.id);
|
||||||
draftCache.projects = cache.projects.filter(
|
draftCache.projects = cache.projects.filter(
|
||||||
(project: any) => project.team.id !== deleteData.data.deleteTeam.team.id,
|
(project: any) => project.team.id !== deleteData.data?.deleteTeam.team.id,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -263,7 +263,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
|
|||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
setTeam(e.value);
|
setTeam(e.value);
|
||||||
}}
|
}}
|
||||||
value={options.filter(d => d.value === team)}
|
value={options.find(d => d.value === team)}
|
||||||
styles={colourStyles}
|
styles={colourStyles}
|
||||||
classNamePrefix="teamSelect"
|
classNamePrefix="teamSelect"
|
||||||
options={options}
|
options={options}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { TaskActivityData, ActivityType } from 'shared/generated/graphql';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
type ActivityMessageProps = {
|
||||||
|
type: ActivityType;
|
||||||
|
data: Array<TaskActivityData>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getVariable(data: Array<TaskActivityData>, name: string) {
|
||||||
|
const target = data.find(d => d.name === name);
|
||||||
|
return target ? target.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDate(timestamp: string | null) {
|
||||||
|
if (timestamp) {
|
||||||
|
return dayjs(timestamp).format('MMM D [at] h:mm A');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActivityMessage: React.FC<ActivityMessageProps> = ({ type, data }) => {
|
||||||
|
let message = '';
|
||||||
|
switch (type) {
|
||||||
|
case ActivityType.TaskAdded:
|
||||||
|
message = `added this task to ${getVariable(data, 'TaskGroup')}`;
|
||||||
|
break;
|
||||||
|
case ActivityType.TaskMoved:
|
||||||
|
message = `moved this task from ${getVariable(data, 'PrevTaskGroup')} to ${getVariable(data, 'CurTaskGroup')}`;
|
||||||
|
break;
|
||||||
|
case ActivityType.TaskDueDateAdded:
|
||||||
|
message = `set this task to be due ${renderDate(getVariable(data, 'DueDate'))}`;
|
||||||
|
break;
|
||||||
|
case ActivityType.TaskDueDateRemoved:
|
||||||
|
message = `removed the due date from this task`;
|
||||||
|
break;
|
||||||
|
case ActivityType.TaskDueDateChanged:
|
||||||
|
message = `changed the due date of this task to ${renderDate(getVariable(data, 'CurDueDate'))}`;
|
||||||
|
break;
|
||||||
|
case ActivityType.TaskMarkedComplete:
|
||||||
|
message = `marked this task complete`;
|
||||||
|
break;
|
||||||
|
case ActivityType.TaskMarkedIncomplete:
|
||||||
|
message = `marked this task incomplete`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
message = '<unknown type>';
|
||||||
|
}
|
||||||
|
return <>{message}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActivityMessage;
|
133
frontend/src/shared/components/TaskDetails/CommentCreator.tsx
Normal file
133
frontend/src/shared/components/TaskDetails/CommentCreator.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
CommentTextArea,
|
||||||
|
CommentEditorContainer,
|
||||||
|
CommentEditorActions,
|
||||||
|
CommentEditorActionIcon,
|
||||||
|
CommentEditorSaveButton,
|
||||||
|
CommentProfile,
|
||||||
|
CommentInnerWrapper,
|
||||||
|
} from './Styles';
|
||||||
|
import { usePopup } from 'shared/components/PopupMenu';
|
||||||
|
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||||
|
import { At, Paperclip, Smile } from 'shared/icons';
|
||||||
|
import { Picker, Emoji } from 'emoji-mart';
|
||||||
|
import Task from 'shared/icons/Task';
|
||||||
|
|
||||||
|
type CommentCreatorProps = {
|
||||||
|
me?: TaskUser;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||||
|
message?: string | null;
|
||||||
|
onCreateComment: (message: string) => void;
|
||||||
|
onCancelEdit?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommentCreator: React.FC<CommentCreatorProps> = ({
|
||||||
|
me,
|
||||||
|
message,
|
||||||
|
onMemberProfile,
|
||||||
|
onCreateComment,
|
||||||
|
onCancelEdit,
|
||||||
|
autoFocus = false,
|
||||||
|
}) => {
|
||||||
|
const $commentWrapper = useRef<HTMLDivElement>(null);
|
||||||
|
const $comment = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const $emoji = useRef<HTMLDivElement>(null);
|
||||||
|
const $emojiCart = useRef<HTMLDivElement>(null);
|
||||||
|
const [comment, setComment] = useState(message ?? '');
|
||||||
|
const [showCommentActions, setShowCommentActions] = useState(autoFocus);
|
||||||
|
const { showPopup, hidePopup } = usePopup();
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFocus && $comment && $comment.current) {
|
||||||
|
$comment.current.select();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
useOnOutsideClick(
|
||||||
|
[$commentWrapper, $emojiCart],
|
||||||
|
showCommentActions,
|
||||||
|
() => {
|
||||||
|
if (onCancelEdit) {
|
||||||
|
onCancelEdit();
|
||||||
|
}
|
||||||
|
setShowCommentActions(false);
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<CommentInnerWrapper ref={$commentWrapper}>
|
||||||
|
{me && onMemberProfile && (
|
||||||
|
<CommentProfile
|
||||||
|
member={me}
|
||||||
|
size={32}
|
||||||
|
onMemberProfile={$target => {
|
||||||
|
onMemberProfile($target, me.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<CommentEditorContainer>
|
||||||
|
<CommentTextArea
|
||||||
|
showCommentActions={showCommentActions}
|
||||||
|
placeholder="Write a comment..."
|
||||||
|
ref={$comment}
|
||||||
|
value={comment}
|
||||||
|
onChange={e => setComment(e.currentTarget.value)}
|
||||||
|
onFocus={() => {
|
||||||
|
setShowCommentActions(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CommentEditorActions visible={showCommentActions}>
|
||||||
|
<CommentEditorActionIcon>
|
||||||
|
<Paperclip width={12} height={12} />
|
||||||
|
</CommentEditorActionIcon>
|
||||||
|
<CommentEditorActionIcon>
|
||||||
|
<At width={12} height={12} />
|
||||||
|
</CommentEditorActionIcon>
|
||||||
|
<CommentEditorActionIcon
|
||||||
|
ref={$emoji}
|
||||||
|
onClick={() => {
|
||||||
|
showPopup(
|
||||||
|
$emoji,
|
||||||
|
<div ref={$emojiCart}>
|
||||||
|
<Picker
|
||||||
|
onClick={emoji => {
|
||||||
|
console.log(emoji);
|
||||||
|
if ($comment && $comment.current) {
|
||||||
|
let textToInsert = `${emoji.colons} `;
|
||||||
|
let cursorPosition = $comment.current.selectionStart;
|
||||||
|
let textBeforeCursorPosition = $comment.current.value.substring(0, cursorPosition);
|
||||||
|
let textAfterCursorPosition = $comment.current.value.substring(
|
||||||
|
cursorPosition,
|
||||||
|
$comment.current.value.length,
|
||||||
|
);
|
||||||
|
setComment(textBeforeCursorPosition + textToInsert + textAfterCursorPosition);
|
||||||
|
}
|
||||||
|
hidePopup();
|
||||||
|
}}
|
||||||
|
set="google"
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Smile width={12} height={12} />
|
||||||
|
</CommentEditorActionIcon>
|
||||||
|
<CommentEditorActionIcon>
|
||||||
|
<Task width={12} height={12} />
|
||||||
|
</CommentEditorActionIcon>
|
||||||
|
<CommentEditorSaveButton
|
||||||
|
onClick={() => {
|
||||||
|
setShowCommentActions(false);
|
||||||
|
onCreateComment(comment);
|
||||||
|
setComment('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</CommentEditorSaveButton>
|
||||||
|
</CommentEditorActions>
|
||||||
|
</CommentEditorContainer>
|
||||||
|
</CommentInnerWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentCreator;
|
@ -475,6 +475,7 @@ export const CommentEditorContainer = styled.div`
|
|||||||
border: 1px solid #414561;
|
border: 1px solid #414561;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
background: #1f243e;
|
||||||
`;
|
`;
|
||||||
export const CommentProfile = styled(TaskAssignee)`
|
export const CommentProfile = styled(TaskAssignee)`
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
@ -484,7 +485,7 @@ export const CommentProfile = styled(TaskAssignee)`
|
|||||||
align-items: normal;
|
align-items: normal;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CommentTextArea = styled(TextareaAutosize)`
|
export const CommentTextArea = styled(TextareaAutosize)<{ showCommentActions: boolean }>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
@ -495,14 +496,16 @@ export const CommentTextArea = styled(TextareaAutosize)`
|
|||||||
transition: max-height 200ms, height 200ms, min-height 200ms;
|
transition: max-height 200ms, height 200ms, min-height 200ms;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
max-height: 36px;
|
max-height: 36px;
|
||||||
&:not(:focus) {
|
${props =>
|
||||||
height: 36px;
|
props.showCommentActions
|
||||||
}
|
? css`
|
||||||
&:focus {
|
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
`
|
||||||
|
: css`
|
||||||
|
height: 36px;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CommentEditorActions = styled.div<{ visible: boolean }>`
|
export const CommentEditorActions = styled.div<{ visible: boolean }>`
|
||||||
@ -529,6 +532,18 @@ export const ActivitySection = styled.div`
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
padding: 8px 26px;
|
padding: 8px 26px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ActivityItemCommentAction = styled.div`
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
svg {
|
||||||
|
fill: ${props => props.theme.colors.text.primary} !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActivityItem = styled.div`
|
export const ActivityItem = styled.div`
|
||||||
@ -537,25 +552,32 @@ export const ActivityItem = styled.div`
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
display: flex;
|
||||||
|
&:hover ${ActivityItemCommentAction} {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActivityItemHeader = styled.div`
|
export const ActivityItemHeader = styled.div<{ editable?: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: 8px;
|
||||||
|
${props => props.editable && 'width: 100%;'}
|
||||||
`;
|
`;
|
||||||
export const ActivityItemHeaderUser = styled(TaskAssignee)`
|
export const ActivityItemHeaderUser = styled(TaskAssignee)`
|
||||||
margin-right: 4px;
|
align-items: start;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActivityItemHeaderTitle = styled.div`
|
export const ActivityItemHeaderTitle = styled.div`
|
||||||
margin-left: 4px;
|
|
||||||
line-height: 18px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: ${props => props.theme.colors.text.primary};
|
||||||
|
padding-bottom: 2px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActivityItemHeaderTitleName = styled.span`
|
export const ActivityItemHeaderTitleName = styled.span`
|
||||||
color: ${props => props.theme.colors.text.primary};
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
padding-right: 3px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActivityItemTimestamp = styled.span<{ margin: number }>`
|
export const ActivityItemTimestamp = styled.span<{ margin: number }>`
|
||||||
@ -568,8 +590,10 @@ export const ActivityItemDetails = styled.div`
|
|||||||
margin-left: 32px;
|
margin-left: 32px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActivityItemComment = styled.div`
|
export const ActivityItemCommentContainer = styled.div``;
|
||||||
|
export const ActivityItemComment = styled.div<{ editable: boolean }>`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
${mixin.boxShadowCard}
|
${mixin.boxShadowCard}
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -577,6 +601,32 @@ export const ActivityItemComment = styled.div`
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
background-color: ${props => mixin.darken(props.theme.colors.alternate, 0.1)};
|
background-color: ${props => mixin.darken(props.theme.colors.alternate, 0.1)};
|
||||||
|
${props => props.editable && 'width: 100%;'}
|
||||||
|
|
||||||
|
& span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
& ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
& ul > li {
|
||||||
|
margin: 8px 8px 8px 24px;
|
||||||
|
margin-inline-start: 24px;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
|
& ul > li ul > li {
|
||||||
|
list-style: circle;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ActivityItemCommentActions = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActivityItemLog = styled.span`
|
export const ActivityItemLog = styled.span`
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import NormalizeStyles from 'App/NormalizeStyles';
|
|
||||||
import BaseStyles from 'App/BaseStyles';
|
|
||||||
import Modal from 'shared/components/Modal';
|
|
||||||
import TaskDetails from '.';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
component: TaskDetails,
|
|
||||||
title: 'TaskDetails',
|
|
||||||
parameters: {
|
|
||||||
backgrounds: [
|
|
||||||
{ name: 'white', value: '#ffffff' },
|
|
||||||
{ name: 'gray', value: '#cdd3e1', default: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Default = () => {
|
|
||||||
const [description, setDescription] = useState('');
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<NormalizeStyles />
|
|
||||||
<BaseStyles />
|
|
||||||
<Modal
|
|
||||||
width={1040}
|
|
||||||
onClose={action('on close')}
|
|
||||||
renderContent={() => {
|
|
||||||
return (
|
|
||||||
<TaskDetails
|
|
||||||
onDeleteItem={action('delete item')}
|
|
||||||
onChangeItemName={action('change item name')}
|
|
||||||
task={{
|
|
||||||
id: '1',
|
|
||||||
taskGroup: { name: 'General', id: '1' },
|
|
||||||
name: 'Hello, world',
|
|
||||||
position: 1,
|
|
||||||
labels: [
|
|
||||||
{
|
|
||||||
id: 'soft-skills',
|
|
||||||
assignedDate: new Date().toString(),
|
|
||||||
projectLabel: {
|
|
||||||
createdDate: new Date().toString(),
|
|
||||||
id: 'label-soft-skills',
|
|
||||||
name: 'Soft Skills',
|
|
||||||
labelColor: {
|
|
||||||
id: '1',
|
|
||||||
name: 'white',
|
|
||||||
colorHex: '#fff',
|
|
||||||
position: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
description,
|
|
||||||
assigned: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
profileIcon: { bgColor: null, url: null, initials: null },
|
|
||||||
fullName: 'Jordan Knott',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
onTaskNameChange={action('task name change')}
|
|
||||||
onTaskDescriptionChange={(_task, desc) => setDescription(desc)}
|
|
||||||
onDeleteTask={action('delete task')}
|
|
||||||
onCloseModal={action('close modal')}
|
|
||||||
onMemberProfile={action('profile')}
|
|
||||||
onOpenAddMemberPopup={action('open add member popup')}
|
|
||||||
onAddItem={action('add item')}
|
|
||||||
onToggleTaskComplete={action('toggle task complete')}
|
|
||||||
onToggleChecklistItem={action('toggle checklist item')}
|
|
||||||
onOpenAddLabelPopup={action('open add label popup')}
|
|
||||||
onChangeChecklistName={action('change checklist name')}
|
|
||||||
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')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -12,17 +12,32 @@ import {
|
|||||||
At,
|
At,
|
||||||
Smile,
|
Smile,
|
||||||
} from 'shared/icons';
|
} from 'shared/icons';
|
||||||
|
import { toArray } from 'react-emoji-render';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||||
|
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||||
|
import { usePopup } from 'shared/components/PopupMenu';
|
||||||
|
import CommentCreator from 'shared/components/TaskDetails/CommentCreator';
|
||||||
|
import { AngleDown } from 'shared/icons/AngleDown';
|
||||||
import Editor from 'rich-markdown-editor';
|
import Editor from 'rich-markdown-editor';
|
||||||
import dark from 'shared/utils/editorTheme';
|
import dark from 'shared/utils/editorTheme';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import { Picker, Emoji } from 'emoji-mart';
|
||||||
|
import 'emoji-mart/css/emoji-mart.css';
|
||||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import ActivityMessage from './ActivityMessage';
|
||||||
import Task from 'shared/icons/Task';
|
import Task from 'shared/icons/Task';
|
||||||
import {
|
import {
|
||||||
|
ActivityItemHeader,
|
||||||
|
ActivityItemTimestamp,
|
||||||
|
ActivityItem,
|
||||||
|
ActivityItemCommentAction,
|
||||||
|
ActivityItemCommentActions,
|
||||||
TaskDetailLabel,
|
TaskDetailLabel,
|
||||||
CommentContainer,
|
CommentContainer,
|
||||||
|
ActivityItemCommentContainer,
|
||||||
MetaDetailContent,
|
MetaDetailContent,
|
||||||
TaskDetailsAddLabelIcon,
|
TaskDetailsAddLabelIcon,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
@ -58,18 +73,126 @@ import {
|
|||||||
TaskMember,
|
TaskMember,
|
||||||
TabBarSection,
|
TabBarSection,
|
||||||
TabBarItem,
|
TabBarItem,
|
||||||
CommentTextArea,
|
|
||||||
CommentEditorContainer,
|
|
||||||
CommentEditorActions,
|
|
||||||
CommentEditorActionIcon,
|
|
||||||
CommentEditorSaveButton,
|
|
||||||
CommentProfile,
|
|
||||||
CommentInnerWrapper,
|
|
||||||
ActivitySection,
|
ActivitySection,
|
||||||
TaskDetailsEditor,
|
TaskDetailsEditor,
|
||||||
|
ActivityItemHeaderUser,
|
||||||
|
ActivityItemHeaderTitle,
|
||||||
|
ActivityItemHeaderTitleName,
|
||||||
|
ActivityItemComment,
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
|
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
|
||||||
import onDragEnd from './onDragEnd';
|
import onDragEnd from './onDragEnd';
|
||||||
|
import { plugin as em } from './remark';
|
||||||
|
|
||||||
|
const parseEmojis = (value: string) => {
|
||||||
|
const emojisArray = toArray(value);
|
||||||
|
|
||||||
|
// toArray outputs React elements for emojis and strings for other
|
||||||
|
const newValue = emojisArray.reduce((previous: any, current: any) => {
|
||||||
|
if (typeof current === 'string') {
|
||||||
|
return previous + current;
|
||||||
|
}
|
||||||
|
return previous + current.props.children;
|
||||||
|
}, '');
|
||||||
|
|
||||||
|
return newValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StreamCommentProps = {
|
||||||
|
comment?: TaskComment | null;
|
||||||
|
onUpdateComment: (message: string) => void;
|
||||||
|
onExtraActions: (commentID: string, $target: React.RefObject<HTMLElement>) => void;
|
||||||
|
onCancelCommentEdit: () => void;
|
||||||
|
editable: boolean;
|
||||||
|
};
|
||||||
|
const StreamComment: React.FC<StreamCommentProps> = ({
|
||||||
|
comment,
|
||||||
|
onExtraActions,
|
||||||
|
editable,
|
||||||
|
onUpdateComment,
|
||||||
|
onCancelCommentEdit,
|
||||||
|
}) => {
|
||||||
|
const $actions = useRef<HTMLDivElement>(null);
|
||||||
|
if (comment) {
|
||||||
|
return (
|
||||||
|
<ActivityItem>
|
||||||
|
<ActivityItemHeaderUser size={32} member={comment.createdBy} />
|
||||||
|
<ActivityItemHeader editable={editable}>
|
||||||
|
<ActivityItemHeaderTitle>
|
||||||
|
<ActivityItemHeaderTitleName>{comment.createdBy.fullName}</ActivityItemHeaderTitleName>
|
||||||
|
<ActivityItemTimestamp margin={8}>
|
||||||
|
{dayjs(comment.createdAt).format('MMM D [at] h:mm A')}
|
||||||
|
{comment.updatedAt && ' (edited)'}
|
||||||
|
</ActivityItemTimestamp>
|
||||||
|
</ActivityItemHeaderTitle>
|
||||||
|
<ActivityItemCommentContainer>
|
||||||
|
<ActivityItemComment editable={editable}>
|
||||||
|
{editable ? (
|
||||||
|
<CommentCreator
|
||||||
|
message={comment.message}
|
||||||
|
autoFocus
|
||||||
|
onCancelEdit={onCancelCommentEdit}
|
||||||
|
onCreateComment={onUpdateComment}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ReactMarkdown escapeHtml={false} plugins={[em]}>
|
||||||
|
{DOMPurify.sanitize(comment.message, { FORBID_TAGS: ['style', 'img'] })}
|
||||||
|
</ReactMarkdown>
|
||||||
|
)}
|
||||||
|
</ActivityItemComment>
|
||||||
|
<ActivityItemCommentActions>
|
||||||
|
<ActivityItemCommentAction
|
||||||
|
ref={$actions}
|
||||||
|
onClick={() => {
|
||||||
|
onExtraActions(comment.id, $actions);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AngleDown width={18} height={18} />
|
||||||
|
</ActivityItemCommentAction>
|
||||||
|
</ActivityItemCommentActions>
|
||||||
|
</ActivityItemCommentContainer>
|
||||||
|
</ActivityItemHeader>
|
||||||
|
</ActivityItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StreamActivityProps = {
|
||||||
|
activity?: TaskActivity | null;
|
||||||
|
};
|
||||||
|
const StreamActivity: React.FC<StreamActivityProps> = ({ activity }) => {
|
||||||
|
if (activity) {
|
||||||
|
return (
|
||||||
|
<ActivityItem>
|
||||||
|
<ActivityItemHeaderUser
|
||||||
|
size={32}
|
||||||
|
member={{
|
||||||
|
id: activity.causedBy.id,
|
||||||
|
fullName: activity.causedBy.fullName,
|
||||||
|
profileIcon: activity.causedBy.profileIcon
|
||||||
|
? activity.causedBy.profileIcon
|
||||||
|
: {
|
||||||
|
url: null,
|
||||||
|
initials: activity.causedBy.fullName.charAt(0),
|
||||||
|
bgColor: '#fff',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ActivityItemHeader>
|
||||||
|
<ActivityItemHeaderTitle>
|
||||||
|
<ActivityItemHeaderTitleName>{activity.causedBy.fullName}</ActivityItemHeaderTitleName>
|
||||||
|
<ActivityMessage type={activity.type} data={activity.data} />
|
||||||
|
</ActivityItemHeaderTitle>
|
||||||
|
<ActivityItemTimestamp margin={0}>
|
||||||
|
{dayjs(activity.createdAt).format('MMM D [at] h:mm A')}
|
||||||
|
</ActivityItemTimestamp>
|
||||||
|
</ActivityItemHeader>
|
||||||
|
</ActivityItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const ChecklistContainer = styled.div``;
|
const ChecklistContainer = styled.div``;
|
||||||
|
|
||||||
@ -114,8 +237,13 @@ type TaskDetailsProps = {
|
|||||||
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||||
onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||||
onOpenAddChecklistPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
onOpenAddChecklistPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||||
|
onCreateComment: (task: Task, message: string) => void;
|
||||||
|
onCommentShowActions: (commentID: string, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||||
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||||
|
onCancelCommentEdit: () => void;
|
||||||
|
onUpdateComment: (commentID: string, message: string) => void;
|
||||||
onChangeChecklistName: (checklistID: string, name: string) => void;
|
onChangeChecklistName: (checklistID: string, name: string) => void;
|
||||||
|
editableComment?: string | null;
|
||||||
onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
|
onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
|
||||||
onCloseModal: () => void;
|
onCloseModal: () => void;
|
||||||
onChecklistDrop: (checklist: TaskChecklist) => void;
|
onChecklistDrop: (checklist: TaskChecklist) => void;
|
||||||
@ -124,11 +252,15 @@ type TaskDetailsProps = {
|
|||||||
|
|
||||||
const TaskDetails: React.FC<TaskDetailsProps> = ({
|
const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||||
me,
|
me,
|
||||||
|
onCancelCommentEdit,
|
||||||
task,
|
task,
|
||||||
|
editableComment = null,
|
||||||
onDeleteChecklist,
|
onDeleteChecklist,
|
||||||
onTaskNameChange,
|
onTaskNameChange,
|
||||||
|
onCommentShowActions,
|
||||||
onOpenAddChecklistPopup,
|
onOpenAddChecklistPopup,
|
||||||
onChangeChecklistName,
|
onChangeChecklistName,
|
||||||
|
onCreateComment,
|
||||||
onChecklistDrop,
|
onChecklistDrop,
|
||||||
onChecklistItemDrop,
|
onChecklistItemDrop,
|
||||||
onToggleTaskComplete,
|
onToggleTaskComplete,
|
||||||
@ -137,6 +269,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
onDeleteItem,
|
onDeleteItem,
|
||||||
onDeleteTask,
|
onDeleteTask,
|
||||||
onCloseModal,
|
onCloseModal,
|
||||||
|
onUpdateComment,
|
||||||
onOpenAddMemberPopup,
|
onOpenAddMemberPopup,
|
||||||
onOpenAddLabelPopup,
|
onOpenAddLabelPopup,
|
||||||
onOpenDueDatePopop,
|
onOpenDueDatePopop,
|
||||||
@ -156,12 +289,38 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
});
|
});
|
||||||
const [saveTimeout, setSaveTimeout] = useState<any>(null);
|
const [saveTimeout, setSaveTimeout] = useState<any>(null);
|
||||||
const [showRaw, setShowRaw] = useState(false);
|
const [showRaw, setShowRaw] = useState(false);
|
||||||
const [showCommentActions, setShowCommentActions] = useState(false);
|
|
||||||
const taskDescriptionRef = useRef(task.description ?? '');
|
const taskDescriptionRef = useRef(task.description ?? '');
|
||||||
const $noMemberBtn = useRef<HTMLDivElement>(null);
|
const $noMemberBtn = useRef<HTMLDivElement>(null);
|
||||||
const $addMemberBtn = useRef<HTMLDivElement>(null);
|
const $addMemberBtn = useRef<HTMLDivElement>(null);
|
||||||
const $dueDateBtn = useRef<HTMLDivElement>(null);
|
const $dueDateBtn = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const activityStream: Array<{ id: string; data: { time: string; type: 'comment' | 'activity' } }> = [];
|
||||||
|
|
||||||
|
if (task.activity) {
|
||||||
|
task.activity.forEach(activity => {
|
||||||
|
activityStream.push({
|
||||||
|
id: activity.id,
|
||||||
|
data: {
|
||||||
|
time: activity.createdAt,
|
||||||
|
type: 'activity',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.comments) {
|
||||||
|
task.comments.forEach(comment => {
|
||||||
|
activityStream.push({
|
||||||
|
id: comment.id,
|
||||||
|
data: {
|
||||||
|
time: comment.createdAt,
|
||||||
|
type: 'comment',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
activityStream.sort((a, b) => (dayjs(a.data.time).isAfter(dayjs(b.data.time)) ? 1 : -1));
|
||||||
|
|
||||||
const saveDescription = () => {
|
const saveDescription = () => {
|
||||||
onTaskDescriptionChange(task, taskDescriptionRef.current);
|
onTaskDescriptionChange(task, taskDescriptionRef.current);
|
||||||
};
|
};
|
||||||
@ -425,46 +584,29 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<TabBarSection>
|
<TabBarSection>
|
||||||
<TabBarItem>Activity</TabBarItem>
|
<TabBarItem>Activity</TabBarItem>
|
||||||
</TabBarSection>
|
</TabBarSection>
|
||||||
<ActivitySection />
|
<ActivitySection>
|
||||||
|
{activityStream.map(stream =>
|
||||||
|
stream.data.type === 'comment' ? (
|
||||||
|
<StreamComment
|
||||||
|
onExtraActions={onCommentShowActions}
|
||||||
|
onCancelCommentEdit={onCancelCommentEdit}
|
||||||
|
onUpdateComment={message => onUpdateComment(stream.id, message)}
|
||||||
|
editable={stream.id === editableComment}
|
||||||
|
comment={task.comments && task.comments.find(comment => comment.id === stream.id)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StreamActivity activity={task.activity && task.activity.find(activity => activity.id === stream.id)} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</ActivitySection>
|
||||||
</InnerContentContainer>
|
</InnerContentContainer>
|
||||||
<CommentContainer>
|
<CommentContainer>
|
||||||
{me && (
|
{me && (
|
||||||
<CommentInnerWrapper>
|
<CommentCreator
|
||||||
<CommentProfile
|
me={me}
|
||||||
member={me}
|
onCreateComment={message => onCreateComment(task, message)}
|
||||||
size={32}
|
onMemberProfile={onMemberProfile}
|
||||||
onMemberProfile={$target => {
|
|
||||||
onMemberProfile($target, me.id);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<CommentEditorContainer>
|
|
||||||
<CommentTextArea
|
|
||||||
disabled
|
|
||||||
placeholder="Write a comment..."
|
|
||||||
onFocus={() => {
|
|
||||||
setShowCommentActions(true);
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
setShowCommentActions(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<CommentEditorActions visible={showCommentActions}>
|
|
||||||
<CommentEditorActionIcon>
|
|
||||||
<Paperclip width={12} height={12} />
|
|
||||||
</CommentEditorActionIcon>
|
|
||||||
<CommentEditorActionIcon>
|
|
||||||
<At width={12} height={12} />
|
|
||||||
</CommentEditorActionIcon>
|
|
||||||
<CommentEditorActionIcon>
|
|
||||||
<Smile width={12} height={12} />
|
|
||||||
</CommentEditorActionIcon>
|
|
||||||
<CommentEditorActionIcon>
|
|
||||||
<Task width={12} height={12} />
|
|
||||||
</CommentEditorActionIcon>
|
|
||||||
<CommentEditorSaveButton>Save</CommentEditorSaveButton>
|
|
||||||
</CommentEditorActions>
|
|
||||||
</CommentEditorContainer>
|
|
||||||
</CommentInnerWrapper>
|
|
||||||
)}
|
)}
|
||||||
</CommentContainer>
|
</CommentContainer>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
|
61
frontend/src/shared/components/TaskDetails/remark.js
Normal file
61
frontend/src/shared/components/TaskDetails/remark.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import visit from 'unist-util-visit';
|
||||||
|
import emoji from 'node-emoji';
|
||||||
|
import emoticon from 'emoticon';
|
||||||
|
import { Emoji } from 'emoji-mart';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ReactDOMServer from 'react-dom/server';
|
||||||
|
|
||||||
|
const RE_EMOJI = /:\+1:|:-1:|:[\w-]+:/g;
|
||||||
|
const RE_SHORT = /[$@|*'",;.=:\-)([\]\\/<>038BOopPsSdDxXzZ]{2,5}/g;
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS = {
|
||||||
|
padSpaceAfter: false,
|
||||||
|
emoticon: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function plugin(options) {
|
||||||
|
const settings = Object.assign({}, DEFAULT_SETTINGS, options);
|
||||||
|
const pad = !!settings.padSpaceAfter;
|
||||||
|
const emoticonEnable = !!settings.emoticon;
|
||||||
|
|
||||||
|
function getEmojiByShortCode(match) {
|
||||||
|
// find emoji by shortcode - full match or with-out last char as it could be from text e.g. :-),
|
||||||
|
const iconFull = emoticon.find(e => e.emoticons.includes(match)); // full match
|
||||||
|
const iconPart = emoticon.find(e => e.emoticons.includes(match.slice(0, -1))); // second search pattern
|
||||||
|
const trimmedChar = iconPart ? match.slice(-1) : '';
|
||||||
|
const addPad = pad ? ' ' : '';
|
||||||
|
let icon = iconFull ? iconFull.emoji + addPad : iconPart && iconPart.emoji + addPad + trimmedChar;
|
||||||
|
return icon || match;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmoji(match) {
|
||||||
|
console.log(match);
|
||||||
|
const got = emoji.get(match);
|
||||||
|
if (pad && got !== match) {
|
||||||
|
return got + ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(got);
|
||||||
|
return ReactDOMServer.renderToStaticMarkup(<Emoji set="google" emoji={match} size={16} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformer(tree) {
|
||||||
|
visit(tree, 'paragraph', function(node) {
|
||||||
|
console.log(tree);
|
||||||
|
// node.value = node.value.replace(RE_EMOJI, getEmoji);
|
||||||
|
node.type = 'html';
|
||||||
|
node.tagName = 'div';
|
||||||
|
node.value = node.children[0].value.replace(RE_EMOJI, getEmoji);
|
||||||
|
|
||||||
|
if (emoticonEnable) {
|
||||||
|
// node.value = node.value.replace(RE_SHORT, getEmojiByShortCode);
|
||||||
|
}
|
||||||
|
console.log(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { plugin };
|
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,40 @@ query findTask($taskID: UUID!) {
|
|||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
comments {
|
||||||
|
id
|
||||||
|
pinned
|
||||||
|
message
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
createdBy {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
profileIcon {
|
||||||
|
initials
|
||||||
|
bgColor
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activity {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
causedBy {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
profileIcon {
|
||||||
|
initials
|
||||||
|
bgColor
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
badges {
|
badges {
|
||||||
checklist {
|
checklist {
|
||||||
total
|
total
|
||||||
|
27
frontend/src/shared/graphql/task/createTaskComment.ts
Normal file
27
frontend/src/shared/graphql/task/createTaskComment.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
|
const CREATE_TASK_MUTATION = gql`
|
||||||
|
mutation createTaskComment($taskID: UUID!, $message: String!) {
|
||||||
|
createTaskComment(input: { taskID: $taskID, message: $message }) {
|
||||||
|
taskID
|
||||||
|
comment {
|
||||||
|
id
|
||||||
|
message
|
||||||
|
pinned
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
createdBy {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
profileIcon {
|
||||||
|
initials
|
||||||
|
bgColor
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default CREATE_TASK_MUTATION;
|
11
frontend/src/shared/graphql/task/deleteTaskComment.ts
Normal file
11
frontend/src/shared/graphql/task/deleteTaskComment.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
|
const CREATE_TASK_MUTATION = gql`
|
||||||
|
mutation deleteTaskComment($commentID: UUID!) {
|
||||||
|
deleteTaskComment(input: { commentID: $commentID }) {
|
||||||
|
commentID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default CREATE_TASK_MUTATION;
|
15
frontend/src/shared/graphql/task/updateTaskComment.ts
Normal file
15
frontend/src/shared/graphql/task/updateTaskComment.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
|
const CREATE_TASK_MUTATION = gql`
|
||||||
|
mutation updateTaskComment($commentID: UUID!, $message: String!) {
|
||||||
|
updateTaskComment(input: { commentID: $commentID, message: $message }) {
|
||||||
|
comment {
|
||||||
|
id
|
||||||
|
updatedAt
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default CREATE_TASK_MUTATION;
|
@ -16,10 +16,14 @@ const useOnOutsideClick = (
|
|||||||
|
|
||||||
const handleMouseUp = (event: any) => {
|
const handleMouseUp = (event: any) => {
|
||||||
if (typeof $ignoredElementRefsMemoized !== 'undefined') {
|
if (typeof $ignoredElementRefsMemoized !== 'undefined') {
|
||||||
const isAnyIgnoredElementAncestorOfTarget = $ignoredElementRefsMemoized.some(
|
const isAnyIgnoredElementAncestorOfTarget = $ignoredElementRefsMemoized.some(($elementRef: any) => {
|
||||||
($elementRef: any) =>
|
if ($elementRef && $elementRef.current) {
|
||||||
$elementRef.current.contains($mouseDownTargetRef.current) || $elementRef.current.contains(event.target),
|
return (
|
||||||
|
$elementRef.current.contains($mouseDownTargetRef.current) || $elementRef.current.contains(event.target)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
if (event.button === 0 && !isAnyIgnoredElementAncestorOfTarget) {
|
if (event.button === 0 && !isAnyIgnoredElementAncestorOfTarget) {
|
||||||
onOutsideClick();
|
onOutsideClick();
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Icon, { IconProps } from './Icon';
|
||||||
|
|
||||||
type Props = {
|
export const AngleDown: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||||
width: number | string;
|
return (
|
||||||
height: number | string;
|
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
|
||||||
color: string;
|
<path d="M143 352.3L7 216.3c-9.4-9.4-9.4-24.6 0-33.9l22.6-22.6c9.4-9.4 24.6-9.4 33.9 0l96.4 96.4 96.4-96.4c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9l-136 136c-9.2 9.4-24.4 9.4-33.8 0z" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AngleDown = ({ width, height, color }: Props) => {
|
type Props = {
|
||||||
|
width?: number | string;
|
||||||
|
height?: number | string;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AngleDownOld = ({ width, height, color }: Props) => {
|
||||||
return (
|
return (
|
||||||
<svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
|
<svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
|
||||||
<path
|
<path
|
||||||
@ -17,10 +26,10 @@ const AngleDown = ({ width, height, color }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AngleDown.defaultProps = {
|
AngleDownOld.defaultProps = {
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 16,
|
height: 16,
|
||||||
color: '#000',
|
color: '#000',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AngleDown;
|
export default AngleDownOld;
|
||||||
|
@ -9,7 +9,7 @@ export function updateApolloCache<T>(
|
|||||||
update: UpdateCacheFn<T>,
|
update: UpdateCacheFn<T>,
|
||||||
variables?: object,
|
variables?: object,
|
||||||
) {
|
) {
|
||||||
let queryArgs: DataProxy.Query<any>;
|
let queryArgs: DataProxy.Query<any, any>;
|
||||||
if (variables) {
|
if (variables) {
|
||||||
queryArgs = {
|
queryArgs = {
|
||||||
query: document,
|
query: document,
|
||||||
|
49
frontend/src/types.d.ts
vendored
49
frontend/src/types.d.ts
vendored
@ -1,3 +1,10 @@
|
|||||||
|
type ProjectLabel = {
|
||||||
|
id: string;
|
||||||
|
createdDate: string;
|
||||||
|
name?: string | null;
|
||||||
|
labelColor: LabelColor;
|
||||||
|
};
|
||||||
|
|
||||||
type ProfileIcon = {
|
type ProfileIcon = {
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
initials?: string | null;
|
initials?: string | null;
|
||||||
@ -56,6 +63,39 @@ type TaskBadges = {
|
|||||||
checklist?: ChecklistBadge | null;
|
checklist?: ChecklistBadge | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TaskActivityData = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CausedBy = {
|
||||||
|
id: string;
|
||||||
|
fullName: string;
|
||||||
|
profileIcon?: null | ProfileIcon;
|
||||||
|
};
|
||||||
|
type TaskActivity = {
|
||||||
|
id: string;
|
||||||
|
type: any;
|
||||||
|
data: Array<TaskActivityData>;
|
||||||
|
causedBy: CausedBy;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreatedBy = {
|
||||||
|
id: string;
|
||||||
|
fullName: string;
|
||||||
|
profileIcon: ProfileIcon;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TaskComment = {
|
||||||
|
id: string;
|
||||||
|
createdBy: CreatedBy;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
pinned: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Task = {
|
type Task = {
|
||||||
id: string;
|
id: string;
|
||||||
taskGroup: InnerTaskGroup;
|
taskGroup: InnerTaskGroup;
|
||||||
@ -69,6 +109,8 @@ type Task = {
|
|||||||
description?: string | null;
|
description?: string | null;
|
||||||
assigned?: Array<TaskUser>;
|
assigned?: Array<TaskUser>;
|
||||||
checklists?: Array<TaskChecklist> | null;
|
checklists?: Array<TaskChecklist> | null;
|
||||||
|
activity?: Array<TaskActivity> | null;
|
||||||
|
comments?: Array<TaskComment> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Project = {
|
type Project = {
|
||||||
@ -89,10 +131,3 @@ type Team = {
|
|||||||
name: string;
|
name: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProjectLabel = {
|
|
||||||
id: string;
|
|
||||||
createdDate: string;
|
|
||||||
name?: string | null;
|
|
||||||
labelColor: LabelColor;
|
|
||||||
};
|
|
||||||
|
9181
frontend/yarn.lock
9181
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
1
go.mod
1
go.mod
@ -23,5 +23,6 @@ require (
|
|||||||
github.com/spf13/viper v1.4.0
|
github.com/spf13/viper v1.4.0
|
||||||
github.com/vektah/gqlparser/v2 v2.0.1
|
github.com/vektah/gqlparser/v2 v2.0.1
|
||||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
|
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/mail.v2 v2.3.1
|
gopkg.in/mail.v2 v2.3.1
|
||||||
)
|
)
|
||||||
|
3
go.sum
3
go.sum
@ -388,7 +388,6 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
|
|||||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||||
github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
|
github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
|
||||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||||
github.com/matcornic/hermes v1.2.0 h1:AuqZpYcTOtTB7cahdevLfnhIpfzmpqw5Czv8vpdnFDU=
|
|
||||||
github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc=
|
github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc=
|
||||||
github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI=
|
github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI=
|
||||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
|
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
|
||||||
@ -892,6 +891,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
|||||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
@ -4,6 +4,7 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -103,6 +104,22 @@ type Task struct {
|
|||||||
CompletedAt sql.NullTime `json:"completed_at"`
|
CompletedAt sql.NullTime `json:"completed_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskActivity struct {
|
||||||
|
TaskActivityID uuid.UUID `json:"task_activity_id"`
|
||||||
|
Active bool `json:"active"`
|
||||||
|
TaskID uuid.UUID `json:"task_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
CausedBy uuid.UUID `json:"caused_by"`
|
||||||
|
ActivityTypeID int32 `json:"activity_type_id"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskActivityType struct {
|
||||||
|
TaskActivityTypeID int32 `json:"task_activity_type_id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Template string `json:"template"`
|
||||||
|
}
|
||||||
|
|
||||||
type TaskAssigned struct {
|
type TaskAssigned struct {
|
||||||
TaskAssignedID uuid.UUID `json:"task_assigned_id"`
|
TaskAssignedID uuid.UUID `json:"task_assigned_id"`
|
||||||
TaskID uuid.UUID `json:"task_id"`
|
TaskID uuid.UUID `json:"task_id"`
|
||||||
@ -128,6 +145,16 @@ type TaskChecklistItem struct {
|
|||||||
DueDate sql.NullTime `json:"due_date"`
|
DueDate sql.NullTime `json:"due_date"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskComment struct {
|
||||||
|
TaskCommentID uuid.UUID `json:"task_comment_id"`
|
||||||
|
TaskID uuid.UUID `json:"task_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt sql.NullTime `json:"updated_at"`
|
||||||
|
CreatedBy uuid.UUID `json:"created_by"`
|
||||||
|
Pinned bool `json:"pinned"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
type TaskGroup struct {
|
type TaskGroup struct {
|
||||||
TaskGroupID uuid.UUID `json:"task_group_id"`
|
TaskGroupID uuid.UUID `json:"task_group_id"`
|
||||||
ProjectID uuid.UUID `json:"project_id"`
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
@ -23,10 +23,12 @@ type Querier interface {
|
|||||||
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
|
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
|
||||||
CreateSystemOption(ctx context.Context, arg CreateSystemOptionParams) (SystemOption, error)
|
CreateSystemOption(ctx context.Context, arg CreateSystemOptionParams) (SystemOption, error)
|
||||||
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
|
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
|
||||||
|
CreateTaskActivity(ctx context.Context, arg CreateTaskActivityParams) (TaskActivity, error)
|
||||||
CreateTaskAll(ctx context.Context, arg CreateTaskAllParams) (Task, error)
|
CreateTaskAll(ctx context.Context, arg CreateTaskAllParams) (Task, error)
|
||||||
CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error)
|
CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error)
|
||||||
CreateTaskChecklist(ctx context.Context, arg CreateTaskChecklistParams) (TaskChecklist, error)
|
CreateTaskChecklist(ctx context.Context, arg CreateTaskChecklistParams) (TaskChecklist, error)
|
||||||
CreateTaskChecklistItem(ctx context.Context, arg CreateTaskChecklistItemParams) (TaskChecklistItem, error)
|
CreateTaskChecklistItem(ctx context.Context, arg CreateTaskChecklistItemParams) (TaskChecklistItem, error)
|
||||||
|
CreateTaskComment(ctx context.Context, arg CreateTaskCommentParams) (TaskComment, error)
|
||||||
CreateTaskGroup(ctx context.Context, arg CreateTaskGroupParams) (TaskGroup, error)
|
CreateTaskGroup(ctx context.Context, arg CreateTaskGroupParams) (TaskGroup, error)
|
||||||
CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error)
|
CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error)
|
||||||
CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error)
|
CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error)
|
||||||
@ -47,6 +49,7 @@ type Querier interface {
|
|||||||
DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error
|
DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error
|
||||||
DeleteTaskChecklistByID(ctx context.Context, taskChecklistID uuid.UUID) error
|
DeleteTaskChecklistByID(ctx context.Context, taskChecklistID uuid.UUID) error
|
||||||
DeleteTaskChecklistItem(ctx context.Context, taskChecklistItemID uuid.UUID) error
|
DeleteTaskChecklistItem(ctx context.Context, taskChecklistItemID uuid.UUID) error
|
||||||
|
DeleteTaskCommentByID(ctx context.Context, taskCommentID uuid.UUID) (TaskComment, error)
|
||||||
DeleteTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (int64, error)
|
DeleteTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (int64, error)
|
||||||
DeleteTaskLabelByID(ctx context.Context, taskLabelID uuid.UUID) error
|
DeleteTaskLabelByID(ctx context.Context, taskLabelID uuid.UUID) error
|
||||||
DeleteTaskLabelForTaskByProjectLabelID(ctx context.Context, arg DeleteTaskLabelForTaskByProjectLabelIDParams) error
|
DeleteTaskLabelForTaskByProjectLabelID(ctx context.Context, arg DeleteTaskLabelForTaskByProjectLabelIDParams) error
|
||||||
@ -55,6 +58,7 @@ type Querier interface {
|
|||||||
DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
|
DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
|
||||||
DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error
|
DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error
|
||||||
DeleteUserAccountInvitedForEmail(ctx context.Context, email string) error
|
DeleteUserAccountInvitedForEmail(ctx context.Context, email string) error
|
||||||
|
GetActivityForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskActivity, error)
|
||||||
GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error)
|
GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error)
|
||||||
GetAllOrganizations(ctx context.Context) ([]Organization, error)
|
GetAllOrganizations(ctx context.Context) ([]Organization, error)
|
||||||
GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
|
GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
|
||||||
@ -65,6 +69,7 @@ type Querier interface {
|
|||||||
GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
|
GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
|
||||||
GetAllVisibleProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error)
|
GetAllVisibleProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error)
|
||||||
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
|
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
|
||||||
|
GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskComment, error)
|
||||||
GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error)
|
GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error)
|
||||||
GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error)
|
GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error)
|
||||||
GetEntityForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetEntityForNotificationIDRow, error)
|
GetEntityForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetEntityForNotificationIDRow, error)
|
||||||
@ -74,6 +79,7 @@ type Querier interface {
|
|||||||
GetInvitedUserByEmail(ctx context.Context, email string) (UserAccountInvited, error)
|
GetInvitedUserByEmail(ctx context.Context, email string) (UserAccountInvited, error)
|
||||||
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
|
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
|
||||||
GetLabelColors(ctx context.Context) ([]LabelColor, error)
|
GetLabelColors(ctx context.Context) ([]LabelColor, error)
|
||||||
|
GetLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) (GetLastMoveForTaskIDRow, error)
|
||||||
GetMemberData(ctx context.Context, projectID uuid.UUID) ([]UserAccount, error)
|
GetMemberData(ctx context.Context, projectID uuid.UUID) ([]UserAccount, error)
|
||||||
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
||||||
GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
||||||
@ -113,6 +119,7 @@ type Querier interface {
|
|||||||
GetTeamRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetTeamRolesForUserIDRow, error)
|
GetTeamRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetTeamRolesForUserIDRow, error)
|
||||||
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
|
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
|
||||||
GetTeamsForUserIDWhereAdmin(ctx context.Context, userID uuid.UUID) ([]Team, error)
|
GetTeamsForUserIDWhereAdmin(ctx context.Context, userID uuid.UUID) ([]Team, error)
|
||||||
|
GetTemplateForActivityID(ctx context.Context, taskActivityTypeID int32) (string, error)
|
||||||
GetUserAccountByEmail(ctx context.Context, email string) (UserAccount, error)
|
GetUserAccountByEmail(ctx context.Context, email string) (UserAccount, error)
|
||||||
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
|
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
|
||||||
GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
|
GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
|
||||||
@ -120,6 +127,7 @@ type Querier interface {
|
|||||||
HasActiveUser(ctx context.Context) (bool, error)
|
HasActiveUser(ctx context.Context) (bool, error)
|
||||||
HasAnyUser(ctx context.Context) (bool, error)
|
HasAnyUser(ctx context.Context) (bool, error)
|
||||||
SetFirstUserActive(ctx context.Context) (UserAccount, error)
|
SetFirstUserActive(ctx context.Context) (UserAccount, error)
|
||||||
|
SetInactiveLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) error
|
||||||
SetTaskChecklistItemComplete(ctx context.Context, arg SetTaskChecklistItemCompleteParams) (TaskChecklistItem, error)
|
SetTaskChecklistItemComplete(ctx context.Context, arg SetTaskChecklistItemCompleteParams) (TaskChecklistItem, error)
|
||||||
SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
|
SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
|
||||||
SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
|
SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
|
||||||
@ -134,6 +142,7 @@ type Querier interface {
|
|||||||
UpdateTaskChecklistItemName(ctx context.Context, arg UpdateTaskChecklistItemNameParams) (TaskChecklistItem, error)
|
UpdateTaskChecklistItemName(ctx context.Context, arg UpdateTaskChecklistItemNameParams) (TaskChecklistItem, error)
|
||||||
UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (TaskChecklist, error)
|
UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (TaskChecklist, error)
|
||||||
UpdateTaskChecklistPosition(ctx context.Context, arg UpdateTaskChecklistPositionParams) (TaskChecklist, error)
|
UpdateTaskChecklistPosition(ctx context.Context, arg UpdateTaskChecklistPositionParams) (TaskChecklist, error)
|
||||||
|
UpdateTaskComment(ctx context.Context, arg UpdateTaskCommentParams) (TaskComment, error)
|
||||||
UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescriptionParams) (Task, error)
|
UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescriptionParams) (Task, error)
|
||||||
UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDateParams) (Task, error)
|
UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDateParams) (Task, error)
|
||||||
UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
|
UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
|
||||||
|
@ -43,3 +43,16 @@ UPDATE task SET complete = $2, completed_at = $3 WHERE task_id = $1 RETURNING *;
|
|||||||
SELECT project_id FROM task
|
SELECT project_id FROM task
|
||||||
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
|
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
|
||||||
WHERE task_id = $1;
|
WHERE task_id = $1;
|
||||||
|
|
||||||
|
-- name: CreateTaskComment :one
|
||||||
|
INSERT INTO task_comment (task_id, message, created_at, created_by)
|
||||||
|
VALUES ($1, $2, $3, $4) RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetCommentsForTaskID :many
|
||||||
|
SELECT * FROM task_comment WHERE task_id = $1 ORDER BY created_at;
|
||||||
|
|
||||||
|
-- name: DeleteTaskCommentByID :one
|
||||||
|
DELETE FROM task_comment WHERE task_comment_id = $1 RETURNING *;
|
||||||
|
|
||||||
|
-- name: UpdateTaskComment :one
|
||||||
|
UPDATE task_comment SET message = $2, updated_at = $3 WHERE task_comment_id = $1 RETURNING *;
|
||||||
|
22
internal/db/query/task_activity.sql
Normal file
22
internal/db/query/task_activity.sql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
-- name: CreateTaskActivity :one
|
||||||
|
INSERT INTO task_activity (task_id, caused_by, created_at, activity_type_id, data)
|
||||||
|
VALUES ($1, $2, $3, $4, $5) RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetActivityForTaskID :many
|
||||||
|
SELECT * FROM task_activity WHERE task_id = $1 AND active = true;
|
||||||
|
|
||||||
|
-- name: GetTemplateForActivityID :one
|
||||||
|
SELECT template FROM task_activity_type WHERE task_activity_type_id = $1;
|
||||||
|
|
||||||
|
-- name: GetLastMoveForTaskID :one
|
||||||
|
SELECT active, created_at, data->>'CurTaskGroupID' AS cur_task_group_id, data->>'PrevTaskGroupID' AS prev_task_group_id FROM task_activity
|
||||||
|
WHERE task_id = $1 AND activity_type_id = 2 AND created_at >= NOW() - INTERVAL '5 minutes'
|
||||||
|
ORDER BY created_at DESC LIMIT 1;
|
||||||
|
|
||||||
|
-- name: SetInactiveLastMoveForTaskID :exec
|
||||||
|
UPDATE task_activity SET active = false WHERE task_activity_id = (
|
||||||
|
SELECT task_activity_id FROM task_activity AS ta
|
||||||
|
WHERE ta.activity_type_id = 2 AND ta.task_id = $1
|
||||||
|
AND ta.created_at >= NOW() - INTERVAL '5 minutes'
|
||||||
|
ORDER BY created_at DESC LIMIT 1
|
||||||
|
);
|
@ -85,6 +85,38 @@ func (q *Queries) CreateTaskAll(ctx context.Context, arg CreateTaskAllParams) (T
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createTaskComment = `-- name: CreateTaskComment :one
|
||||||
|
INSERT INTO task_comment (task_id, message, created_at, created_by)
|
||||||
|
VALUES ($1, $2, $3, $4) RETURNING task_comment_id, task_id, created_at, updated_at, created_by, pinned, message
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateTaskCommentParams struct {
|
||||||
|
TaskID uuid.UUID `json:"task_id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
CreatedBy uuid.UUID `json:"created_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateTaskComment(ctx context.Context, arg CreateTaskCommentParams) (TaskComment, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createTaskComment,
|
||||||
|
arg.TaskID,
|
||||||
|
arg.Message,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.CreatedBy,
|
||||||
|
)
|
||||||
|
var i TaskComment
|
||||||
|
err := row.Scan(
|
||||||
|
&i.TaskCommentID,
|
||||||
|
&i.TaskID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.CreatedBy,
|
||||||
|
&i.Pinned,
|
||||||
|
&i.Message,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const deleteTaskByID = `-- name: DeleteTaskByID :exec
|
const deleteTaskByID = `-- name: DeleteTaskByID :exec
|
||||||
DELETE FROM task WHERE task_id = $1
|
DELETE FROM task WHERE task_id = $1
|
||||||
`
|
`
|
||||||
@ -94,6 +126,25 @@ func (q *Queries) DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteTaskCommentByID = `-- name: DeleteTaskCommentByID :one
|
||||||
|
DELETE FROM task_comment WHERE task_comment_id = $1 RETURNING task_comment_id, task_id, created_at, updated_at, created_by, pinned, message
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteTaskCommentByID(ctx context.Context, taskCommentID uuid.UUID) (TaskComment, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, deleteTaskCommentByID, taskCommentID)
|
||||||
|
var i TaskComment
|
||||||
|
err := row.Scan(
|
||||||
|
&i.TaskCommentID,
|
||||||
|
&i.TaskID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.CreatedBy,
|
||||||
|
&i.Pinned,
|
||||||
|
&i.Message,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const deleteTasksByTaskGroupID = `-- name: DeleteTasksByTaskGroupID :execrows
|
const deleteTasksByTaskGroupID = `-- name: DeleteTasksByTaskGroupID :execrows
|
||||||
DELETE FROM task where task_group_id = $1
|
DELETE FROM task where task_group_id = $1
|
||||||
`
|
`
|
||||||
@ -143,6 +194,41 @@ func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCommentsForTaskID = `-- name: GetCommentsForTaskID :many
|
||||||
|
SELECT task_comment_id, task_id, created_at, updated_at, created_by, pinned, message FROM task_comment WHERE task_id = $1 ORDER BY created_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskComment, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getCommentsForTaskID, taskID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []TaskComment
|
||||||
|
for rows.Next() {
|
||||||
|
var i TaskComment
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TaskCommentID,
|
||||||
|
&i.TaskID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.CreatedBy,
|
||||||
|
&i.Pinned,
|
||||||
|
&i.Message,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const getProjectIDForTask = `-- name: GetProjectIDForTask :one
|
const getProjectIDForTask = `-- name: GetProjectIDForTask :one
|
||||||
SELECT project_id FROM task
|
SELECT project_id FROM task
|
||||||
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
|
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
|
||||||
@ -241,6 +327,31 @@ func (q *Queries) SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateTaskComment = `-- name: UpdateTaskComment :one
|
||||||
|
UPDATE task_comment SET message = $2, updated_at = $3 WHERE task_comment_id = $1 RETURNING task_comment_id, task_id, created_at, updated_at, created_by, pinned, message
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateTaskCommentParams struct {
|
||||||
|
TaskCommentID uuid.UUID `json:"task_comment_id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
UpdatedAt sql.NullTime `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateTaskComment(ctx context.Context, arg UpdateTaskCommentParams) (TaskComment, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateTaskComment, arg.TaskCommentID, arg.Message, arg.UpdatedAt)
|
||||||
|
var i TaskComment
|
||||||
|
err := row.Scan(
|
||||||
|
&i.TaskCommentID,
|
||||||
|
&i.TaskID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.CreatedBy,
|
||||||
|
&i.Pinned,
|
||||||
|
&i.Message,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const updateTaskDescription = `-- name: UpdateTaskDescription :one
|
const updateTaskDescription = `-- name: UpdateTaskDescription :one
|
||||||
UPDATE task SET description = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at
|
UPDATE task SET description = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at
|
||||||
`
|
`
|
||||||
|
131
internal/db/task_activity.sql.go
Normal file
131
internal/db/task_activity.sql.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// source: task_activity.sql
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createTaskActivity = `-- name: CreateTaskActivity :one
|
||||||
|
INSERT INTO task_activity (task_id, caused_by, created_at, activity_type_id, data)
|
||||||
|
VALUES ($1, $2, $3, $4, $5) RETURNING task_activity_id, active, task_id, created_at, caused_by, activity_type_id, data
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateTaskActivityParams struct {
|
||||||
|
TaskID uuid.UUID `json:"task_id"`
|
||||||
|
CausedBy uuid.UUID `json:"caused_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
ActivityTypeID int32 `json:"activity_type_id"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateTaskActivity(ctx context.Context, arg CreateTaskActivityParams) (TaskActivity, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createTaskActivity,
|
||||||
|
arg.TaskID,
|
||||||
|
arg.CausedBy,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.ActivityTypeID,
|
||||||
|
arg.Data,
|
||||||
|
)
|
||||||
|
var i TaskActivity
|
||||||
|
err := row.Scan(
|
||||||
|
&i.TaskActivityID,
|
||||||
|
&i.Active,
|
||||||
|
&i.TaskID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.CausedBy,
|
||||||
|
&i.ActivityTypeID,
|
||||||
|
&i.Data,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActivityForTaskID = `-- name: GetActivityForTaskID :many
|
||||||
|
SELECT task_activity_id, active, task_id, created_at, caused_by, activity_type_id, data FROM task_activity WHERE task_id = $1 AND active = true
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetActivityForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskActivity, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getActivityForTaskID, taskID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []TaskActivity
|
||||||
|
for rows.Next() {
|
||||||
|
var i TaskActivity
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TaskActivityID,
|
||||||
|
&i.Active,
|
||||||
|
&i.TaskID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.CausedBy,
|
||||||
|
&i.ActivityTypeID,
|
||||||
|
&i.Data,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLastMoveForTaskID = `-- name: GetLastMoveForTaskID :one
|
||||||
|
SELECT active, created_at, data->>'CurTaskGroupID' AS cur_task_group_id, data->>'PrevTaskGroupID' AS prev_task_group_id FROM task_activity
|
||||||
|
WHERE task_id = $1 AND activity_type_id = 2 AND created_at >= NOW() - INTERVAL '5 minutes'
|
||||||
|
ORDER BY created_at DESC LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetLastMoveForTaskIDRow struct {
|
||||||
|
Active bool `json:"active"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
CurTaskGroupID interface{} `json:"cur_task_group_id"`
|
||||||
|
PrevTaskGroupID interface{} `json:"prev_task_group_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) (GetLastMoveForTaskIDRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getLastMoveForTaskID, taskID)
|
||||||
|
var i GetLastMoveForTaskIDRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Active,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.CurTaskGroupID,
|
||||||
|
&i.PrevTaskGroupID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTemplateForActivityID = `-- name: GetTemplateForActivityID :one
|
||||||
|
SELECT template FROM task_activity_type WHERE task_activity_type_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetTemplateForActivityID(ctx context.Context, taskActivityTypeID int32) (string, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getTemplateForActivityID, taskActivityTypeID)
|
||||||
|
var template string
|
||||||
|
err := row.Scan(&template)
|
||||||
|
return template, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const setInactiveLastMoveForTaskID = `-- name: SetInactiveLastMoveForTaskID :exec
|
||||||
|
UPDATE task_activity SET active = false WHERE task_activity_id = (
|
||||||
|
SELECT task_activity_id FROM task_activity AS ta
|
||||||
|
WHERE ta.activity_type_id = 2 AND ta.task_id = $1
|
||||||
|
AND ta.created_at >= NOW() - INTERVAL '5 minutes'
|
||||||
|
ORDER BY created_at DESC LIMIT 1
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) SetInactiveLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, setInactiveLastMoveForTaskID, taskID)
|
||||||
|
return err
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -255,3 +255,28 @@ func GetActionType(actionType int32) ActionType {
|
|||||||
panic("Not a valid entity type!")
|
panic("Not a valid entity type!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MemberType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
MemberTypeInvited MemberType = "INVITED"
|
||||||
|
MemberTypeJoined MemberType = "JOINED"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MasterEntry struct {
|
||||||
|
MemberType MemberType
|
||||||
|
ID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TASK_ADDED_TO_TASK_GROUP int32 = 1
|
||||||
|
TASK_MOVED_TO_TASK_GROUP int32 = 2
|
||||||
|
TASK_MARK_COMPLETE int32 = 3
|
||||||
|
TASK_MARK_INCOMPLETE int32 = 4
|
||||||
|
TASK_DUE_DATE_CHANGED int32 = 5
|
||||||
|
TASK_DUE_DATE_ADDED int32 = 6
|
||||||
|
TASK_DUE_DATE_REMOVED int32 = 7
|
||||||
|
TASK_CHECKLIST_CHANGED int32 = 8
|
||||||
|
TASK_CHECKLIST_ADDED int32 = 9
|
||||||
|
TASK_CHECKLIST_REMOVED int32 = 10
|
||||||
|
)
|
||||||
|
@ -41,3 +41,7 @@ func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*
|
|||||||
|
|
||||||
return &MemberList{Teams: teams, Projects: projects}, nil
|
return &MemberList{Teams: teams, Projects: projects}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActivityData struct {
|
||||||
|
Data map[string]string
|
||||||
|
}
|
||||||
|
@ -22,6 +22,12 @@ type AssignTaskInput struct {
|
|||||||
UserID uuid.UUID `json:"userID"`
|
UserID uuid.UUID `json:"userID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CausedBy struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
FullName string `json:"fullName"`
|
||||||
|
ProfileIcon *ProfileIcon `json:"profileIcon"`
|
||||||
|
}
|
||||||
|
|
||||||
type ChecklistBadge struct {
|
type ChecklistBadge struct {
|
||||||
Complete int `json:"complete"`
|
Complete int `json:"complete"`
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
@ -39,6 +45,16 @@ type CreateTaskChecklistItem struct {
|
|||||||
Position float64 `json:"position"`
|
Position float64 `json:"position"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateTaskComment struct {
|
||||||
|
TaskID uuid.UUID `json:"taskID"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTaskCommentPayload struct {
|
||||||
|
TaskID uuid.UUID `json:"taskID"`
|
||||||
|
Comment *db.TaskComment `json:"comment"`
|
||||||
|
}
|
||||||
|
|
||||||
type CreateTeamMember struct {
|
type CreateTeamMember struct {
|
||||||
UserID uuid.UUID `json:"userID"`
|
UserID uuid.UUID `json:"userID"`
|
||||||
TeamID uuid.UUID `json:"teamID"`
|
TeamID uuid.UUID `json:"teamID"`
|
||||||
@ -49,6 +65,12 @@ type CreateTeamMemberPayload struct {
|
|||||||
TeamMember *Member `json:"teamMember"`
|
TeamMember *Member `json:"teamMember"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreatedBy struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
FullName string `json:"fullName"`
|
||||||
|
ProfileIcon *ProfileIcon `json:"profileIcon"`
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteInvitedProjectMember struct {
|
type DeleteInvitedProjectMember struct {
|
||||||
ProjectID uuid.UUID `json:"projectID"`
|
ProjectID uuid.UUID `json:"projectID"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
@ -108,6 +130,15 @@ type DeleteTaskChecklistPayload struct {
|
|||||||
TaskChecklist *db.TaskChecklist `json:"taskChecklist"`
|
TaskChecklist *db.TaskChecklist `json:"taskChecklist"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteTaskComment struct {
|
||||||
|
CommentID uuid.UUID `json:"commentID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteTaskCommentPayload struct {
|
||||||
|
TaskID uuid.UUID `json:"taskID"`
|
||||||
|
CommentID uuid.UUID `json:"commentID"`
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteTaskGroupInput struct {
|
type DeleteTaskGroupInput struct {
|
||||||
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||||
}
|
}
|
||||||
@ -374,6 +405,11 @@ type SortTaskGroupPayload struct {
|
|||||||
Tasks []db.Task `json:"tasks"`
|
Tasks []db.Task `json:"tasks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskActivityData struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
type TaskBadges struct {
|
type TaskBadges struct {
|
||||||
Checklist *ChecklistBadge `json:"checklist"`
|
Checklist *ChecklistBadge `json:"checklist"`
|
||||||
}
|
}
|
||||||
@ -466,6 +502,16 @@ type UpdateTaskChecklistName struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateTaskComment struct {
|
||||||
|
CommentID uuid.UUID `json:"commentID"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateTaskCommentPayload struct {
|
||||||
|
TaskID uuid.UUID `json:"taskID"`
|
||||||
|
Comment *db.TaskComment `json:"comment"`
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateTaskDescriptionInput struct {
|
type UpdateTaskDescriptionInput struct {
|
||||||
TaskID uuid.UUID `json:"taskID"`
|
TaskID uuid.UUID `json:"taskID"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
@ -615,6 +661,63 @@ func (e ActionType) MarshalGQL(w io.Writer) {
|
|||||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActivityType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActivityTypeTaskAdded ActivityType = "TASK_ADDED"
|
||||||
|
ActivityTypeTaskMoved ActivityType = "TASK_MOVED"
|
||||||
|
ActivityTypeTaskMarkedComplete ActivityType = "TASK_MARKED_COMPLETE"
|
||||||
|
ActivityTypeTaskMarkedIncomplete ActivityType = "TASK_MARKED_INCOMPLETE"
|
||||||
|
ActivityTypeTaskDueDateChanged ActivityType = "TASK_DUE_DATE_CHANGED"
|
||||||
|
ActivityTypeTaskDueDateAdded ActivityType = "TASK_DUE_DATE_ADDED"
|
||||||
|
ActivityTypeTaskDueDateRemoved ActivityType = "TASK_DUE_DATE_REMOVED"
|
||||||
|
ActivityTypeTaskChecklistChanged ActivityType = "TASK_CHECKLIST_CHANGED"
|
||||||
|
ActivityTypeTaskChecklistAdded ActivityType = "TASK_CHECKLIST_ADDED"
|
||||||
|
ActivityTypeTaskChecklistRemoved ActivityType = "TASK_CHECKLIST_REMOVED"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AllActivityType = []ActivityType{
|
||||||
|
ActivityTypeTaskAdded,
|
||||||
|
ActivityTypeTaskMoved,
|
||||||
|
ActivityTypeTaskMarkedComplete,
|
||||||
|
ActivityTypeTaskMarkedIncomplete,
|
||||||
|
ActivityTypeTaskDueDateChanged,
|
||||||
|
ActivityTypeTaskDueDateAdded,
|
||||||
|
ActivityTypeTaskDueDateRemoved,
|
||||||
|
ActivityTypeTaskChecklistChanged,
|
||||||
|
ActivityTypeTaskChecklistAdded,
|
||||||
|
ActivityTypeTaskChecklistRemoved,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ActivityType) IsValid() bool {
|
||||||
|
switch e {
|
||||||
|
case ActivityTypeTaskAdded, ActivityTypeTaskMoved, ActivityTypeTaskMarkedComplete, ActivityTypeTaskMarkedIncomplete, ActivityTypeTaskDueDateChanged, ActivityTypeTaskDueDateAdded, ActivityTypeTaskDueDateRemoved, ActivityTypeTaskChecklistChanged, ActivityTypeTaskChecklistAdded, ActivityTypeTaskChecklistRemoved:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ActivityType) String() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ActivityType) UnmarshalGQL(v interface{}) error {
|
||||||
|
str, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("enums must be strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
*e = ActivityType(str)
|
||||||
|
if !e.IsValid() {
|
||||||
|
return fmt.Errorf("%s is not a valid ActivityType", str)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ActivityType) MarshalGQL(w io.Writer) {
|
||||||
|
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||||
|
}
|
||||||
|
|
||||||
type ActorType string
|
type ActorType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -135,6 +135,38 @@ type TaskBadges {
|
|||||||
checklist: ChecklistBadge
|
checklist: ChecklistBadge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CausedBy {
|
||||||
|
id: ID!
|
||||||
|
fullName: String!
|
||||||
|
profileIcon: ProfileIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskActivityData {
|
||||||
|
name: String!
|
||||||
|
value: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActivityType {
|
||||||
|
TASK_ADDED
|
||||||
|
TASK_MOVED
|
||||||
|
TASK_MARKED_COMPLETE
|
||||||
|
TASK_MARKED_INCOMPLETE
|
||||||
|
TASK_DUE_DATE_CHANGED
|
||||||
|
TASK_DUE_DATE_ADDED
|
||||||
|
TASK_DUE_DATE_REMOVED
|
||||||
|
TASK_CHECKLIST_CHANGED
|
||||||
|
TASK_CHECKLIST_ADDED
|
||||||
|
TASK_CHECKLIST_REMOVED
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskActivity {
|
||||||
|
id: ID!
|
||||||
|
type: ActivityType!
|
||||||
|
data: [TaskActivityData!]!
|
||||||
|
causedBy: CausedBy!
|
||||||
|
createdAt: Time!
|
||||||
|
}
|
||||||
|
|
||||||
type Task {
|
type Task {
|
||||||
id: ID!
|
id: ID!
|
||||||
taskGroup: TaskGroup!
|
taskGroup: TaskGroup!
|
||||||
@ -149,6 +181,23 @@ type Task {
|
|||||||
labels: [TaskLabel!]!
|
labels: [TaskLabel!]!
|
||||||
checklists: [TaskChecklist!]!
|
checklists: [TaskChecklist!]!
|
||||||
badges: TaskBadges!
|
badges: TaskBadges!
|
||||||
|
activity: [TaskActivity!]!
|
||||||
|
comments: [TaskComment!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatedBy {
|
||||||
|
id: ID!
|
||||||
|
fullName: String!
|
||||||
|
profileIcon: ProfileIcon!
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskComment {
|
||||||
|
id: ID!
|
||||||
|
createdAt: Time!
|
||||||
|
updatedAt: Time
|
||||||
|
message: String!
|
||||||
|
createdBy: CreatedBy!
|
||||||
|
pinned: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Organization {
|
type Organization {
|
||||||
@ -582,6 +631,44 @@ type DeleteTaskChecklistPayload {
|
|||||||
taskChecklist: TaskChecklist!
|
taskChecklist: TaskChecklist!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extend type Mutation {
|
||||||
|
createTaskComment(input: CreateTaskComment):
|
||||||
|
CreateTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||||
|
deleteTaskComment(input: DeleteTaskComment):
|
||||||
|
DeleteTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||||
|
updateTaskComment(input: UpdateTaskComment):
|
||||||
|
UpdateTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateTaskComment {
|
||||||
|
taskID: UUID!
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTaskCommentPayload {
|
||||||
|
taskID: UUID!
|
||||||
|
comment: TaskComment!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateTaskComment {
|
||||||
|
commentID: UUID!
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateTaskCommentPayload {
|
||||||
|
taskID: UUID!
|
||||||
|
comment: TaskComment!
|
||||||
|
}
|
||||||
|
|
||||||
|
input DeleteTaskComment {
|
||||||
|
commentID: UUID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteTaskCommentPayload {
|
||||||
|
taskID: UUID!
|
||||||
|
commentID: UUID!
|
||||||
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createTaskGroup(input: NewTaskGroup!):
|
createTaskGroup(input: NewTaskGroup!):
|
||||||
TaskGroup! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
|
TaskGroup! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
|
||||||
@ -856,4 +943,3 @@ type DeleteUserAccountPayload {
|
|||||||
ok: Boolean!
|
ok: Boolean!
|
||||||
userAccount: UserAccount!
|
userAccount: UserAccount!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
@ -17,12 +17,11 @@ import (
|
|||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
"github.com/jordanknott/taskcafe/internal/logger"
|
"github.com/jordanknott/taskcafe/internal/logger"
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
gomail "gopkg.in/mail.v2"
|
|
||||||
|
|
||||||
hermes "github.com/matcornic/hermes/v2"
|
hermes "github.com/matcornic/hermes/v2"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
gomail "gopkg.in/mail.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *labelColorResolver) ID(ctx context.Context, obj *db.LabelColor) (uuid.UUID, error) {
|
func (r *labelColorResolver) ID(ctx context.Context, obj *db.LabelColor) (uuid.UUID, error) {
|
||||||
@ -363,6 +362,28 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.T
|
|||||||
createdAt := time.Now().UTC()
|
createdAt := time.Now().UTC()
|
||||||
logger.New(ctx).WithFields(log.Fields{"positon": input.Position, "taskGroupID": input.TaskGroupID}).Info("creating task")
|
logger.New(ctx).WithFields(log.Fields{"positon": input.Position, "taskGroupID": input.TaskGroupID}).Info("creating task")
|
||||||
task, err := r.Repository.CreateTask(ctx, db.CreateTaskParams{input.TaskGroupID, createdAt, input.Name, input.Position})
|
task, err := r.Repository.CreateTask(ctx, db.CreateTaskParams{input.TaskGroupID, createdAt, input.Name, input.Position})
|
||||||
|
if err != nil {
|
||||||
|
logger.New(ctx).WithError(err).Error("issue while creating task")
|
||||||
|
return &db.Task{}, err
|
||||||
|
}
|
||||||
|
taskGroup, err := r.Repository.GetTaskGroupByID(ctx, input.TaskGroupID)
|
||||||
|
if err != nil {
|
||||||
|
logger.New(ctx).WithError(err).Error("issue while creating task")
|
||||||
|
return &db.Task{}, err
|
||||||
|
}
|
||||||
|
data := map[string]string{
|
||||||
|
"TaskGroup": taskGroup.Name,
|
||||||
|
}
|
||||||
|
userID, _ := GetUserID(ctx)
|
||||||
|
d, err := json.Marshal(data)
|
||||||
|
_, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{
|
||||||
|
TaskID: task.TaskID,
|
||||||
|
Data: d,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
CausedBy: userID,
|
||||||
|
ActivityTypeID: 1,
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.New(ctx).WithError(err).Error("issue while creating task")
|
logger.New(ctx).WithError(err).Error("issue while creating task")
|
||||||
return &db.Task{}, err
|
return &db.Task{}, err
|
||||||
@ -387,12 +408,44 @@ func (r *mutationResolver) UpdateTaskDescription(ctx context.Context, input Upda
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) UpdateTaskLocation(ctx context.Context, input NewTaskLocation) (*UpdateTaskLocationPayload, error) {
|
func (r *mutationResolver) UpdateTaskLocation(ctx context.Context, input NewTaskLocation) (*UpdateTaskLocationPayload, error) {
|
||||||
|
userID, _ := GetUserID(ctx)
|
||||||
previousTask, err := r.Repository.GetTaskByID(ctx, input.TaskID)
|
previousTask, err := r.Repository.GetTaskByID(ctx, input.TaskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &UpdateTaskLocationPayload{}, err
|
return &UpdateTaskLocationPayload{}, err
|
||||||
}
|
}
|
||||||
task, err := r.Repository.UpdateTaskLocation(ctx, db.UpdateTaskLocationParams{input.TaskID, input.TaskGroupID, input.Position})
|
task, _ := r.Repository.UpdateTaskLocation(ctx, db.UpdateTaskLocationParams{TaskID: input.TaskID, TaskGroupID: input.TaskGroupID, Position: input.Position})
|
||||||
|
if previousTask.TaskGroupID != input.TaskGroupID {
|
||||||
|
skipAndDelete := false
|
||||||
|
lastMove, err := r.Repository.GetLastMoveForTaskID(ctx, input.TaskID)
|
||||||
|
if err == nil {
|
||||||
|
if lastMove.Active && lastMove.PrevTaskGroupID == input.TaskGroupID.String() {
|
||||||
|
skipAndDelete = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if skipAndDelete {
|
||||||
|
_ = r.Repository.SetInactiveLastMoveForTaskID(ctx, input.TaskID)
|
||||||
|
} else {
|
||||||
|
prevTaskGroup, _ := r.Repository.GetTaskGroupByID(ctx, previousTask.TaskGroupID)
|
||||||
|
curTaskGroup, _ := r.Repository.GetTaskGroupByID(ctx, input.TaskGroupID)
|
||||||
|
|
||||||
|
data := map[string]string{
|
||||||
|
"PrevTaskGroup": prevTaskGroup.Name,
|
||||||
|
"PrevTaskGroupID": prevTaskGroup.TaskGroupID.String(),
|
||||||
|
"CurTaskGroup": curTaskGroup.Name,
|
||||||
|
"CurTaskGroupID": curTaskGroup.TaskGroupID.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
createdAt := time.Now().UTC()
|
||||||
|
d, _ := json.Marshal(data)
|
||||||
|
_, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{
|
||||||
|
TaskID: task.TaskID,
|
||||||
|
Data: d,
|
||||||
|
CausedBy: userID,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
ActivityTypeID: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
return &UpdateTaskLocationPayload{Task: &task, PreviousTaskGroupID: previousTask.TaskGroupID}, err
|
return &UpdateTaskLocationPayload{Task: &task, PreviousTaskGroupID: previousTask.TaskGroupID}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,6 +456,21 @@ func (r *mutationResolver) UpdateTaskName(ctx context.Context, input UpdateTaskN
|
|||||||
|
|
||||||
func (r *mutationResolver) SetTaskComplete(ctx context.Context, input SetTaskComplete) (*db.Task, error) {
|
func (r *mutationResolver) SetTaskComplete(ctx context.Context, input SetTaskComplete) (*db.Task, error) {
|
||||||
completedAt := time.Now().UTC()
|
completedAt := time.Now().UTC()
|
||||||
|
data := map[string]string{}
|
||||||
|
activityType := TASK_MARK_INCOMPLETE
|
||||||
|
if input.Complete {
|
||||||
|
activityType = TASK_MARK_COMPLETE
|
||||||
|
}
|
||||||
|
createdAt := time.Now().UTC()
|
||||||
|
userID, _ := GetUserID(ctx)
|
||||||
|
d, err := json.Marshal(data)
|
||||||
|
_, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{
|
||||||
|
TaskID: input.TaskID,
|
||||||
|
Data: d,
|
||||||
|
CausedBy: userID,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
ActivityTypeID: activityType,
|
||||||
|
})
|
||||||
task, err := r.Repository.SetTaskComplete(ctx, db.SetTaskCompleteParams{TaskID: input.TaskID, Complete: input.Complete, CompletedAt: sql.NullTime{Time: completedAt, Valid: true}})
|
task, err := r.Repository.SetTaskComplete(ctx, db.SetTaskCompleteParams{TaskID: input.TaskID, Complete: input.Complete, CompletedAt: sql.NullTime{Time: completedAt, Valid: true}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &db.Task{}, err
|
return &db.Task{}, err
|
||||||
@ -411,6 +479,23 @@ func (r *mutationResolver) SetTaskComplete(ctx context.Context, input SetTaskCom
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTaskDueDate) (*db.Task, error) {
|
func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTaskDueDate) (*db.Task, error) {
|
||||||
|
userID, _ := GetUserID(ctx)
|
||||||
|
prevTask, err := r.Repository.GetTaskByID(ctx, input.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
return &db.Task{}, err
|
||||||
|
}
|
||||||
|
data := map[string]string{}
|
||||||
|
var activityType = TASK_DUE_DATE_ADDED
|
||||||
|
if input.DueDate == nil && prevTask.DueDate.Valid {
|
||||||
|
activityType = TASK_DUE_DATE_REMOVED
|
||||||
|
data["PrevDueDate"] = prevTask.DueDate.Time.String()
|
||||||
|
} else if prevTask.DueDate.Valid {
|
||||||
|
activityType = TASK_DUE_DATE_CHANGED
|
||||||
|
data["PrevDueDate"] = prevTask.DueDate.Time.String()
|
||||||
|
data["CurDueDate"] = input.DueDate.String()
|
||||||
|
} else {
|
||||||
|
data["DueDate"] = input.DueDate.String()
|
||||||
|
}
|
||||||
var dueDate sql.NullTime
|
var dueDate sql.NullTime
|
||||||
if input.DueDate == nil {
|
if input.DueDate == nil {
|
||||||
dueDate = sql.NullTime{Valid: false, Time: time.Now()}
|
dueDate = sql.NullTime{Valid: false, Time: time.Now()}
|
||||||
@ -421,6 +506,15 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa
|
|||||||
TaskID: input.TaskID,
|
TaskID: input.TaskID,
|
||||||
DueDate: dueDate,
|
DueDate: dueDate,
|
||||||
})
|
})
|
||||||
|
createdAt := time.Now().UTC()
|
||||||
|
d, err := json.Marshal(data)
|
||||||
|
_, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{
|
||||||
|
TaskID: task.TaskID,
|
||||||
|
Data: d,
|
||||||
|
CausedBy: userID,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
ActivityTypeID: activityType,
|
||||||
|
})
|
||||||
|
|
||||||
return &task, err
|
return &task, err
|
||||||
}
|
}
|
||||||
@ -562,6 +656,33 @@ func (r *mutationResolver) UpdateTaskChecklistItemLocation(ctx context.Context,
|
|||||||
return &UpdateTaskChecklistItemLocationPayload{PrevChecklistID: currentChecklistItem.TaskChecklistID, TaskChecklistID: input.TaskChecklistID, ChecklistItem: &checklistItem}, err
|
return &UpdateTaskChecklistItemLocationPayload{PrevChecklistID: currentChecklistItem.TaskChecklistID, TaskChecklistID: input.TaskChecklistID, ChecklistItem: &checklistItem}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) CreateTaskComment(ctx context.Context, input *CreateTaskComment) (*CreateTaskCommentPayload, error) {
|
||||||
|
userID, _ := GetUserID(ctx)
|
||||||
|
createdAt := time.Now().UTC()
|
||||||
|
comment, err := r.Repository.CreateTaskComment(ctx, db.CreateTaskCommentParams{
|
||||||
|
TaskID: input.TaskID,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
CreatedBy: userID,
|
||||||
|
Message: input.Message,
|
||||||
|
})
|
||||||
|
return &CreateTaskCommentPayload{Comment: &comment, TaskID: input.TaskID}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) DeleteTaskComment(ctx context.Context, input *DeleteTaskComment) (*DeleteTaskCommentPayload, error) {
|
||||||
|
task, err := r.Repository.DeleteTaskCommentByID(ctx, input.CommentID)
|
||||||
|
return &DeleteTaskCommentPayload{TaskID: task.TaskID, CommentID: input.CommentID}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) UpdateTaskComment(ctx context.Context, input *UpdateTaskComment) (*UpdateTaskCommentPayload, error) {
|
||||||
|
updatedAt := time.Now().UTC()
|
||||||
|
comment, err := r.Repository.UpdateTaskComment(ctx, db.UpdateTaskCommentParams{
|
||||||
|
TaskCommentID: input.CommentID,
|
||||||
|
UpdatedAt: sql.NullTime{Valid: true, Time: updatedAt},
|
||||||
|
Message: input.Message,
|
||||||
|
})
|
||||||
|
return &UpdateTaskCommentPayload{Comment: &comment}, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*db.TaskGroup, error) {
|
func (r *mutationResolver) CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*db.TaskGroup, error) {
|
||||||
createdAt := time.Now().UTC()
|
createdAt := time.Now().UTC()
|
||||||
project, err := r.Repository.CreateTaskGroup(ctx,
|
project, err := r.Repository.CreateTaskGroup(ctx,
|
||||||
@ -1558,6 +1679,80 @@ func (r *taskResolver) Badges(ctx context.Context, obj *db.Task) (*TaskBadges, e
|
|||||||
return &TaskBadges{Checklist: &ChecklistBadge{Total: total, Complete: complete}}, nil
|
return &TaskBadges{Checklist: &ChecklistBadge{Total: total, Complete: complete}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *taskResolver) Activity(ctx context.Context, obj *db.Task) ([]db.TaskActivity, error) {
|
||||||
|
activity, err := r.Repository.GetActivityForTaskID(ctx, obj.TaskID)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return []db.TaskActivity{}, nil
|
||||||
|
}
|
||||||
|
return activity, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskResolver) Comments(ctx context.Context, obj *db.Task) ([]db.TaskComment, error) {
|
||||||
|
comments, err := r.Repository.GetCommentsForTaskID(ctx, obj.TaskID)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return []db.TaskComment{}, nil
|
||||||
|
}
|
||||||
|
return comments, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskActivityResolver) ID(ctx context.Context, obj *db.TaskActivity) (uuid.UUID, error) {
|
||||||
|
return obj.TaskActivityID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskActivityResolver) Type(ctx context.Context, obj *db.TaskActivity) (ActivityType, error) {
|
||||||
|
switch obj.ActivityTypeID {
|
||||||
|
case 1:
|
||||||
|
return ActivityTypeTaskAdded, nil
|
||||||
|
case 2:
|
||||||
|
return ActivityTypeTaskMoved, nil
|
||||||
|
case 3:
|
||||||
|
return ActivityTypeTaskMarkedComplete, nil
|
||||||
|
case 4:
|
||||||
|
return ActivityTypeTaskMarkedIncomplete, nil
|
||||||
|
case 5:
|
||||||
|
return ActivityTypeTaskDueDateChanged, nil
|
||||||
|
case 6:
|
||||||
|
return ActivityTypeTaskDueDateAdded, nil
|
||||||
|
case 7:
|
||||||
|
return ActivityTypeTaskDueDateRemoved, nil
|
||||||
|
case 8:
|
||||||
|
return ActivityTypeTaskChecklistChanged, nil
|
||||||
|
case 9:
|
||||||
|
return ActivityTypeTaskChecklistAdded, nil
|
||||||
|
case 10:
|
||||||
|
return ActivityTypeTaskChecklistRemoved, nil
|
||||||
|
default:
|
||||||
|
return ActivityTypeTaskAdded, errors.New("unknown type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskActivityResolver) Data(ctx context.Context, obj *db.TaskActivity) ([]TaskActivityData, error) {
|
||||||
|
var data map[string]string
|
||||||
|
_ = json.Unmarshal(obj.Data, &data)
|
||||||
|
activity := []TaskActivityData{}
|
||||||
|
for name, value := range data {
|
||||||
|
activity = append(activity, TaskActivityData{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return activity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskActivityResolver) CausedBy(ctx context.Context, obj *db.TaskActivity) (*CausedBy, error) {
|
||||||
|
user, err := r.Repository.GetUserAccountByID(ctx, obj.CausedBy)
|
||||||
|
var url *string
|
||||||
|
if user.ProfileAvatarUrl.Valid {
|
||||||
|
url = &user.ProfileAvatarUrl.String
|
||||||
|
}
|
||||||
|
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
|
||||||
|
return &CausedBy{
|
||||||
|
ID: obj.CausedBy,
|
||||||
|
FullName: user.FullName,
|
||||||
|
ProfileIcon: profileIcon,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *taskChecklistResolver) ID(ctx context.Context, obj *db.TaskChecklist) (uuid.UUID, error) {
|
func (r *taskChecklistResolver) ID(ctx context.Context, obj *db.TaskChecklist) (uuid.UUID, error) {
|
||||||
return obj.TaskChecklistID, nil
|
return obj.TaskChecklistID, nil
|
||||||
}
|
}
|
||||||
@ -1574,6 +1769,31 @@ func (r *taskChecklistItemResolver) DueDate(ctx context.Context, obj *db.TaskChe
|
|||||||
panic(fmt.Errorf("not implemented"))
|
panic(fmt.Errorf("not implemented"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *taskCommentResolver) ID(ctx context.Context, obj *db.TaskComment) (uuid.UUID, error) {
|
||||||
|
return obj.TaskCommentID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskCommentResolver) UpdatedAt(ctx context.Context, obj *db.TaskComment) (*time.Time, error) {
|
||||||
|
if obj.UpdatedAt.Valid {
|
||||||
|
return &obj.UpdatedAt.Time, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskCommentResolver) CreatedBy(ctx context.Context, obj *db.TaskComment) (*CreatedBy, error) {
|
||||||
|
user, err := r.Repository.GetUserAccountByID(ctx, obj.CreatedBy)
|
||||||
|
var url *string
|
||||||
|
if user.ProfileAvatarUrl.Valid {
|
||||||
|
url = &user.ProfileAvatarUrl.String
|
||||||
|
}
|
||||||
|
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
|
||||||
|
return &CreatedBy{
|
||||||
|
ID: obj.CreatedBy,
|
||||||
|
FullName: user.FullName,
|
||||||
|
ProfileIcon: profileIcon,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *taskGroupResolver) ID(ctx context.Context, obj *db.TaskGroup) (uuid.UUID, error) {
|
func (r *taskGroupResolver) ID(ctx context.Context, obj *db.TaskGroup) (uuid.UUID, error) {
|
||||||
return obj.TaskGroupID, nil
|
return obj.TaskGroupID, nil
|
||||||
}
|
}
|
||||||
@ -1619,6 +1839,7 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
|
|||||||
if user.ProfileAvatarUrl.Valid {
|
if user.ProfileAvatarUrl.Valid {
|
||||||
url = &user.ProfileAvatarUrl.String
|
url = &user.ProfileAvatarUrl.String
|
||||||
}
|
}
|
||||||
|
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
|
||||||
role, err := r.Repository.GetRoleForTeamMember(ctx, db.GetRoleForTeamMemberParams{UserID: user.UserID, TeamID: obj.TeamID})
|
role, err := r.Repository.GetRoleForTeamMember(ctx, db.GetRoleForTeamMemberParams{UserID: user.UserID, TeamID: obj.TeamID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.New(ctx).WithError(err).Error("get role for projet member by user ID")
|
logger.New(ctx).WithError(err).Error("get role for projet member by user ID")
|
||||||
@ -1634,7 +1855,6 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
|
|||||||
return members, err
|
return members, err
|
||||||
}
|
}
|
||||||
|
|
||||||
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
|
|
||||||
members = append(members, Member{ID: user.UserID, FullName: user.FullName, ProfileIcon: profileIcon,
|
members = append(members, Member{ID: user.UserID, FullName: user.FullName, ProfileIcon: profileIcon,
|
||||||
Username: user.Username, Owned: ownedList, Member: memberList, Role: &db.Role{Code: role.Code, Name: role.Name},
|
Username: user.Username, Owned: ownedList, Member: memberList, Role: &db.Role{Code: role.Code, Name: role.Name},
|
||||||
})
|
})
|
||||||
@ -1724,6 +1944,9 @@ func (r *Resolver) RefreshToken() RefreshTokenResolver { return &refreshTokenRes
|
|||||||
// Task returns TaskResolver implementation.
|
// Task returns TaskResolver implementation.
|
||||||
func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
|
func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
|
||||||
|
|
||||||
|
// TaskActivity returns TaskActivityResolver implementation.
|
||||||
|
func (r *Resolver) TaskActivity() TaskActivityResolver { return &taskActivityResolver{r} }
|
||||||
|
|
||||||
// TaskChecklist returns TaskChecklistResolver implementation.
|
// TaskChecklist returns TaskChecklistResolver implementation.
|
||||||
func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} }
|
func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} }
|
||||||
|
|
||||||
@ -1732,6 +1955,9 @@ func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver {
|
|||||||
return &taskChecklistItemResolver{r}
|
return &taskChecklistItemResolver{r}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TaskComment returns TaskCommentResolver implementation.
|
||||||
|
func (r *Resolver) TaskComment() TaskCommentResolver { return &taskCommentResolver{r} }
|
||||||
|
|
||||||
// TaskGroup returns TaskGroupResolver implementation.
|
// TaskGroup returns TaskGroupResolver implementation.
|
||||||
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
|
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
|
||||||
|
|
||||||
@ -1753,27 +1979,11 @@ type projectLabelResolver struct{ *Resolver }
|
|||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
type refreshTokenResolver struct{ *Resolver }
|
type refreshTokenResolver struct{ *Resolver }
|
||||||
type taskResolver struct{ *Resolver }
|
type taskResolver struct{ *Resolver }
|
||||||
|
type taskActivityResolver struct{ *Resolver }
|
||||||
type taskChecklistResolver struct{ *Resolver }
|
type taskChecklistResolver struct{ *Resolver }
|
||||||
type taskChecklistItemResolver struct{ *Resolver }
|
type taskChecklistItemResolver struct{ *Resolver }
|
||||||
|
type taskCommentResolver struct{ *Resolver }
|
||||||
type taskGroupResolver struct{ *Resolver }
|
type taskGroupResolver struct{ *Resolver }
|
||||||
type taskLabelResolver struct{ *Resolver }
|
type taskLabelResolver struct{ *Resolver }
|
||||||
type teamResolver struct{ *Resolver }
|
type teamResolver struct{ *Resolver }
|
||||||
type userAccountResolver struct{ *Resolver }
|
type userAccountResolver struct{ *Resolver }
|
||||||
|
|
||||||
// !!! WARNING !!!
|
|
||||||
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
|
|
||||||
// one last chance to move it out of harms way if you want. There are two reasons this happens:
|
|
||||||
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
|
|
||||||
// it when you're done.
|
|
||||||
// - You have helper methods in this file. Move them out to keep these resolver files clean.
|
|
||||||
type MemberType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
MemberTypeInvited MemberType = "INVITED"
|
|
||||||
MemberTypeJoined MemberType = "JOINED"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MasterEntry struct {
|
|
||||||
MemberType MemberType
|
|
||||||
ID uuid.UUID
|
|
||||||
}
|
|
||||||
|
@ -135,6 +135,38 @@ type TaskBadges {
|
|||||||
checklist: ChecklistBadge
|
checklist: ChecklistBadge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CausedBy {
|
||||||
|
id: ID!
|
||||||
|
fullName: String!
|
||||||
|
profileIcon: ProfileIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskActivityData {
|
||||||
|
name: String!
|
||||||
|
value: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActivityType {
|
||||||
|
TASK_ADDED
|
||||||
|
TASK_MOVED
|
||||||
|
TASK_MARKED_COMPLETE
|
||||||
|
TASK_MARKED_INCOMPLETE
|
||||||
|
TASK_DUE_DATE_CHANGED
|
||||||
|
TASK_DUE_DATE_ADDED
|
||||||
|
TASK_DUE_DATE_REMOVED
|
||||||
|
TASK_CHECKLIST_CHANGED
|
||||||
|
TASK_CHECKLIST_ADDED
|
||||||
|
TASK_CHECKLIST_REMOVED
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskActivity {
|
||||||
|
id: ID!
|
||||||
|
type: ActivityType!
|
||||||
|
data: [TaskActivityData!]!
|
||||||
|
causedBy: CausedBy!
|
||||||
|
createdAt: Time!
|
||||||
|
}
|
||||||
|
|
||||||
type Task {
|
type Task {
|
||||||
id: ID!
|
id: ID!
|
||||||
taskGroup: TaskGroup!
|
taskGroup: TaskGroup!
|
||||||
@ -149,6 +181,23 @@ type Task {
|
|||||||
labels: [TaskLabel!]!
|
labels: [TaskLabel!]!
|
||||||
checklists: [TaskChecklist!]!
|
checklists: [TaskChecklist!]!
|
||||||
badges: TaskBadges!
|
badges: TaskBadges!
|
||||||
|
activity: [TaskActivity!]!
|
||||||
|
comments: [TaskComment!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatedBy {
|
||||||
|
id: ID!
|
||||||
|
fullName: String!
|
||||||
|
profileIcon: ProfileIcon!
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskComment {
|
||||||
|
id: ID!
|
||||||
|
createdAt: Time!
|
||||||
|
updatedAt: Time
|
||||||
|
message: String!
|
||||||
|
createdBy: CreatedBy!
|
||||||
|
pinned: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Organization {
|
type Organization {
|
||||||
|
37
internal/graph/schema/task_comment.gql
Normal file
37
internal/graph/schema/task_comment.gql
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
extend type Mutation {
|
||||||
|
createTaskComment(input: CreateTaskComment):
|
||||||
|
CreateTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||||
|
deleteTaskComment(input: DeleteTaskComment):
|
||||||
|
DeleteTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||||
|
updateTaskComment(input: UpdateTaskComment):
|
||||||
|
UpdateTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateTaskComment {
|
||||||
|
taskID: UUID!
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTaskCommentPayload {
|
||||||
|
taskID: UUID!
|
||||||
|
comment: TaskComment!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateTaskComment {
|
||||||
|
commentID: UUID!
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateTaskCommentPayload {
|
||||||
|
taskID: UUID!
|
||||||
|
comment: TaskComment!
|
||||||
|
}
|
||||||
|
|
||||||
|
input DeleteTaskComment {
|
||||||
|
commentID: UUID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteTaskCommentPayload {
|
||||||
|
taskID: UUID!
|
||||||
|
commentID: UUID!
|
||||||
|
}
|
27
migrations/0060_add-task_activity-table.up.sql
Normal file
27
migrations/0060_add-task_activity-table.up.sql
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
CREATE TABLE task_activity_type (
|
||||||
|
task_activity_type_id int PRIMARY KEY,
|
||||||
|
code text NOT NULL,
|
||||||
|
template text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO task_activity_type (task_activity_type_id, code, template) VALUES
|
||||||
|
(1, 'task_added_to_task_group', 'added this task to {{ index .Data "TaskGroup" }}'),
|
||||||
|
(2, 'task_moved_to_task_group', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'),
|
||||||
|
(3, 'task_mark_complete', 'marked this task complete'),
|
||||||
|
(4, 'task_mark_incomplete', 'marked this task incomplete'),
|
||||||
|
(5, 'task_due_date_changed', 'changed the due date to {{ index .Data "DueDate" }}'),
|
||||||
|
(6, 'task_due_date_added', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'),
|
||||||
|
(7, 'task_due_date_removed', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'),
|
||||||
|
(8, 'task_checklist_changed', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'),
|
||||||
|
(9, 'task_checklist_added', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'),
|
||||||
|
(10, 'task_checklist_removed', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}');
|
||||||
|
|
||||||
|
CREATE TABLE task_activity (
|
||||||
|
task_activity_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
active boolean NOT NULL DEFAULT true,
|
||||||
|
task_id uuid NOT NULL REFERENCES task(task_id),
|
||||||
|
created_at timestamptz NOT NULL,
|
||||||
|
caused_by uuid NOT NULL,
|
||||||
|
activity_type_id int NOT NULL REFERENCES task_activity_type(task_activity_type_id),
|
||||||
|
data jsonb
|
||||||
|
);
|
9
migrations/0061_add-task_comment-table.up.sql
Normal file
9
migrations/0061_add-task_comment-table.up.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE task_comment (
|
||||||
|
task_comment_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
task_id uuid NOT NULL REFERENCES task(task_id),
|
||||||
|
created_at timestamptz NOT NULL,
|
||||||
|
updated_at timestamptz,
|
||||||
|
created_by uuid NOT NULL REFERENCES user_account(user_id),
|
||||||
|
pinned boolean NOT NULL DEFAULT false,
|
||||||
|
message TEXT NOT NULL
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user