Compare commits
1 Commits
0.3.0
...
refactor/c
Author | SHA1 | Date | |
---|---|---|---|
2de48e288b |
@ -21,4 +21,4 @@ windows:
|
|||||||
- database:
|
- database:
|
||||||
root: ./
|
root: ./
|
||||||
panes:
|
panes:
|
||||||
- pgcli postgres://taskcafe:taskcafe_test@localhost:8855/taskcafe
|
- pgcli postgres://taskcafe:taskcafe_test@localhost:5432/taskcafe
|
||||||
|
@ -17,9 +17,8 @@ user = 'taskcafe'
|
|||||||
password = 'taskcafe_test'
|
password = 'taskcafe_test'
|
||||||
|
|
||||||
[smtp]
|
[smtp]
|
||||||
username = 'taskcafe@example.com'
|
username = 'admin@example.com'
|
||||||
password = ''
|
password = 'example'
|
||||||
from = 'no-reply@taskcafe.com'
|
server = 'mail.example.com'
|
||||||
host = 'localhost'
|
port = 465
|
||||||
port = 11500
|
connection_security = 'STARTTLS'
|
||||||
skip_verify = false
|
|
||||||
|
@ -9,8 +9,6 @@
|
|||||||
"@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",
|
||||||
@ -37,16 +35,12 @@
|
|||||||
"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",
|
||||||
@ -54,7 +48,6 @@
|
|||||||
"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",
|
||||||
|
@ -174,7 +174,7 @@ const AdminRoute = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Admin | Taskcafé';
|
document.title = 'Admin | Taskcafé';
|
||||||
}, []);
|
}, []);
|
||||||
const { loading, data } = useUsersQuery({ fetchPolicy: 'cache-and-network' });
|
const { loading, data } = useUsersQuery();
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const [deleteInvitedUser] = useDeleteInvitedUserAccountMutation({
|
const [deleteInvitedUser] = useDeleteInvitedUserAccountMutation({
|
||||||
@ -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({
|
||||||
@ -214,6 +214,9 @@ const AdminRoute = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (loading) {
|
||||||
|
return <GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />;
|
||||||
|
}
|
||||||
if (data && user) {
|
if (data && user) {
|
||||||
if (user.roles.org !== 'admin') {
|
if (user.roles.org !== 'admin') {
|
||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
@ -256,7 +259,7 @@ const AdminRoute = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />;
|
return <span>error</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdminRoute;
|
export default AdminRoute;
|
||||||
|
@ -25,11 +25,6 @@ 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);
|
||||||
|
@ -128,10 +128,12 @@ const TeamProjectContainer = styled.div`
|
|||||||
const colors = [theme.colors.primary, theme.colors.secondary];
|
const colors = [theme.colors.primary, theme.colors.secondary];
|
||||||
|
|
||||||
const ProjectFinder = () => {
|
const ProjectFinder = () => {
|
||||||
const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'cache-and-network' });
|
const { loading, data } = useGetProjectsQuery();
|
||||||
|
if (loading) {
|
||||||
|
return <span>loading</span>;
|
||||||
|
}
|
||||||
if (data) {
|
if (data) {
|
||||||
const { projects, teams } = data;
|
const { projects, teams } = data;
|
||||||
const personalProjects = data.projects.filter(p => p.team === null);
|
|
||||||
const projectTeams = teams.map(team => {
|
const projectTeams = teams.map(team => {
|
||||||
return {
|
return {
|
||||||
id: team.id,
|
id: team.id,
|
||||||
@ -141,22 +143,6 @@ const ProjectFinder = () => {
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TeamContainer>
|
|
||||||
<TeamTitle>Personal</TeamTitle>
|
|
||||||
<TeamProjects>
|
|
||||||
{personalProjects.map((project, idx) => (
|
|
||||||
<TeamProjectContainer key={project.id}>
|
|
||||||
<TeamProjectLink to={`/projects/${project.id}`}>
|
|
||||||
<TeamProjectBackground color={colors[idx % 5]} />
|
|
||||||
<TeamProjectAvatar color={colors[idx % 5]} />
|
|
||||||
<TeamProjectContent>
|
|
||||||
<TeamProjectTitle>{project.name}</TeamProjectTitle>
|
|
||||||
</TeamProjectContent>
|
|
||||||
</TeamProjectLink>
|
|
||||||
</TeamProjectContainer>
|
|
||||||
))}
|
|
||||||
</TeamProjects>
|
|
||||||
</TeamContainer>
|
|
||||||
{projectTeams.map(team => (
|
{projectTeams.map(team => (
|
||||||
<TeamContainer key={team.id}>
|
<TeamContainer key={team.id}>
|
||||||
<TeamTitle>{team.name}</TeamTitle>
|
<TeamTitle>{team.name}</TeamTitle>
|
||||||
@ -178,10 +164,10 @@ const ProjectFinder = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <span>loading</span>;
|
return <span>error</span>;
|
||||||
};
|
};
|
||||||
type ProjectPopupProps = {
|
type ProjectPopupProps = {
|
||||||
history: any;
|
history: History<History.PoorMansUnknown>;
|
||||||
name: string;
|
name: string;
|
||||||
projectID: string;
|
projectID: string;
|
||||||
};
|
};
|
||||||
@ -196,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,6 +46,10 @@ 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,11 +296,9 @@ 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 },
|
||||||
@ -315,9 +313,7 @@ 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 },
|
||||||
);
|
);
|
||||||
@ -336,7 +332,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 = [];
|
||||||
@ -352,9 +348,7 @@ 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 },
|
||||||
);
|
);
|
||||||
@ -370,24 +364,19 @@ 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) {
|
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
|
||||||
const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(t => t.id === task.id);
|
(t: Task) => t.id !== task.id,
|
||||||
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
|
);
|
||||||
(t: Task) => t.id !== task.id,
|
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
|
||||||
);
|
...taskGroups[newTaskGroupIdx].tasks,
|
||||||
if (previousTask) {
|
{ ...task },
|
||||||
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
|
];
|
||||||
...taskGroups[newTaskGroupIdx].tasks,
|
|
||||||
{ ...previousTask },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -459,6 +448,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <BoardLoading />;
|
||||||
|
}
|
||||||
const getTaskStatusFilterLabel = (filter: TaskStatusFilter) => {
|
const getTaskStatusFilterLabel = (filter: TaskStatusFilter) => {
|
||||||
if (filter.status === TaskStatus.COMPLETE) {
|
if (filter.status === TaskStatus.COMPLETE) {
|
||||||
return 'Complete';
|
return 'Complete';
|
||||||
@ -815,7 +807,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BoardLoading />;
|
return <span>Error</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectBoard;
|
export default ProjectBoard;
|
||||||
|
@ -21,9 +21,6 @@ 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';
|
||||||
@ -36,73 +33,6 @@ 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 (
|
||||||
@ -200,40 +130,6 @@ 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) => {
|
||||||
@ -242,23 +138,21 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
cache =>
|
cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
if (response.data) {
|
const { prevChecklistID, checklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
|
||||||
const { prevChecklistID, taskChecklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
|
if (checklistID !== prevChecklistID) {
|
||||||
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) {
|
draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter(
|
||||||
draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter(
|
i => i.id !== checklistItem.id,
|
||||||
i => i.id !== checklistItem.id,
|
);
|
||||||
);
|
draftCache.findTask.checklists[newIdx].items.push({
|
||||||
draftCache.findTask.checklists[newIdx].items.push({
|
...item,
|
||||||
...item,
|
position: checklistItem.position,
|
||||||
position: checklistItem.position,
|
taskChecklistID: checklistID,
|
||||||
taskChecklistID,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -294,7 +188,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 = {
|
||||||
@ -318,10 +212,8 @@ 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 },
|
||||||
);
|
);
|
||||||
@ -335,21 +227,19 @@ 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) {
|
draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(
|
||||||
draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(
|
c => item.id !== c.id,
|
||||||
c => item.id !== c.id,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
|
||||||
draftCache.findTask.badges.checklist = {
|
|
||||||
__typename: 'ChecklistBadge',
|
|
||||||
complete,
|
|
||||||
total,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
||||||
|
draftCache.findTask.badges.checklist = {
|
||||||
|
__typename: 'ChecklistBadge',
|
||||||
|
complete,
|
||||||
|
total,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
{ taskID },
|
{ taskID },
|
||||||
);
|
);
|
||||||
@ -362,30 +252,24 @@ 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);
|
if (idx !== -1) {
|
||||||
if (idx !== -1) {
|
draftCache.findTask.checklists[idx].items.push({ ...item });
|
||||||
draftCache.findTask.checklists[idx].items.push({ ...item });
|
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
||||||
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
draftCache.findTask.badges.checklist = {
|
||||||
draftCache.findTask.badges.checklist = {
|
__typename: 'ChecklistBadge',
|
||||||
__typename: 'ChecklistBadge',
|
complete,
|
||||||
complete,
|
total,
|
||||||
total,
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ taskID },
|
{ taskID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { loading, data, refetch } = useFindTaskQuery({
|
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
|
||||||
variables: { taskID },
|
|
||||||
pollInterval: 3000,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
});
|
|
||||||
const [setTaskComplete] = useSetTaskCompleteMutation();
|
const [setTaskComplete] = useSetTaskCompleteMutation();
|
||||||
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
|
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
@ -405,8 +289,9 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
refreshCache();
|
refreshCache();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [updateTaskComment] = useUpdateTaskCommentMutation();
|
if (loading) {
|
||||||
const [editableComment, setEditableComment] = useState<null | string>(null);
|
return null;
|
||||||
|
}
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -420,31 +305,8 @@ 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,9 +36,7 @@ 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,
|
||||||
@ -55,7 +53,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,6 +32,7 @@ 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';
|
||||||
@ -134,6 +135,7 @@ 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 [];
|
||||||
}
|
}
|
||||||
@ -161,10 +163,12 @@ 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: {
|
||||||
@ -176,15 +180,17 @@ const fetchMembers = async (client: any, projectID: string, options: MemberFilte
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
console.log(`${m.user.email} is added`);
|
||||||
|
emails.push(m.user.email);
|
||||||
|
return {
|
||||||
|
label: m.user.fullName,
|
||||||
|
value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
emails.push(m.user.email);
|
|
||||||
return {
|
|
||||||
label: m.user.fullName,
|
|
||||||
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)) {
|
||||||
@ -237,6 +243,7 @@ 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
|
||||||
@ -416,16 +423,14 @@ 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 },
|
||||||
@ -436,7 +441,6 @@ const Project = () => {
|
|||||||
|
|
||||||
const { loading, data, error } = useFindProjectQuery({
|
const { loading, data, error } = useFindProjectQuery({
|
||||||
variables: { projectID },
|
variables: { projectID },
|
||||||
pollInterval: 3000,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateProjectName] = useUpdateProjectNameMutation({
|
const [updateProjectName] = useUpdateProjectNameMutation({
|
||||||
@ -446,7 +450,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 },
|
||||||
);
|
);
|
||||||
@ -460,16 +464,14 @@ 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,
|
];
|
||||||
];
|
draftCache.findProject.invitedMembers = [
|
||||||
draftCache.findProject.invitedMembers = [
|
...cache.findProject.invitedMembers,
|
||||||
...cache.findProject.invitedMembers,
|
...response.data.inviteProjectMembers.invitedMembers,
|
||||||
...response.data.inviteProjectMembers.invitedMembers,
|
];
|
||||||
];
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
@ -483,7 +485,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 },
|
||||||
@ -498,7 +500,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 },
|
||||||
@ -517,6 +519,14 @@ const Project = () => {
|
|||||||
document.title = `${data.findProject.name} | Taskcafé`;
|
document.title = `${data.findProject.name} | Taskcafé`;
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GlobalTopNavbar onSaveProjectName={NOOP} name="" projectID={null} />
|
||||||
|
<BoardLoading />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
history.push('/projects');
|
history.push('/projects');
|
||||||
}
|
}
|
||||||
@ -629,12 +639,7 @@ const Project = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return <div>Error</div>;
|
||||||
<>
|
|
||||||
<GlobalTopNavbar onSaveProjectName={NOOP} name="" projectID={null} />
|
|
||||||
<BoardLoading />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Project;
|
export default Project;
|
||||||
|
@ -202,7 +202,7 @@ type ShowNewProject = {
|
|||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const { loading, data } = useGetProjectsQuery({ pollInterval: 3000, fetchPolicy: 'cache-and-network' });
|
const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only' });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Taskcafé';
|
document.title = 'Taskcafé';
|
||||||
}, []);
|
}, []);
|
||||||
@ -210,9 +210,7 @@ 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 });
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -224,13 +222,14 @@ const Projects = () => {
|
|||||||
update: (client, createData) => {
|
update: (client, createData) => {
|
||||||
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, draftCache => {
|
||||||
if (createData.data) {
|
draftCache.teams.push({ ...createData.data.createTeam });
|
||||||
draftCache.teams.push({ ...createData.data?.createTeam });
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (loading) {
|
||||||
|
return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
|
||||||
|
}
|
||||||
|
|
||||||
const colors = theme.colors.multiColors;
|
const colors = theme.colors.multiColors;
|
||||||
if (data && user) {
|
if (data && user) {
|
||||||
@ -392,7 +391,7 @@ const Projects = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
|
return <div>Error!</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Projects;
|
export default Projects;
|
||||||
|
@ -419,11 +419,7 @@ type MembersProps = {
|
|||||||
|
|
||||||
const Members: React.FC<MembersProps> = ({ teamID }) => {
|
const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const { loading, data } = useGetTeamQuery({
|
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
||||||
variables: { teamID },
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
pollInterval: 3000,
|
|
||||||
});
|
|
||||||
const { user, setUserRoles } = useCurrentUser();
|
const { user, setUserRoles } = useCurrentUser();
|
||||||
const warning =
|
const warning =
|
||||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||||
@ -434,13 +430,11 @@ 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 },
|
||||||
);
|
);
|
||||||
@ -465,13 +459,16 @@ 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 },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (loading) {
|
||||||
|
return <span>loading</span>;
|
||||||
|
}
|
||||||
|
|
||||||
if (data && user) {
|
if (data && user) {
|
||||||
return (
|
return (
|
||||||
@ -559,7 +556,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>loading</div>;
|
return <div>error</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Members;
|
export default Members;
|
||||||
|
@ -155,11 +155,10 @@ type TeamProjectsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
|
const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
|
||||||
const { loading, data } = useGetTeamQuery({
|
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
||||||
variables: { teamID },
|
if (loading) {
|
||||||
fetchPolicy: 'cache-and-network',
|
return <span>loading</span>;
|
||||||
pollInterval: 3000,
|
}
|
||||||
});
|
|
||||||
if (data) {
|
if (data) {
|
||||||
return (
|
return (
|
||||||
<ProjectsContainer>
|
<ProjectsContainer>
|
||||||
@ -190,7 +189,7 @@ const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
|
|||||||
</ProjectsContainer>
|
</ProjectsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <span>loading</span>;
|
return <span>error</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TeamProjects;
|
export default TeamProjects;
|
||||||
|
@ -33,7 +33,7 @@ const Wrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type TeamPopupProps = {
|
type TeamPopupProps = {
|
||||||
history: History<any>;
|
history: History<History.PoorMansUnknown>;
|
||||||
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,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -94,6 +94,23 @@ const Teams = () => {
|
|||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const [currentTab, setCurrentTab] = useState(0);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<GlobalTopNavbar
|
||||||
|
menuType={[
|
||||||
|
{ name: 'Projects', link: `${match.url}` },
|
||||||
|
{ name: 'Members', link: `${match.url}/members` },
|
||||||
|
]}
|
||||||
|
currentTab={currentTab}
|
||||||
|
onSetTab={tab => {
|
||||||
|
setCurrentTab(tab);
|
||||||
|
}}
|
||||||
|
onSaveProjectName={NOOP}
|
||||||
|
projectID={null}
|
||||||
|
name={null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (data && user) {
|
if (data && user) {
|
||||||
if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
|
if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
|
||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
@ -129,21 +146,7 @@ const Teams = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return <div>Error!</div>;
|
||||||
<GlobalTopNavbar
|
|
||||||
menuType={[
|
|
||||||
{ name: 'Projects', link: `${match.url}` },
|
|
||||||
{ name: 'Members', link: `${match.url}/members` },
|
|
||||||
]}
|
|
||||||
currentTab={currentTab}
|
|
||||||
onSetTab={tab => {
|
|
||||||
setCurrentTab(tab);
|
|
||||||
}}
|
|
||||||
onSaveProjectName={NOOP}
|
|
||||||
projectID={null}
|
|
||||||
name={null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Teams;
|
export default Teams;
|
||||||
|
@ -20,14 +20,14 @@ export const MemberManagerSearch = styled(TextareaAutosize)`
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
background: ${props => props.theme.colors.bg.secondary};
|
background: ${props => props.theme.colors.bgColor.secondary};
|
||||||
outline: none;
|
outline: none;
|
||||||
color: ${props => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
border-color: ${props => props.theme.colors.border};
|
border-color: ${props => props.theme.colors.border};
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
|
box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
|
||||||
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
|
background: ${props => mixin.darken(props.theme.colors.bgColor.secondary, 0.15)};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -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.find(d => d.value === team)}
|
value={options.filter(d => d.value === team)}
|
||||||
styles={colourStyles}
|
styles={colourStyles}
|
||||||
classNamePrefix="teamSelect"
|
classNamePrefix="teamSelect"
|
||||||
options={options}
|
options={options}
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
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;
|
|
@ -1,133 +0,0 @@
|
|||||||
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,7 +475,6 @@ 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;
|
||||||
@ -485,7 +484,7 @@ export const CommentProfile = styled(TaskAssignee)`
|
|||||||
align-items: normal;
|
align-items: normal;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CommentTextArea = styled(TextareaAutosize)<{ showCommentActions: boolean }>`
|
export const CommentTextArea = styled(TextareaAutosize)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
@ -496,16 +495,14 @@ export const CommentTextArea = styled(TextareaAutosize)<{ showCommentActions: bo
|
|||||||
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;
|
||||||
${props =>
|
&:not(:focus) {
|
||||||
props.showCommentActions
|
height: 36px;
|
||||||
? css`
|
}
|
||||||
min-height: 80px;
|
&:focus {
|
||||||
max-height: none;
|
min-height: 80px;
|
||||||
line-height: 20px;
|
max-height: none;
|
||||||
`
|
line-height: 20px;
|
||||||
: css`
|
}
|
||||||
height: 36px;
|
|
||||||
`}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CommentEditorActions = styled.div<{ visible: boolean }>`
|
export const CommentEditorActions = styled.div<{ visible: boolean }>`
|
||||||
@ -532,18 +529,6 @@ 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`
|
||||||
@ -552,32 +537,25 @@ 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<{ editable?: boolean }>`
|
export const ActivityItemHeader = styled.div`
|
||||||
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)`
|
||||||
align-items: start;
|
margin-right: 4px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
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 }>`
|
||||||
@ -590,10 +568,8 @@ export const ActivityItemDetails = styled.div`
|
|||||||
margin-left: 32px;
|
margin-left: 32px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActivityItemCommentContainer = styled.div``;
|
export const ActivityItemComment = 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;
|
||||||
@ -601,32 +577,6 @@ export const ActivityItemComment = styled.div<{ editable: boolean }>`
|
|||||||
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`
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
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,32 +12,17 @@ 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,
|
||||||
@ -73,126 +58,18 @@ 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``;
|
||||||
|
|
||||||
@ -237,13 +114,8 @@ 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;
|
||||||
@ -252,15 +124,11 @@ 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,
|
||||||
@ -269,7 +137,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
onDeleteItem,
|
onDeleteItem,
|
||||||
onDeleteTask,
|
onDeleteTask,
|
||||||
onCloseModal,
|
onCloseModal,
|
||||||
onUpdateComment,
|
|
||||||
onOpenAddMemberPopup,
|
onOpenAddMemberPopup,
|
||||||
onOpenAddLabelPopup,
|
onOpenAddLabelPopup,
|
||||||
onOpenDueDatePopop,
|
onOpenDueDatePopop,
|
||||||
@ -289,38 +156,12 @@ 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);
|
||||||
};
|
};
|
||||||
@ -584,29 +425,46 @@ 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 && (
|
||||||
<CommentCreator
|
<CommentInnerWrapper>
|
||||||
me={me}
|
<CommentProfile
|
||||||
onCreateComment={message => onCreateComment(task, message)}
|
member={me}
|
||||||
onMemberProfile={onMemberProfile}
|
size={32}
|
||||||
/>
|
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>
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
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,40 +10,6 @@ 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
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
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;
|
|
@ -1,11 +0,0 @@
|
|||||||
import gql from 'graphql-tag';
|
|
||||||
|
|
||||||
const CREATE_TASK_MUTATION = gql`
|
|
||||||
mutation deleteTaskComment($commentID: UUID!) {
|
|
||||||
deleteTaskComment(input: { commentID: $commentID }) {
|
|
||||||
commentID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default CREATE_TASK_MUTATION;
|
|
@ -1,15 +0,0 @@
|
|||||||
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,14 +16,10 @@ const useOnOutsideClick = (
|
|||||||
|
|
||||||
const handleMouseUp = (event: any) => {
|
const handleMouseUp = (event: any) => {
|
||||||
if (typeof $ignoredElementRefsMemoized !== 'undefined') {
|
if (typeof $ignoredElementRefsMemoized !== 'undefined') {
|
||||||
const isAnyIgnoredElementAncestorOfTarget = $ignoredElementRefsMemoized.some(($elementRef: any) => {
|
const isAnyIgnoredElementAncestorOfTarget = $ignoredElementRefsMemoized.some(
|
||||||
if ($elementRef && $elementRef.current) {
|
($elementRef: any) =>
|
||||||
return (
|
$elementRef.current.contains($mouseDownTargetRef.current) || $elementRef.current.contains(event.target),
|
||||||
$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,21 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon, { IconProps } from './Icon';
|
|
||||||
|
|
||||||
export const AngleDown: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
|
||||||
return (
|
|
||||||
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
width?: number | string;
|
width: number | string;
|
||||||
height?: number | string;
|
height: number | string;
|
||||||
color?: string;
|
color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AngleDownOld = ({ width, height, color }: Props) => {
|
const AngleDown = ({ 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
|
||||||
@ -26,10 +17,10 @@ const AngleDownOld = ({ width, height, color }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AngleDownOld.defaultProps = {
|
AngleDown.defaultProps = {
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 16,
|
height: 16,
|
||||||
color: '#000',
|
color: '#000',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AngleDownOld;
|
export default AngleDown;
|
||||||
|
@ -9,7 +9,7 @@ export function updateApolloCache<T>(
|
|||||||
update: UpdateCacheFn<T>,
|
update: UpdateCacheFn<T>,
|
||||||
variables?: object,
|
variables?: object,
|
||||||
) {
|
) {
|
||||||
let queryArgs: DataProxy.Query<any, any>;
|
let queryArgs: DataProxy.Query<any>;
|
||||||
if (variables) {
|
if (variables) {
|
||||||
queryArgs = {
|
queryArgs = {
|
||||||
query: document,
|
query: document,
|
||||||
|
@ -61,9 +61,9 @@ export const base = {
|
|||||||
export const dark = {
|
export const dark = {
|
||||||
...base,
|
...base,
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
text: `${theme.colors.text.primary}`,
|
text: `rgba(${theme.colors.text.primary})`,
|
||||||
code: `${theme.colors.text.primary}`,
|
code: `rgba(${theme.colors.text.primary})`,
|
||||||
cursor: `${theme.colors.text.primary}`,
|
cursor: `rgba(${theme.colors.text.primary})`,
|
||||||
divider: '#4E5C6E',
|
divider: '#4E5C6E',
|
||||||
placeholder: '#52657A',
|
placeholder: '#52657A',
|
||||||
|
|
||||||
|
49
frontend/src/types.d.ts
vendored
49
frontend/src/types.d.ts
vendored
@ -1,10 +1,3 @@
|
|||||||
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;
|
||||||
@ -63,39 +56,6 @@ 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;
|
||||||
@ -109,8 +69,6 @@ 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 = {
|
||||||
@ -131,3 +89,10 @@ type Team = {
|
|||||||
name: string;
|
name: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ProjectLabel = {
|
||||||
|
id: string;
|
||||||
|
createdDate: string;
|
||||||
|
name?: string | null;
|
||||||
|
labelColor: LabelColor;
|
||||||
|
};
|
||||||
|
9187
frontend/yarn.lock
9187
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
1
go.mod
1
go.mod
@ -23,6 +23,5 @@ 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,6 +388,7 @@ 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=
|
||||||
@ -891,8 +892,6 @@ 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=
|
||||||
|
@ -15,7 +15,6 @@ import (
|
|||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/jordanknott/taskcafe/internal/route"
|
"github.com/jordanknott/taskcafe/internal/route"
|
||||||
"github.com/jordanknott/taskcafe/internal/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -76,25 +75,11 @@ func newWebCmd() *cobra.Command {
|
|||||||
log.Warn("server.secret is not set, generating a random secret")
|
log.Warn("server.secret is not set, generating a random secret")
|
||||||
secret = uuid.New().String()
|
secret = uuid.New().String()
|
||||||
}
|
}
|
||||||
r, _ := route.NewRouter(db, utils.EmailConfig{
|
r, _ := route.NewRouter(db, []byte(secret))
|
||||||
From: viper.GetString("smtp.from"),
|
|
||||||
Host: viper.GetString("smtp.host"),
|
|
||||||
Port: viper.GetInt("smtp.port"),
|
|
||||||
Username: viper.GetString("smtp.username"),
|
|
||||||
Password: viper.GetString("smtp.password"),
|
|
||||||
InsecureSkipVerify: viper.GetBool("smtp.skip_verify"),
|
|
||||||
}, []byte(secret))
|
|
||||||
return http.ListenAndServe(viper.GetString("server.hostname"), r)
|
return http.ListenAndServe(viper.GetString("server.hostname"), r)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
viper.SetDefault("smtp.from", "no-reply@example.com")
|
|
||||||
viper.SetDefault("smtp.host", "localhost")
|
|
||||||
viper.SetDefault("smtp.port", 587)
|
|
||||||
viper.SetDefault("smtp.username", "")
|
|
||||||
viper.SetDefault("smtp.password", "")
|
|
||||||
viper.SetDefault("smtp.skip_verify", false)
|
|
||||||
|
|
||||||
cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server")
|
cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server")
|
||||||
|
|
||||||
viper.BindPFlag("migrate", cc.Flags().Lookup("migrate"))
|
viper.BindPFlag("migrate", cc.Flags().Lookup("migrate"))
|
||||||
|
@ -4,7 +4,6 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -104,22 +103,6 @@ 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"`
|
||||||
@ -145,16 +128,6 @@ 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,12 +23,10 @@ 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)
|
||||||
@ -49,7 +47,6 @@ 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
|
||||||
@ -58,7 +55,6 @@ 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)
|
||||||
@ -69,7 +65,6 @@ 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)
|
||||||
@ -79,7 +74,6 @@ 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)
|
||||||
@ -119,7 +113,6 @@ 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)
|
||||||
@ -127,7 +120,6 @@ 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)
|
||||||
@ -142,7 +134,6 @@ 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,16 +43,3 @@ 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 *;
|
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
-- 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,38 +85,6 @@ 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
|
||||||
`
|
`
|
||||||
@ -126,25 +94,6 @@ 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
|
||||||
`
|
`
|
||||||
@ -194,41 +143,6 @@ 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
|
||||||
@ -327,31 +241,6 @@ 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
|
||||||
`
|
`
|
||||||
|
@ -1,131 +0,0 @@
|
|||||||
// 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
@ -26,11 +26,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewHandler returns a new graphql endpoint handler.
|
// NewHandler returns a new graphql endpoint handler.
|
||||||
func NewHandler(repo db.Repository, emailConfig utils.EmailConfig) http.Handler {
|
func NewHandler(repo db.Repository) http.Handler {
|
||||||
c := Config{
|
c := Config{
|
||||||
Resolvers: &Resolver{
|
Resolvers: &Resolver{
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
EmailConfig: emailConfig,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
|
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
|
||||||
@ -45,7 +44,7 @@ func NewHandler(repo db.Repository, emailConfig utils.EmailConfig) http.Handler
|
|||||||
}
|
}
|
||||||
|
|
||||||
var subjectID uuid.UUID
|
var subjectID uuid.UUID
|
||||||
in := graphql.GetFieldContext(ctx).Args["input"]
|
in := graphql.GetResolverContext(ctx).Args["input"]
|
||||||
val := reflect.ValueOf(in) // could be any underlying type
|
val := reflect.ValueOf(in) // could be any underlying type
|
||||||
if val.Kind() == reflect.Ptr {
|
if val.Kind() == reflect.Ptr {
|
||||||
val = reflect.Indirect(val)
|
val = reflect.Indirect(val)
|
||||||
@ -256,28 +255,3 @@ 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,7 +41,3 @@ 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,12 +22,6 @@ 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"`
|
||||||
@ -45,16 +39,6 @@ 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"`
|
||||||
@ -65,12 +49,6 @@ 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"`
|
||||||
@ -130,15 +108,6 @@ 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"`
|
||||||
}
|
}
|
||||||
@ -405,11 +374,6 @@ 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"`
|
||||||
}
|
}
|
||||||
@ -502,16 +466,6 @@ 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"`
|
||||||
@ -661,63 +615,6 @@ 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 (
|
||||||
|
@ -7,12 +7,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
"github.com/jordanknott/taskcafe/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Resolver handles resolving GraphQL queries & mutations
|
// Resolver handles resolving GraphQL queries & mutations
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
Repository db.Repository
|
Repository db.Repository
|
||||||
EmailConfig utils.EmailConfig
|
mu sync.Mutex
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
@ -135,38 +135,6 @@ 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!
|
||||||
@ -181,23 +149,6 @@ 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 {
|
||||||
@ -631,44 +582,6 @@ 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)
|
||||||
@ -943,3 +856,4 @@ type DeleteUserAccountPayload {
|
|||||||
ok: Boolean!
|
ok: Boolean!
|
||||||
userAccount: UserAccount!
|
userAccount: UserAccount!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,8 +5,9 @@ package graph
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
@ -15,8 +16,10 @@ import (
|
|||||||
"github.com/jordanknott/taskcafe/internal/auth"
|
"github.com/jordanknott/taskcafe/internal/auth"
|
||||||
"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/jordanknott/taskcafe/internal/utils"
|
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
|
gomail "gopkg.in/mail.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"
|
||||||
@ -191,11 +194,79 @@ func (r *mutationResolver) InviteProjectMembers(ctx context.Context, input Invit
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return &InviteProjectMembersPayload{Ok: false}, err
|
return &InviteProjectMembersPayload{Ok: false}, err
|
||||||
}
|
}
|
||||||
invite := utils.EmailInvite{To: *invitedMember.Email, FullName: *invitedMember.Email, ConfirmToken: confirmToken.ConfirmTokenID.String()}
|
// send out invitation
|
||||||
err = utils.SendEmailInvite(r.EmailConfig, invite)
|
// add project invite entry
|
||||||
|
// send out notification?
|
||||||
|
h := hermes.Hermes{
|
||||||
|
// Optional Theme
|
||||||
|
Product: hermes.Product{
|
||||||
|
// Appears in header & footer of e-mails
|
||||||
|
Name: "Taskscafe",
|
||||||
|
Link: "http://localhost:3333/",
|
||||||
|
// Optional product logo
|
||||||
|
Logo: "https://github.com/JordanKnott/taskcafe/raw/master/.github/taskcafe-full.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
email := hermes.Email{
|
||||||
|
Body: hermes.Body{
|
||||||
|
Name: "Jordan Knott",
|
||||||
|
Intros: []string{
|
||||||
|
"You have been invited to join Taskcafe",
|
||||||
|
},
|
||||||
|
Actions: []hermes.Action{
|
||||||
|
{
|
||||||
|
Instructions: "To get started with Taskcafe, please click here:",
|
||||||
|
Button: hermes.Button{
|
||||||
|
Color: "#7367F0", // Optional action button color
|
||||||
|
TextColor: "#FFFFFF",
|
||||||
|
Text: "Register your account",
|
||||||
|
Link: "http://localhost:3000/register?confirmToken=" + confirmToken.ConfirmTokenID.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outros: []string{
|
||||||
|
"Need help, or have questions? Just reply to this email, we'd love to help.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate an HTML email with the provided contents (for modern clients)
|
||||||
|
emailBody, err := h.GenerateHTML(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.New(ctx).WithError(err).Error("issue sending email")
|
panic(err) // Tip: Handle error with something else than a panic ;)
|
||||||
return &InviteProjectMembersPayload{Ok: false}, err
|
}
|
||||||
|
emailBodyPlain, err := h.GeneratePlainText(email)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // Tip: Handle error with something else than a panic ;)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := gomail.NewMessage()
|
||||||
|
|
||||||
|
// Set E-Mail sender
|
||||||
|
m.SetHeader("From", "no-reply@taskcafe.com")
|
||||||
|
|
||||||
|
// Set E-Mail receivers
|
||||||
|
m.SetHeader("To", invitedUser.Email)
|
||||||
|
|
||||||
|
// Set E-Mail subject
|
||||||
|
m.SetHeader("Subject", "You have been invited to Taskcafe")
|
||||||
|
|
||||||
|
// Set E-Mail body. You can set plain text or html with text/html
|
||||||
|
m.SetBody("text/html", emailBody)
|
||||||
|
m.AddAlternative("text/plain", emailBodyPlain)
|
||||||
|
|
||||||
|
// Settings for SMTP server
|
||||||
|
d := gomail.NewDialer("127.0.0.1", 11500, "no-reply@taskcafe.com", "")
|
||||||
|
|
||||||
|
// This is only needed when SSL/TLS certificate is not valid on server.
|
||||||
|
// In production this should be set to false.
|
||||||
|
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||||
|
|
||||||
|
// Now send E-Mail
|
||||||
|
if err := d.DialAndSend(m); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return &InviteProjectMembersPayload{Ok: false}, err
|
return &InviteProjectMembersPayload{Ok: false}, err
|
||||||
@ -292,28 +363,6 @@ 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
|
||||||
@ -338,44 +387,12 @@ 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, _ := r.Repository.UpdateTaskLocation(ctx, db.UpdateTaskLocationParams{TaskID: input.TaskID, TaskGroupID: input.TaskGroupID, Position: input.Position})
|
task, err := r.Repository.UpdateTaskLocation(ctx, db.UpdateTaskLocationParams{input.TaskID, input.TaskGroupID, 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,21 +403,6 @@ 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
|
||||||
@ -409,23 +411,6 @@ 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()}
|
||||||
@ -436,15 +421,6 @@ 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
|
||||||
}
|
}
|
||||||
@ -586,33 +562,6 @@ 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,
|
||||||
@ -1609,80 +1558,6 @@ 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
|
||||||
}
|
}
|
||||||
@ -1699,31 +1574,6 @@ 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
|
||||||
}
|
}
|
||||||
@ -1769,7 +1619,6 @@ 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")
|
||||||
@ -1785,6 +1634,7 @@ 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},
|
||||||
})
|
})
|
||||||
@ -1874,9 +1724,6 @@ 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} }
|
||||||
|
|
||||||
@ -1885,9 +1732,6 @@ 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} }
|
||||||
|
|
||||||
@ -1909,11 +1753,27 @@ 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,38 +135,6 @@ 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!
|
||||||
@ -181,23 +149,6 @@ 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 {
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
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!
|
|
||||||
}
|
|
@ -16,7 +16,6 @@ import (
|
|||||||
"github.com/jordanknott/taskcafe/internal/frontend"
|
"github.com/jordanknott/taskcafe/internal/frontend"
|
||||||
"github.com/jordanknott/taskcafe/internal/graph"
|
"github.com/jordanknott/taskcafe/internal/graph"
|
||||||
"github.com/jordanknott/taskcafe/internal/logger"
|
"github.com/jordanknott/taskcafe/internal/logger"
|
||||||
"github.com/jordanknott/taskcafe/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FrontendHandler serves an embed React client through chi
|
// FrontendHandler serves an embed React client through chi
|
||||||
@ -65,7 +64,7 @@ type TaskcafeHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRouter creates a new router for chi
|
// NewRouter creates a new router for chi
|
||||||
func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, jwtKey []byte) (chi.Router, error) {
|
func NewRouter(dbConnection *sqlx.DB, jwtKey []byte) (chi.Router, error) {
|
||||||
formatter := new(log.TextFormatter)
|
formatter := new(log.TextFormatter)
|
||||||
formatter.TimestampFormat = "02-01-2006 15:04:05"
|
formatter.TimestampFormat = "02-01-2006 15:04:05"
|
||||||
formatter.FullTimestamp = true
|
formatter.FullTimestamp = true
|
||||||
@ -95,7 +94,7 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, jwtKey []by
|
|||||||
r.Group(func(mux chi.Router) {
|
r.Group(func(mux chi.Router) {
|
||||||
mux.Use(auth.Middleware)
|
mux.Use(auth.Middleware)
|
||||||
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
||||||
mux.Handle("/graphql", graph.NewHandler(*repository, emailConfig))
|
mux.Handle("/graphql", graph.NewHandler(*repository))
|
||||||
})
|
})
|
||||||
|
|
||||||
frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}
|
frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}
|
||||||
|
@ -1,94 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
hermes "github.com/matcornic/hermes/v2"
|
|
||||||
gomail "gopkg.in/mail.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EmailConfig struct {
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
From string
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
SiteURL string
|
|
||||||
InsecureSkipVerify bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type EmailInvite struct {
|
|
||||||
ConfirmToken string
|
|
||||||
FullName string
|
|
||||||
To string
|
|
||||||
}
|
|
||||||
|
|
||||||
func SendEmailInvite(config EmailConfig, invite EmailInvite) error {
|
|
||||||
h := hermes.Hermes{
|
|
||||||
Product: hermes.Product{
|
|
||||||
Name: "Taskscafe",
|
|
||||||
Link: config.SiteURL,
|
|
||||||
Logo: "https://github.com/JordanKnott/taskcafe/raw/master/.github/taskcafe-full.png",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
email := hermes.Email{
|
|
||||||
Body: hermes.Body{
|
|
||||||
Name: invite.FullName,
|
|
||||||
Intros: []string{
|
|
||||||
"You have been invited to join Taskcafe",
|
|
||||||
},
|
|
||||||
Actions: []hermes.Action{
|
|
||||||
{
|
|
||||||
Instructions: "To get started with Taskcafe, please click here:",
|
|
||||||
Button: hermes.Button{
|
|
||||||
Color: "#7367F0", // Optional action button color
|
|
||||||
TextColor: "#FFFFFF",
|
|
||||||
Text: "Register your account",
|
|
||||||
Link: config.SiteURL + "/register?confirmToken=" + invite.ConfirmToken,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Outros: []string{
|
|
||||||
"Need help, or have questions? Just reply to this email, we'd love to help.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
emailBody, err := h.GenerateHTML(email)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
emailBodyPlain, err := h.GeneratePlainText(email)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
m := gomail.NewMessage()
|
|
||||||
|
|
||||||
// Set E-Mail sender
|
|
||||||
m.SetHeader("From", config.From)
|
|
||||||
|
|
||||||
// Set E-Mail receivers
|
|
||||||
m.SetHeader("To", invite.To)
|
|
||||||
|
|
||||||
// Set E-Mail subject
|
|
||||||
m.SetHeader("Subject", "You have been invited to Taskcafe")
|
|
||||||
|
|
||||||
// Set E-Mail body. You can set plain text or html with text/html
|
|
||||||
m.SetBody("text/html", emailBody)
|
|
||||||
m.AddAlternative("text/plain", emailBodyPlain)
|
|
||||||
|
|
||||||
// Settings for SMTP server
|
|
||||||
d := gomail.NewDialer(config.Host, config.Port, config.Username, config.Password)
|
|
||||||
|
|
||||||
// This is only needed when SSL/TLS certificate is not valid on server.
|
|
||||||
// In production this should be set to false.
|
|
||||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}
|
|
||||||
|
|
||||||
// Now send E-Mail
|
|
||||||
if err := d.DialAndSend(m); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
@ -1,9 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
Reference in New Issue
Block a user