Compare commits
	
		
			1 Commits
		
	
	
		
			0.3.0
			...
			refactor/c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2de48e288b | 
@@ -21,4 +21,4 @@ windows:
 | 
			
		||||
  - database:
 | 
			
		||||
      root: ./
 | 
			
		||||
      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'
 | 
			
		||||
 | 
			
		||||
[smtp]
 | 
			
		||||
username = 'taskcafe@example.com'
 | 
			
		||||
password = ''
 | 
			
		||||
from = 'no-reply@taskcafe.com'
 | 
			
		||||
host = 'localhost'
 | 
			
		||||
port = 11500
 | 
			
		||||
skip_verify = false
 | 
			
		||||
username = 'admin@example.com'
 | 
			
		||||
password = 'example'
 | 
			
		||||
server = 'mail.example.com'
 | 
			
		||||
port = 465
 | 
			
		||||
connection_security = 'STARTTLS'
 | 
			
		||||
 
 | 
			
		||||
@@ -9,8 +9,6 @@
 | 
			
		||||
    "@types/axios": "^0.14.0",
 | 
			
		||||
    "@types/color": "^3.0.1",
 | 
			
		||||
    "@types/date-fns": "^2.6.0",
 | 
			
		||||
    "@types/dompurify": "^2.0.4",
 | 
			
		||||
    "@types/emoji-mart": "^3.0.4",
 | 
			
		||||
    "@types/jest": "^24.0.0",
 | 
			
		||||
    "@types/jwt-decode": "^2.2.1",
 | 
			
		||||
    "@types/lodash": "^4.14.149",
 | 
			
		||||
@@ -37,16 +35,12 @@
 | 
			
		||||
    "color": "^3.1.2",
 | 
			
		||||
    "date-fns": "^2.14.0",
 | 
			
		||||
    "dayjs": "^1.9.1",
 | 
			
		||||
    "dompurify": "^2.2.6",
 | 
			
		||||
    "emoji-mart": "^3.0.0",
 | 
			
		||||
    "emoticon": "^3.2.0",
 | 
			
		||||
    "graphql": "^15.0.0",
 | 
			
		||||
    "graphql-tag": "^2.10.3",
 | 
			
		||||
    "history": "^4.10.1",
 | 
			
		||||
    "immer": "^6.0.3",
 | 
			
		||||
    "jwt-decode": "^2.2.0",
 | 
			
		||||
    "lodash": "^4.17.20",
 | 
			
		||||
    "node-emoji": "^1.10.0",
 | 
			
		||||
    "prop-types": "^15.7.2",
 | 
			
		||||
    "query-string": "^6.13.7",
 | 
			
		||||
    "react": "^16.12.0",
 | 
			
		||||
@@ -54,7 +48,6 @@
 | 
			
		||||
    "react-beautiful-dnd": "^13.0.0",
 | 
			
		||||
    "react-datepicker": "^2.14.1",
 | 
			
		||||
    "react-dom": "^16.12.0",
 | 
			
		||||
    "react-emoji-render": "^1.2.4",
 | 
			
		||||
    "react-hook-form": "^6.0.6",
 | 
			
		||||
    "react-markdown": "^4.3.1",
 | 
			
		||||
    "react-router": "^5.1.2",
 | 
			
		||||
 
 | 
			
		||||
@@ -174,7 +174,7 @@ const AdminRoute = () => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.title = 'Admin | Taskcafé';
 | 
			
		||||
  }, []);
 | 
			
		||||
  const { loading, data } = useUsersQuery({ fetchPolicy: 'cache-and-network' });
 | 
			
		||||
  const { loading, data } = useUsersQuery();
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
  const { user } = useCurrentUser();
 | 
			
		||||
  const [deleteInvitedUser] = useDeleteInvitedUserAccountMutation({
 | 
			
		||||
@@ -182,7 +182,7 @@ const AdminRoute = () => {
 | 
			
		||||
      updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
 | 
			
		||||
        produce(cache, draftCache => {
 | 
			
		||||
          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) => {
 | 
			
		||||
      updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
 | 
			
		||||
        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,
 | 
			
		||||
      });
 | 
			
		||||
      const newData = produce(cacheData, (draftState: any) => {
 | 
			
		||||
        draftState.users = [...draftState.users, { ...createData.data?.createUserAccount }];
 | 
			
		||||
        draftState.users = [...draftState.users, { ...createData.data.createUserAccount }];
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      client.writeQuery({
 | 
			
		||||
@@ -214,6 +214,9 @@ const AdminRoute = () => {
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />;
 | 
			
		||||
  }
 | 
			
		||||
  if (data && user) {
 | 
			
		||||
    if (user.roles.org !== 'admin') {
 | 
			
		||||
      return <Redirect to="/" />;
 | 
			
		||||
@@ -256,7 +259,7 @@ const AdminRoute = () => {
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return <GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />;
 | 
			
		||||
  return <span>error</span>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AdminRoute;
 | 
			
		||||
 
 | 
			
		||||
@@ -25,11 +25,6 @@ const MainContent = styled.div`
 | 
			
		||||
  flex-grow: 1;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
type RefreshTokenResponse = {
 | 
			
		||||
  accessToken: string;
 | 
			
		||||
  setup?: null | { confirmToken: string };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const AuthorizedRoutes = () => {
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
 
 | 
			
		||||
@@ -128,10 +128,12 @@ const TeamProjectContainer = styled.div`
 | 
			
		||||
const colors = [theme.colors.primary, theme.colors.secondary];
 | 
			
		||||
 | 
			
		||||
const ProjectFinder = () => {
 | 
			
		||||
  const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'cache-and-network' });
 | 
			
		||||
  const { loading, data } = useGetProjectsQuery();
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <span>loading</span>;
 | 
			
		||||
  }
 | 
			
		||||
  if (data) {
 | 
			
		||||
    const { projects, teams } = data;
 | 
			
		||||
    const personalProjects = data.projects.filter(p => p.team === null);
 | 
			
		||||
    const projectTeams = teams.map(team => {
 | 
			
		||||
      return {
 | 
			
		||||
        id: team.id,
 | 
			
		||||
@@ -141,22 +143,6 @@ const ProjectFinder = () => {
 | 
			
		||||
    });
 | 
			
		||||
    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 => (
 | 
			
		||||
          <TeamContainer key={team.id}>
 | 
			
		||||
            <TeamTitle>{team.name}</TeamTitle>
 | 
			
		||||
@@ -178,10 +164,10 @@ const ProjectFinder = () => {
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return <span>loading</span>;
 | 
			
		||||
  return <span>error</span>;
 | 
			
		||||
};
 | 
			
		||||
type ProjectPopupProps = {
 | 
			
		||||
  history: any;
 | 
			
		||||
  history: History<History.PoorMansUnknown>;
 | 
			
		||||
  name: string;
 | 
			
		||||
  projectID: string;
 | 
			
		||||
};
 | 
			
		||||
@@ -196,7 +182,7 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
 | 
			
		||||
 | 
			
		||||
      const newData = produce(cacheData, (draftState: any) => {
 | 
			
		||||
        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();
 | 
			
		||||
type RefreshTokenResponse = {
 | 
			
		||||
  accessToken: string;
 | 
			
		||||
  isInstalled: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const App = () => {
 | 
			
		||||
  const [user, setUser] = useState<CurrentUserRaw | null>(null);
 | 
			
		||||
 
 | 
			
		||||
@@ -280,7 +280,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            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 },
 | 
			
		||||
@@ -296,12 +296,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            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 (newTaskData.data) {
 | 
			
		||||
              draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -315,9 +313,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (newTaskGroupData.data) {
 | 
			
		||||
            draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -336,7 +332,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            const idx = cache.findProject.taskGroups.findIndex(
 | 
			
		||||
              t => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
 | 
			
		||||
              t => t.id === resp.data.deleteTaskGroupTasks.taskGroupID,
 | 
			
		||||
            );
 | 
			
		||||
            if (idx !== -1) {
 | 
			
		||||
              draftCache.findProject.taskGroups[idx].tasks = [];
 | 
			
		||||
@@ -352,9 +348,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (resp.data) {
 | 
			
		||||
            draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -370,26 +364,21 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (newTask.data) {
 | 
			
		||||
            const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
 | 
			
		||||
            if (previousTaskGroupID !== task.taskGroup.id) {
 | 
			
		||||
              const { taskGroups } = cache.findProject;
 | 
			
		||||
              const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
 | 
			
		||||
              const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
 | 
			
		||||
              if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
 | 
			
		||||
                  const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(t => t.id === task.id);
 | 
			
		||||
                draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
 | 
			
		||||
                  (t: Task) => t.id !== task.id,
 | 
			
		||||
                );
 | 
			
		||||
                  if (previousTask) {
 | 
			
		||||
                draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
 | 
			
		||||
                  ...taskGroups[newTaskGroupIdx].tasks,
 | 
			
		||||
                      { ...previousTask },
 | 
			
		||||
                  { ...task },
 | 
			
		||||
                ];
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -459,6 +448,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <BoardLoading />;
 | 
			
		||||
  }
 | 
			
		||||
  const getTaskStatusFilterLabel = (filter: TaskStatusFilter) => {
 | 
			
		||||
    if (filter.status === TaskStatus.COMPLETE) {
 | 
			
		||||
      return 'Complete';
 | 
			
		||||
@@ -815,7 +807,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <BoardLoading />;
 | 
			
		||||
  return <span>Error</span>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ProjectBoard;
 | 
			
		||||
 
 | 
			
		||||
@@ -21,9 +21,6 @@ import {
 | 
			
		||||
  useCreateTaskChecklistItemMutation,
 | 
			
		||||
  FindTaskDocument,
 | 
			
		||||
  FindTaskQuery,
 | 
			
		||||
  useCreateTaskCommentMutation,
 | 
			
		||||
  useDeleteTaskCommentMutation,
 | 
			
		||||
  useUpdateTaskCommentMutation,
 | 
			
		||||
} from 'shared/generated/graphql';
 | 
			
		||||
import { useCurrentUser } from 'App/context';
 | 
			
		||||
import MiniProfile from 'shared/components/MiniProfile';
 | 
			
		||||
@@ -36,73 +33,6 @@ import { useForm } from 'react-hook-form';
 | 
			
		||||
import updateApolloCache from 'shared/utils/cache';
 | 
			
		||||
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 total = checklists.reduce((prev: any, next: any) => {
 | 
			
		||||
    return (
 | 
			
		||||
@@ -200,40 +130,6 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
  const { user } = useCurrentUser();
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
  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 [updateTaskChecklistItemLocation] = useUpdateTaskChecklistItemLocationMutation({
 | 
			
		||||
    update: (client, response) => {
 | 
			
		||||
@@ -242,11 +138,10 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
        FindTaskDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (response.data) {
 | 
			
		||||
              const { prevChecklistID, taskChecklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
 | 
			
		||||
              if (taskChecklistID !== prevChecklistID) {
 | 
			
		||||
            const { prevChecklistID, checklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
 | 
			
		||||
            if (checklistID !== prevChecklistID) {
 | 
			
		||||
              const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID);
 | 
			
		||||
                const newIdx = cache.findTask.checklists.findIndex(c => c.id === taskChecklistID);
 | 
			
		||||
              const newIdx = cache.findTask.checklists.findIndex(c => c.id === checklistID);
 | 
			
		||||
              if (oldIdx > -1 && newIdx > -1) {
 | 
			
		||||
                const item = cache.findTask.checklists[oldIdx].items.find(i => i.id === checklistItem.id);
 | 
			
		||||
                if (item) {
 | 
			
		||||
@@ -256,12 +151,11 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
                  draftCache.findTask.checklists[newIdx].items.push({
 | 
			
		||||
                    ...item,
 | 
			
		||||
                    position: checklistItem.position,
 | 
			
		||||
                      taskChecklistID,
 | 
			
		||||
                    taskChecklistID: checklistID,
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { taskID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -294,7 +188,7 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            const { checklists } = cache.findTask;
 | 
			
		||||
            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);
 | 
			
		||||
            draftCache.findTask.badges.checklist = {
 | 
			
		||||
@@ -318,10 +212,8 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
        FindTaskDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (createData.data) {
 | 
			
		||||
            const item = createData.data.createTaskChecklist;
 | 
			
		||||
            draftCache.findTask.checklists.push({ ...item });
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { taskID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -335,7 +227,6 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
        FindTaskDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (deleteData.data) {
 | 
			
		||||
            const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
 | 
			
		||||
            const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID);
 | 
			
		||||
            if (targetIdx > -1) {
 | 
			
		||||
@@ -349,7 +240,6 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
              complete,
 | 
			
		||||
              total,
 | 
			
		||||
            };
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { taskID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -362,7 +252,6 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
        FindTaskDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (newTaskItem.data) {
 | 
			
		||||
            const item = newTaskItem.data.createTaskChecklistItem;
 | 
			
		||||
            const { checklists } = cache.findTask;
 | 
			
		||||
            const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
 | 
			
		||||
@@ -375,17 +264,12 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
                total,
 | 
			
		||||
              };
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { taskID },
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const { loading, data, refetch } = useFindTaskQuery({
 | 
			
		||||
    variables: { taskID },
 | 
			
		||||
    pollInterval: 3000,
 | 
			
		||||
    fetchPolicy: 'cache-and-network',
 | 
			
		||||
  });
 | 
			
		||||
  const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
 | 
			
		||||
  const [setTaskComplete] = useSetTaskCompleteMutation();
 | 
			
		||||
  const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
 | 
			
		||||
    onCompleted: () => {
 | 
			
		||||
@@ -405,8 +289,9 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
      refreshCache();
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const [updateTaskComment] = useUpdateTaskCommentMutation();
 | 
			
		||||
  const [editableComment, setEditableComment] = useState<null | string>(null);
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  if (!data) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
@@ -420,31 +305,8 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
        renderContent={() => {
 | 
			
		||||
          return (
 | 
			
		||||
            <TaskDetails
 | 
			
		||||
              onCancelCommentEdit={() => setEditableComment(null)}
 | 
			
		||||
              onUpdateComment={(commentID, message) => {
 | 
			
		||||
                updateTaskComment({ variables: { commentID, message } });
 | 
			
		||||
              }}
 | 
			
		||||
              editableComment={editableComment}
 | 
			
		||||
              me={data.me.user}
 | 
			
		||||
              onCommentShowActions={(commentID, $targetRef) => {
 | 
			
		||||
                showPopup(
 | 
			
		||||
                  $targetRef,
 | 
			
		||||
                  <TaskCommentActions
 | 
			
		||||
                    onDeleteComment={() => {
 | 
			
		||||
                      deleteTaskComment({ variables: { commentID } });
 | 
			
		||||
                      hidePopup();
 | 
			
		||||
                    }}
 | 
			
		||||
                    onEditComment={() => {
 | 
			
		||||
                      setEditableComment(commentID);
 | 
			
		||||
                      hidePopup();
 | 
			
		||||
                    }}
 | 
			
		||||
                  />,
 | 
			
		||||
                );
 | 
			
		||||
              }}
 | 
			
		||||
              task={data.findTask}
 | 
			
		||||
              onCreateComment={(task, message) => {
 | 
			
		||||
                createTaskComment({ variables: { taskID: task.id, message } });
 | 
			
		||||
              }}
 | 
			
		||||
              onChecklistDrop={checklist => {
 | 
			
		||||
                updateTaskChecklistLocation({
 | 
			
		||||
                  variables: { taskChecklistID: checklist.id, position: checklist.position },
 | 
			
		||||
 
 | 
			
		||||
@@ -36,9 +36,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (newLabelData.data) {
 | 
			
		||||
            draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        {
 | 
			
		||||
          projectID,
 | 
			
		||||
@@ -55,7 +53,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            draftCache.findProject.labels = cache.findProject.labels.filter(
 | 
			
		||||
              label => label.id !== newLabelData.data?.deleteProjectLabel.id,
 | 
			
		||||
              label => label.id !== newLabelData.data.deleteProjectLabel.id,
 | 
			
		||||
            );
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,7 @@ import {
 | 
			
		||||
  FindProjectDocument,
 | 
			
		||||
  FindProjectQuery,
 | 
			
		||||
} from 'shared/generated/graphql';
 | 
			
		||||
 | 
			
		||||
import produce from 'immer';
 | 
			
		||||
import UserContext, { useCurrentUser } from 'App/context';
 | 
			
		||||
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) => {
 | 
			
		||||
  console.log(input.trim().length < 3);
 | 
			
		||||
  if (input && input.trim().length < 3) {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
@@ -161,10 +163,12 @@ const fetchMembers = async (client: any, projectID: string, options: MemberFilte
 | 
			
		||||
 | 
			
		||||
  let results: any = [];
 | 
			
		||||
  const emails: Array<string> = [];
 | 
			
		||||
  console.log(res.data && res.data.searchMembers);
 | 
			
		||||
  if (res.data && res.data.searchMembers) {
 | 
			
		||||
    results = [
 | 
			
		||||
      ...res.data.searchMembers.map((m: any) => {
 | 
			
		||||
        if (m.status === 'INVITED') {
 | 
			
		||||
          console.log(`${m.id} is added`);
 | 
			
		||||
          return {
 | 
			
		||||
            label: m.id,
 | 
			
		||||
            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 },
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
    ];
 | 
			
		||||
    console.log(results);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 }) => {
 | 
			
		||||
  console.log(data);
 | 
			
		||||
  return !isDisabled ? (
 | 
			
		||||
    <OptionWrapper {...innerProps} isFocused={isFocused}>
 | 
			
		||||
      <TaskAssignee
 | 
			
		||||
@@ -416,16 +423,14 @@ const Project = () => {
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (resp.data) {
 | 
			
		||||
            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) {
 | 
			
		||||
              draftCache.findProject.taskGroups[taskGroupIdx].tasks = cache.findProject.taskGroups[
 | 
			
		||||
                taskGroupIdx
 | 
			
		||||
                ].tasks.filter(t => t.id !== resp.data?.deleteTask.taskID);
 | 
			
		||||
              }
 | 
			
		||||
              ].tasks.filter(t => t.id !== resp.data.deleteTask.taskID);
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
@@ -436,7 +441,6 @@ const Project = () => {
 | 
			
		||||
 | 
			
		||||
  const { loading, data, error } = useFindProjectQuery({
 | 
			
		||||
    variables: { projectID },
 | 
			
		||||
    pollInterval: 3000,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const [updateProjectName] = useUpdateProjectNameMutation({
 | 
			
		||||
@@ -446,7 +450,7 @@ const Project = () => {
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            draftCache.findProject.name = newName.data?.updateProjectName.name ?? '';
 | 
			
		||||
            draftCache.findProject.name = newName.data.updateProjectName.name;
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -460,7 +464,6 @@ const Project = () => {
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (response.data) {
 | 
			
		||||
            draftCache.findProject.members = [
 | 
			
		||||
              ...cache.findProject.members,
 | 
			
		||||
              ...response.data.inviteProjectMembers.members,
 | 
			
		||||
@@ -469,7 +472,6 @@ const Project = () => {
 | 
			
		||||
              ...cache.findProject.invitedMembers,
 | 
			
		||||
              ...response.data.inviteProjectMembers.invitedMembers,
 | 
			
		||||
            ];
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -483,7 +485,7 @@ const Project = () => {
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            draftCache.findProject.invitedMembers = cache.findProject.invitedMembers.filter(
 | 
			
		||||
              m => m.email !== response.data?.deleteInvitedProjectMember.invitedMember.email ?? '',
 | 
			
		||||
              m => m.email !== response.data.deleteInvitedProjectMember.invitedMember.email,
 | 
			
		||||
            );
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
@@ -498,7 +500,7 @@ const Project = () => {
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            draftCache.findProject.members = cache.findProject.members.filter(
 | 
			
		||||
              m => m.id !== response.data?.deleteProjectMember.member.id,
 | 
			
		||||
              m => m.id !== response.data.deleteProjectMember.member.id,
 | 
			
		||||
            );
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
@@ -517,6 +519,14 @@ const Project = () => {
 | 
			
		||||
      document.title = `${data.findProject.name} | Taskcafé`;
 | 
			
		||||
    }
 | 
			
		||||
  }, [data]);
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <GlobalTopNavbar onSaveProjectName={NOOP} name="" projectID={null} />
 | 
			
		||||
        <BoardLoading />
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  if (error) {
 | 
			
		||||
    history.push('/projects');
 | 
			
		||||
  }
 | 
			
		||||
@@ -629,12 +639,7 @@ const Project = () => {
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <GlobalTopNavbar onSaveProjectName={NOOP} name="" projectID={null} />
 | 
			
		||||
      <BoardLoading />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <div>Error</div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Project;
 | 
			
		||||
 
 | 
			
		||||
@@ -202,7 +202,7 @@ type ShowNewProject = {
 | 
			
		||||
 | 
			
		||||
const Projects = () => {
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
  const { loading, data } = useGetProjectsQuery({ pollInterval: 3000, fetchPolicy: 'cache-and-network' });
 | 
			
		||||
  const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only' });
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.title = 'Taskcafé';
 | 
			
		||||
  }, []);
 | 
			
		||||
@@ -210,9 +210,7 @@ const Projects = () => {
 | 
			
		||||
    update: (client, newProject) => {
 | 
			
		||||
      updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
 | 
			
		||||
        produce(cache, draftCache => {
 | 
			
		||||
          if (newProject.data) {
 | 
			
		||||
          draftCache.projects.push({ ...newProject.data.createProject });
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
@@ -224,13 +222,14 @@ const Projects = () => {
 | 
			
		||||
    update: (client, createData) => {
 | 
			
		||||
      updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
 | 
			
		||||
        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;
 | 
			
		||||
  if (data && user) {
 | 
			
		||||
@@ -392,7 +391,7 @@ const Projects = () => {
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
 | 
			
		||||
  return <div>Error!</div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Projects;
 | 
			
		||||
 
 | 
			
		||||
@@ -419,11 +419,7 @@ type MembersProps = {
 | 
			
		||||
 | 
			
		||||
const Members: React.FC<MembersProps> = ({ teamID }) => {
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({
 | 
			
		||||
    variables: { teamID },
 | 
			
		||||
    fetchPolicy: 'cache-and-network',
 | 
			
		||||
    pollInterval: 3000,
 | 
			
		||||
  });
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({ variables: { teamID } });
 | 
			
		||||
  const { user, setUserRoles } = useCurrentUser();
 | 
			
		||||
  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”.';
 | 
			
		||||
@@ -434,13 +430,11 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
 | 
			
		||||
        GetTeamDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (response.data) {
 | 
			
		||||
            draftCache.findTeam.members.push({
 | 
			
		||||
              ...response.data.createTeamMember.teamMember,
 | 
			
		||||
              member: { __typename: 'MemberList', projects: [], teams: [] },
 | 
			
		||||
              owned: { __typename: 'OwnedList', projects: [], teams: [] },
 | 
			
		||||
            });
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { teamID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -465,13 +459,16 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            draftCache.findTeam.members = cache.findTeam.members.filter(
 | 
			
		||||
              member => member.id !== response.data?.deleteTeamMember.userID,
 | 
			
		||||
              member => member.id !== response.data.deleteTeamMember.userID,
 | 
			
		||||
            );
 | 
			
		||||
          }),
 | 
			
		||||
        { teamID },
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <span>loading</span>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (data && user) {
 | 
			
		||||
    return (
 | 
			
		||||
@@ -559,7 +556,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <div>loading</div>;
 | 
			
		||||
  return <div>error</div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Members;
 | 
			
		||||
 
 | 
			
		||||
@@ -155,11 +155,10 @@ type TeamProjectsProps = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({
 | 
			
		||||
    variables: { teamID },
 | 
			
		||||
    fetchPolicy: 'cache-and-network',
 | 
			
		||||
    pollInterval: 3000,
 | 
			
		||||
  });
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({ variables: { teamID } });
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <span>loading</span>;
 | 
			
		||||
  }
 | 
			
		||||
  if (data) {
 | 
			
		||||
    return (
 | 
			
		||||
      <ProjectsContainer>
 | 
			
		||||
@@ -190,7 +189,7 @@ const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
 | 
			
		||||
      </ProjectsContainer>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return <span>loading</span>;
 | 
			
		||||
  return <span>error</span>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default TeamProjects;
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ const Wrapper = styled.div`
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
type TeamPopupProps = {
 | 
			
		||||
  history: History<any>;
 | 
			
		||||
  history: History<History.PoorMansUnknown>;
 | 
			
		||||
  name: string;
 | 
			
		||||
  teamID: string;
 | 
			
		||||
};
 | 
			
		||||
@@ -44,9 +44,9 @@ export const TeamPopup: React.FC<TeamPopupProps> = ({ history, name, teamID }) =
 | 
			
		||||
    update: (client, deleteData) => {
 | 
			
		||||
      updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
 | 
			
		||||
        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(
 | 
			
		||||
            (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 [currentTab, setCurrentTab] = useState(0);
 | 
			
		||||
  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 (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
 | 
			
		||||
      return <Redirect to="/" />;
 | 
			
		||||
@@ -129,21 +146,7 @@ const Teams = () => {
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  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}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
  return <div>Error!</div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Teams;
 | 
			
		||||
 
 | 
			
		||||
@@ -20,14 +20,14 @@ export const MemberManagerSearch = styled(TextareaAutosize)`
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
 | 
			
		||||
  background: ${props => props.theme.colors.bg.secondary};
 | 
			
		||||
  background: ${props => props.theme.colors.bgColor.secondary};
 | 
			
		||||
  outline: none;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  border-color: ${props => props.theme.colors.border};
 | 
			
		||||
 | 
			
		||||
  &:focus {
 | 
			
		||||
    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) => {
 | 
			
		||||
                    setTeam(e.value);
 | 
			
		||||
                  }}
 | 
			
		||||
                  value={options.find(d => d.value === team)}
 | 
			
		||||
                  value={options.filter(d => d.value === team)}
 | 
			
		||||
                  styles={colourStyles}
 | 
			
		||||
                  classNamePrefix="teamSelect"
 | 
			
		||||
                  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;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  background: #1f243e;
 | 
			
		||||
`;
 | 
			
		||||
export const CommentProfile = styled(TaskAssignee)`
 | 
			
		||||
  margin-right: 8px;
 | 
			
		||||
@@ -485,7 +484,7 @@ export const CommentProfile = styled(TaskAssignee)`
 | 
			
		||||
  align-items: normal;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const CommentTextArea = styled(TextareaAutosize)<{ showCommentActions: boolean }>`
 | 
			
		||||
export const CommentTextArea = styled(TextareaAutosize)`
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  line-height: 28px;
 | 
			
		||||
  padding: 4px 6px;
 | 
			
		||||
@@ -496,16 +495,14 @@ export const CommentTextArea = styled(TextareaAutosize)<{ showCommentActions: bo
 | 
			
		||||
  transition: max-height 200ms, height 200ms, min-height 200ms;
 | 
			
		||||
  min-height: 36px;
 | 
			
		||||
  max-height: 36px;
 | 
			
		||||
  ${props =>
 | 
			
		||||
    props.showCommentActions
 | 
			
		||||
      ? css`
 | 
			
		||||
  &:not(:focus) {
 | 
			
		||||
    height: 36px;
 | 
			
		||||
  }
 | 
			
		||||
  &:focus {
 | 
			
		||||
    min-height: 80px;
 | 
			
		||||
    max-height: none;
 | 
			
		||||
    line-height: 20px;
 | 
			
		||||
        `
 | 
			
		||||
      : css`
 | 
			
		||||
          height: 36px;
 | 
			
		||||
        `}
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const CommentEditorActions = styled.div<{ visible: boolean }>`
 | 
			
		||||
@@ -532,18 +529,6 @@ export const ActivitySection = styled.div`
 | 
			
		||||
  overflow-x: hidden;
 | 
			
		||||
 | 
			
		||||
  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`
 | 
			
		||||
@@ -552,32 +537,25 @@ export const ActivityItem = styled.div`
 | 
			
		||||
  overflow-wrap: break-word;
 | 
			
		||||
  word-wrap: 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;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  padding-left: 8px;
 | 
			
		||||
  ${props => props.editable && 'width: 100%;'}
 | 
			
		||||
`;
 | 
			
		||||
export const ActivityItemHeaderUser = styled(TaskAssignee)`
 | 
			
		||||
  align-items: start;
 | 
			
		||||
  margin-right: 4px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemHeaderTitle = styled.div`
 | 
			
		||||
  margin-left: 4px;
 | 
			
		||||
  line-height: 18px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  padding-bottom: 2px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemHeaderTitleName = styled.span`
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  padding-right: 3px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemTimestamp = styled.span<{ margin: number }>`
 | 
			
		||||
@@ -590,10 +568,8 @@ export const ActivityItemDetails = styled.div`
 | 
			
		||||
  margin-left: 32px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemCommentContainer = styled.div``;
 | 
			
		||||
export const ActivityItemComment = styled.div<{ editable: boolean }>`
 | 
			
		||||
export const ActivityItemComment = styled.div`
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  ${mixin.boxShadowCard}
 | 
			
		||||
  position: relative;
 | 
			
		||||
@@ -601,32 +577,6 @@ export const ActivityItemComment = styled.div<{ editable: boolean }>`
 | 
			
		||||
  padding: 8px 12px;
 | 
			
		||||
  margin: 4px 0;
 | 
			
		||||
  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`
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
  Smile,
 | 
			
		||||
} 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 dark from 'shared/utils/editorTheme';
 | 
			
		||||
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 dayjs from 'dayjs';
 | 
			
		||||
import ActivityMessage from './ActivityMessage';
 | 
			
		||||
 | 
			
		||||
import Task from 'shared/icons/Task';
 | 
			
		||||
import {
 | 
			
		||||
  ActivityItemHeader,
 | 
			
		||||
  ActivityItemTimestamp,
 | 
			
		||||
  ActivityItem,
 | 
			
		||||
  ActivityItemCommentAction,
 | 
			
		||||
  ActivityItemCommentActions,
 | 
			
		||||
  TaskDetailLabel,
 | 
			
		||||
  CommentContainer,
 | 
			
		||||
  ActivityItemCommentContainer,
 | 
			
		||||
  MetaDetailContent,
 | 
			
		||||
  TaskDetailsAddLabelIcon,
 | 
			
		||||
  ActionButton,
 | 
			
		||||
@@ -73,126 +58,18 @@ import {
 | 
			
		||||
  TaskMember,
 | 
			
		||||
  TabBarSection,
 | 
			
		||||
  TabBarItem,
 | 
			
		||||
  CommentTextArea,
 | 
			
		||||
  CommentEditorContainer,
 | 
			
		||||
  CommentEditorActions,
 | 
			
		||||
  CommentEditorActionIcon,
 | 
			
		||||
  CommentEditorSaveButton,
 | 
			
		||||
  CommentProfile,
 | 
			
		||||
  CommentInnerWrapper,
 | 
			
		||||
  ActivitySection,
 | 
			
		||||
  TaskDetailsEditor,
 | 
			
		||||
  ActivityItemHeaderUser,
 | 
			
		||||
  ActivityItemHeaderTitle,
 | 
			
		||||
  ActivityItemHeaderTitleName,
 | 
			
		||||
  ActivityItemComment,
 | 
			
		||||
} from './Styles';
 | 
			
		||||
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
 | 
			
		||||
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``;
 | 
			
		||||
 | 
			
		||||
@@ -237,13 +114,8 @@ type TaskDetailsProps = {
 | 
			
		||||
  onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
 | 
			
		||||
  onOpenDueDatePopop: (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;
 | 
			
		||||
  onCancelCommentEdit: () => void;
 | 
			
		||||
  onUpdateComment: (commentID: string, message: string) => void;
 | 
			
		||||
  onChangeChecklistName: (checklistID: string, name: string) => void;
 | 
			
		||||
  editableComment?: string | null;
 | 
			
		||||
  onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
 | 
			
		||||
  onCloseModal: () => void;
 | 
			
		||||
  onChecklistDrop: (checklist: TaskChecklist) => void;
 | 
			
		||||
@@ -252,15 +124,11 @@ type TaskDetailsProps = {
 | 
			
		||||
 | 
			
		||||
const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
  me,
 | 
			
		||||
  onCancelCommentEdit,
 | 
			
		||||
  task,
 | 
			
		||||
  editableComment = null,
 | 
			
		||||
  onDeleteChecklist,
 | 
			
		||||
  onTaskNameChange,
 | 
			
		||||
  onCommentShowActions,
 | 
			
		||||
  onOpenAddChecklistPopup,
 | 
			
		||||
  onChangeChecklistName,
 | 
			
		||||
  onCreateComment,
 | 
			
		||||
  onChecklistDrop,
 | 
			
		||||
  onChecklistItemDrop,
 | 
			
		||||
  onToggleTaskComplete,
 | 
			
		||||
@@ -269,7 +137,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
  onDeleteItem,
 | 
			
		||||
  onDeleteTask,
 | 
			
		||||
  onCloseModal,
 | 
			
		||||
  onUpdateComment,
 | 
			
		||||
  onOpenAddMemberPopup,
 | 
			
		||||
  onOpenAddLabelPopup,
 | 
			
		||||
  onOpenDueDatePopop,
 | 
			
		||||
@@ -289,38 +156,12 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
  });
 | 
			
		||||
  const [saveTimeout, setSaveTimeout] = useState<any>(null);
 | 
			
		||||
  const [showRaw, setShowRaw] = useState(false);
 | 
			
		||||
  const [showCommentActions, setShowCommentActions] = useState(false);
 | 
			
		||||
  const taskDescriptionRef = useRef(task.description ?? '');
 | 
			
		||||
  const $noMemberBtn = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const $addMemberBtn = 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 = () => {
 | 
			
		||||
    onTaskDescriptionChange(task, taskDescriptionRef.current);
 | 
			
		||||
  };
 | 
			
		||||
@@ -584,29 +425,46 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
          <TabBarSection>
 | 
			
		||||
            <TabBarItem>Activity</TabBarItem>
 | 
			
		||||
          </TabBarSection>
 | 
			
		||||
          <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>
 | 
			
		||||
          <ActivitySection />
 | 
			
		||||
        </InnerContentContainer>
 | 
			
		||||
        <CommentContainer>
 | 
			
		||||
          {me && (
 | 
			
		||||
            <CommentCreator
 | 
			
		||||
              me={me}
 | 
			
		||||
              onCreateComment={message => onCreateComment(task, message)}
 | 
			
		||||
              onMemberProfile={onMemberProfile}
 | 
			
		||||
            <CommentInnerWrapper>
 | 
			
		||||
              <CommentProfile
 | 
			
		||||
                member={me}
 | 
			
		||||
                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>
 | 
			
		||||
      </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
 | 
			
		||||
      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 {
 | 
			
		||||
      checklist {
 | 
			
		||||
        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) => {
 | 
			
		||||
      if (typeof $ignoredElementRefsMemoized !== 'undefined') {
 | 
			
		||||
        const isAnyIgnoredElementAncestorOfTarget = $ignoredElementRefsMemoized.some(($elementRef: any) => {
 | 
			
		||||
          if ($elementRef && $elementRef.current) {
 | 
			
		||||
            return (
 | 
			
		||||
              $elementRef.current.contains($mouseDownTargetRef.current) || $elementRef.current.contains(event.target)
 | 
			
		||||
        const isAnyIgnoredElementAncestorOfTarget = $ignoredElementRefsMemoized.some(
 | 
			
		||||
          ($elementRef: any) =>
 | 
			
		||||
            $elementRef.current.contains($mouseDownTargetRef.current) || $elementRef.current.contains(event.target),
 | 
			
		||||
        );
 | 
			
		||||
          }
 | 
			
		||||
          return false;
 | 
			
		||||
        });
 | 
			
		||||
        if (event.button === 0 && !isAnyIgnoredElementAncestorOfTarget) {
 | 
			
		||||
          onOutsideClick();
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,12 @@
 | 
			
		||||
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 = {
 | 
			
		||||
  width?: number | string;
 | 
			
		||||
  height?: number | string;
 | 
			
		||||
  color?: string;
 | 
			
		||||
  width: number | string;
 | 
			
		||||
  height: number | string;
 | 
			
		||||
  color: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const AngleDownOld = ({ width, height, color }: Props) => {
 | 
			
		||||
const AngleDown = ({ width, height, color }: Props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
 | 
			
		||||
      <path
 | 
			
		||||
@@ -26,10 +17,10 @@ const AngleDownOld = ({ width, height, color }: Props) => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
AngleDownOld.defaultProps = {
 | 
			
		||||
AngleDown.defaultProps = {
 | 
			
		||||
  width: 24,
 | 
			
		||||
  height: 16,
 | 
			
		||||
  color: '#000',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AngleDownOld;
 | 
			
		||||
export default AngleDown;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ export function updateApolloCache<T>(
 | 
			
		||||
  update: UpdateCacheFn<T>,
 | 
			
		||||
  variables?: object,
 | 
			
		||||
) {
 | 
			
		||||
  let queryArgs: DataProxy.Query<any, any>;
 | 
			
		||||
  let queryArgs: DataProxy.Query<any>;
 | 
			
		||||
  if (variables) {
 | 
			
		||||
    queryArgs = {
 | 
			
		||||
      query: document,
 | 
			
		||||
 
 | 
			
		||||
@@ -61,9 +61,9 @@ export const base = {
 | 
			
		||||
export const dark = {
 | 
			
		||||
  ...base,
 | 
			
		||||
  background: 'transparent',
 | 
			
		||||
  text: `${theme.colors.text.primary}`,
 | 
			
		||||
  code: `${theme.colors.text.primary}`,
 | 
			
		||||
  cursor: `${theme.colors.text.primary}`,
 | 
			
		||||
  text: `rgba(${theme.colors.text.primary})`,
 | 
			
		||||
  code: `rgba(${theme.colors.text.primary})`,
 | 
			
		||||
  cursor: `rgba(${theme.colors.text.primary})`,
 | 
			
		||||
  divider: '#4E5C6E',
 | 
			
		||||
  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 = {
 | 
			
		||||
  url?: string | null;
 | 
			
		||||
  initials?: string | null;
 | 
			
		||||
@@ -63,39 +56,6 @@ type TaskBadges = {
 | 
			
		||||
  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 = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  taskGroup: InnerTaskGroup;
 | 
			
		||||
@@ -109,8 +69,6 @@ type Task = {
 | 
			
		||||
  description?: string | null;
 | 
			
		||||
  assigned?: Array<TaskUser>;
 | 
			
		||||
  checklists?: Array<TaskChecklist> | null;
 | 
			
		||||
  activity?: Array<TaskActivity> | null;
 | 
			
		||||
  comments?: Array<TaskComment> | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Project = {
 | 
			
		||||
@@ -131,3 +89,10 @@ type Team = {
 | 
			
		||||
  name: 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/vektah/gqlparser/v2 v2.0.1
 | 
			
		||||
	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
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
 | 
			
		||||
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/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI=
 | 
			
		||||
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/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 | 
			
		||||
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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,6 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/route"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/utils"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -76,25 +75,11 @@ func newWebCmd() *cobra.Command {
 | 
			
		||||
				log.Warn("server.secret is not set, generating a random secret")
 | 
			
		||||
				secret = uuid.New().String()
 | 
			
		||||
			}
 | 
			
		||||
			r, _ := route.NewRouter(db, utils.EmailConfig{
 | 
			
		||||
				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))
 | 
			
		||||
			r, _ := route.NewRouter(db, []byte(secret))
 | 
			
		||||
			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")
 | 
			
		||||
 | 
			
		||||
	viper.BindPFlag("migrate", cc.Flags().Lookup("migrate"))
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ package db
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
@@ -104,22 +103,6 @@ type Task struct {
 | 
			
		||||
	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 {
 | 
			
		||||
	TaskAssignedID uuid.UUID `json:"task_assigned_id"`
 | 
			
		||||
	TaskID         uuid.UUID `json:"task_id"`
 | 
			
		||||
@@ -145,16 +128,6 @@ type TaskChecklistItem struct {
 | 
			
		||||
	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 {
 | 
			
		||||
	TaskGroupID uuid.UUID `json:"task_group_id"`
 | 
			
		||||
	ProjectID   uuid.UUID `json:"project_id"`
 | 
			
		||||
 
 | 
			
		||||
@@ -23,12 +23,10 @@ type Querier interface {
 | 
			
		||||
	CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
 | 
			
		||||
	CreateSystemOption(ctx context.Context, arg CreateSystemOptionParams) (SystemOption, 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)
 | 
			
		||||
	CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error)
 | 
			
		||||
	CreateTaskChecklist(ctx context.Context, arg CreateTaskChecklistParams) (TaskChecklist, 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)
 | 
			
		||||
	CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error)
 | 
			
		||||
	CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error)
 | 
			
		||||
@@ -49,7 +47,6 @@ type Querier interface {
 | 
			
		||||
	DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error
 | 
			
		||||
	DeleteTaskChecklistByID(ctx context.Context, taskChecklistID 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)
 | 
			
		||||
	DeleteTaskLabelByID(ctx context.Context, taskLabelID uuid.UUID) error
 | 
			
		||||
	DeleteTaskLabelForTaskByProjectLabelID(ctx context.Context, arg DeleteTaskLabelForTaskByProjectLabelIDParams) error
 | 
			
		||||
@@ -58,7 +55,6 @@ type Querier interface {
 | 
			
		||||
	DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
 | 
			
		||||
	DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) 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)
 | 
			
		||||
	GetAllOrganizations(ctx context.Context) ([]Organization, error)
 | 
			
		||||
	GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
 | 
			
		||||
@@ -69,7 +65,6 @@ type Querier interface {
 | 
			
		||||
	GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
 | 
			
		||||
	GetAllVisibleProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, 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)
 | 
			
		||||
	GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, 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)
 | 
			
		||||
	GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (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)
 | 
			
		||||
	GetMemberProjectIDsForUserID(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)
 | 
			
		||||
	GetTeamsForOrganization(ctx context.Context, organizationID 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)
 | 
			
		||||
	GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
 | 
			
		||||
	GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
 | 
			
		||||
@@ -127,7 +120,6 @@ type Querier interface {
 | 
			
		||||
	HasActiveUser(ctx context.Context) (bool, error)
 | 
			
		||||
	HasAnyUser(ctx context.Context) (bool, error)
 | 
			
		||||
	SetFirstUserActive(ctx context.Context) (UserAccount, error)
 | 
			
		||||
	SetInactiveLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) error
 | 
			
		||||
	SetTaskChecklistItemComplete(ctx context.Context, arg SetTaskChecklistItemCompleteParams) (TaskChecklistItem, error)
 | 
			
		||||
	SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
 | 
			
		||||
	SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
 | 
			
		||||
@@ -142,7 +134,6 @@ type Querier interface {
 | 
			
		||||
	UpdateTaskChecklistItemName(ctx context.Context, arg UpdateTaskChecklistItemNameParams) (TaskChecklistItem, error)
 | 
			
		||||
	UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (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)
 | 
			
		||||
	UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDateParams) (Task, 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
 | 
			
		||||
  INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
 | 
			
		||||
  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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
DELETE FROM task WHERE task_id = $1
 | 
			
		||||
`
 | 
			
		||||
@@ -126,25 +94,6 @@ func (q *Queries) DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error {
 | 
			
		||||
	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
 | 
			
		||||
DELETE FROM task where task_group_id = $1
 | 
			
		||||
`
 | 
			
		||||
@@ -194,41 +143,6 @@ func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
 | 
			
		||||
	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
 | 
			
		||||
SELECT project_id FROM task
 | 
			
		||||
  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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
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.
 | 
			
		||||
func NewHandler(repo db.Repository, emailConfig utils.EmailConfig) http.Handler {
 | 
			
		||||
func NewHandler(repo db.Repository) http.Handler {
 | 
			
		||||
	c := Config{
 | 
			
		||||
		Resolvers: &Resolver{
 | 
			
		||||
			Repository: repo,
 | 
			
		||||
			EmailConfig: emailConfig,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	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
 | 
			
		||||
		in := graphql.GetFieldContext(ctx).Args["input"]
 | 
			
		||||
		in := graphql.GetResolverContext(ctx).Args["input"]
 | 
			
		||||
		val := reflect.ValueOf(in) // could be any underlying type
 | 
			
		||||
		if val.Kind() == reflect.Ptr {
 | 
			
		||||
			val = reflect.Indirect(val)
 | 
			
		||||
@@ -256,28 +255,3 @@ func GetActionType(actionType int32) ActionType {
 | 
			
		||||
		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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ActivityData struct {
 | 
			
		||||
	Data map[string]string
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,12 +22,6 @@ type AssignTaskInput struct {
 | 
			
		||||
	UserID uuid.UUID `json:"userID"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CausedBy struct {
 | 
			
		||||
	ID          uuid.UUID    `json:"id"`
 | 
			
		||||
	FullName    string       `json:"fullName"`
 | 
			
		||||
	ProfileIcon *ProfileIcon `json:"profileIcon"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChecklistBadge struct {
 | 
			
		||||
	Complete int `json:"complete"`
 | 
			
		||||
	Total    int `json:"total"`
 | 
			
		||||
@@ -45,16 +39,6 @@ type CreateTaskChecklistItem struct {
 | 
			
		||||
	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 {
 | 
			
		||||
	UserID uuid.UUID `json:"userID"`
 | 
			
		||||
	TeamID uuid.UUID `json:"teamID"`
 | 
			
		||||
@@ -65,12 +49,6 @@ type CreateTeamMemberPayload struct {
 | 
			
		||||
	TeamMember *Member  `json:"teamMember"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CreatedBy struct {
 | 
			
		||||
	ID          uuid.UUID    `json:"id"`
 | 
			
		||||
	FullName    string       `json:"fullName"`
 | 
			
		||||
	ProfileIcon *ProfileIcon `json:"profileIcon"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DeleteInvitedProjectMember struct {
 | 
			
		||||
	ProjectID uuid.UUID `json:"projectID"`
 | 
			
		||||
	Email     string    `json:"email"`
 | 
			
		||||
@@ -130,15 +108,6 @@ type DeleteTaskChecklistPayload struct {
 | 
			
		||||
	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 {
 | 
			
		||||
	TaskGroupID uuid.UUID `json:"taskGroupID"`
 | 
			
		||||
}
 | 
			
		||||
@@ -405,11 +374,6 @@ type SortTaskGroupPayload struct {
 | 
			
		||||
	Tasks       []db.Task `json:"tasks"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TaskActivityData struct {
 | 
			
		||||
	Name  string `json:"name"`
 | 
			
		||||
	Value string `json:"value"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TaskBadges struct {
 | 
			
		||||
	Checklist *ChecklistBadge `json:"checklist"`
 | 
			
		||||
}
 | 
			
		||||
@@ -502,16 +466,6 @@ type UpdateTaskChecklistName struct {
 | 
			
		||||
	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 {
 | 
			
		||||
	TaskID      uuid.UUID `json:"taskID"`
 | 
			
		||||
	Description string    `json:"description"`
 | 
			
		||||
@@ -661,63 +615,6 @@ func (e ActionType) MarshalGQL(w io.Writer) {
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,10 @@ import (
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/db"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Resolver handles resolving GraphQL queries & mutations
 | 
			
		||||
type Resolver struct {
 | 
			
		||||
	Repository db.Repository
 | 
			
		||||
	EmailConfig utils.EmailConfig
 | 
			
		||||
	mu         sync.Mutex
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -135,38 +135,6 @@ type TaskBadges {
 | 
			
		||||
  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 {
 | 
			
		||||
  id: ID!
 | 
			
		||||
  taskGroup: TaskGroup!
 | 
			
		||||
@@ -181,23 +149,6 @@ type Task {
 | 
			
		||||
  labels: [TaskLabel!]!
 | 
			
		||||
  checklists: [TaskChecklist!]!
 | 
			
		||||
  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 {
 | 
			
		||||
@@ -631,44 +582,6 @@ type DeleteTaskChecklistPayload {
 | 
			
		||||
  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 {
 | 
			
		||||
  createTaskGroup(input: NewTaskGroup!):
 | 
			
		||||
    TaskGroup! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
 | 
			
		||||
@@ -943,3 +856,4 @@ type DeleteUserAccountPayload {
 | 
			
		||||
  ok: Boolean!
 | 
			
		||||
  userAccount: UserAccount!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,9 @@ package graph
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -15,8 +16,10 @@ import (
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/auth"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/db"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/logger"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/utils"
 | 
			
		||||
	"github.com/lithammer/fuzzysearch/fuzzy"
 | 
			
		||||
	gomail "gopkg.in/mail.v2"
 | 
			
		||||
 | 
			
		||||
	hermes "github.com/matcornic/hermes/v2"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/vektah/gqlparser/v2/gqlerror"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
@@ -191,11 +194,79 @@ func (r *mutationResolver) InviteProjectMembers(ctx context.Context, input Invit
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						return &InviteProjectMembersPayload{Ok: false}, err
 | 
			
		||||
					}
 | 
			
		||||
					invite := utils.EmailInvite{To: *invitedMember.Email, FullName: *invitedMember.Email, ConfirmToken: confirmToken.ConfirmTokenID.String()}
 | 
			
		||||
					err = utils.SendEmailInvite(r.EmailConfig, invite)
 | 
			
		||||
					// send out invitation
 | 
			
		||||
					// 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 {
 | 
			
		||||
						logger.New(ctx).WithError(err).Error("issue sending email")
 | 
			
		||||
						return &InviteProjectMembersPayload{Ok: false}, err
 | 
			
		||||
						panic(err) // Tip: Handle error with something else than a panic ;)
 | 
			
		||||
					}
 | 
			
		||||
					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 {
 | 
			
		||||
					return &InviteProjectMembersPayload{Ok: false}, err
 | 
			
		||||
@@ -292,28 +363,6 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.T
 | 
			
		||||
	createdAt := time.Now().UTC()
 | 
			
		||||
	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})
 | 
			
		||||
	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 {
 | 
			
		||||
		logger.New(ctx).WithError(err).Error("issue while creating task")
 | 
			
		||||
		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) {
 | 
			
		||||
	userID, _ := GetUserID(ctx)
 | 
			
		||||
	previousTask, err := r.Repository.GetTaskByID(ctx, input.TaskID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &UpdateTaskLocationPayload{}, err
 | 
			
		||||
	}
 | 
			
		||||
	task, _ := r.Repository.UpdateTaskLocation(ctx, db.UpdateTaskLocationParams{TaskID: input.TaskID, TaskGroupID: input.TaskGroupID, Position: input.Position})
 | 
			
		||||
	if previousTask.TaskGroupID != input.TaskGroupID {
 | 
			
		||||
		skipAndDelete := false
 | 
			
		||||
		lastMove, err := r.Repository.GetLastMoveForTaskID(ctx, input.TaskID)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			if lastMove.Active && lastMove.PrevTaskGroupID == input.TaskGroupID.String() {
 | 
			
		||||
				skipAndDelete = true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if skipAndDelete {
 | 
			
		||||
			_ = r.Repository.SetInactiveLastMoveForTaskID(ctx, input.TaskID)
 | 
			
		||||
		} else {
 | 
			
		||||
			prevTaskGroup, _ := r.Repository.GetTaskGroupByID(ctx, previousTask.TaskGroupID)
 | 
			
		||||
			curTaskGroup, _ := r.Repository.GetTaskGroupByID(ctx, input.TaskGroupID)
 | 
			
		||||
	task, err := r.Repository.UpdateTaskLocation(ctx, db.UpdateTaskLocationParams{input.TaskID, input.TaskGroupID, input.Position})
 | 
			
		||||
 | 
			
		||||
			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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -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) {
 | 
			
		||||
	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}})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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) {
 | 
			
		||||
	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
 | 
			
		||||
	if input.DueDate == nil {
 | 
			
		||||
		dueDate = sql.NullTime{Valid: false, Time: time.Now()}
 | 
			
		||||
@@ -436,15 +421,6 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa
 | 
			
		||||
		TaskID:  input.TaskID,
 | 
			
		||||
		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
 | 
			
		||||
}
 | 
			
		||||
@@ -586,33 +562,6 @@ func (r *mutationResolver) UpdateTaskChecklistItemLocation(ctx context.Context,
 | 
			
		||||
	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) {
 | 
			
		||||
	createdAt := time.Now().UTC()
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
	return obj.TaskChecklistID, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1699,31 +1574,6 @@ func (r *taskChecklistItemResolver) DueDate(ctx context.Context, obj *db.TaskChe
 | 
			
		||||
	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) {
 | 
			
		||||
	return obj.TaskGroupID, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1769,7 +1619,6 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
 | 
			
		||||
		if user.ProfileAvatarUrl.Valid {
 | 
			
		||||
			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})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
 | 
			
		||||
		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},
 | 
			
		||||
		})
 | 
			
		||||
@@ -1874,9 +1724,6 @@ func (r *Resolver) RefreshToken() RefreshTokenResolver { return &refreshTokenRes
 | 
			
		||||
// Task returns TaskResolver implementation.
 | 
			
		||||
func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
 | 
			
		||||
 | 
			
		||||
// TaskActivity returns TaskActivityResolver implementation.
 | 
			
		||||
func (r *Resolver) TaskActivity() TaskActivityResolver { return &taskActivityResolver{r} }
 | 
			
		||||
 | 
			
		||||
// TaskChecklist returns TaskChecklistResolver implementation.
 | 
			
		||||
func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} }
 | 
			
		||||
 | 
			
		||||
@@ -1885,9 +1732,6 @@ func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver {
 | 
			
		||||
	return &taskChecklistItemResolver{r}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TaskComment returns TaskCommentResolver implementation.
 | 
			
		||||
func (r *Resolver) TaskComment() TaskCommentResolver { return &taskCommentResolver{r} }
 | 
			
		||||
 | 
			
		||||
// TaskGroup returns TaskGroupResolver implementation.
 | 
			
		||||
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
 | 
			
		||||
 | 
			
		||||
@@ -1909,11 +1753,27 @@ type projectLabelResolver struct{ *Resolver }
 | 
			
		||||
type queryResolver struct{ *Resolver }
 | 
			
		||||
type refreshTokenResolver struct{ *Resolver }
 | 
			
		||||
type taskResolver struct{ *Resolver }
 | 
			
		||||
type taskActivityResolver struct{ *Resolver }
 | 
			
		||||
type taskChecklistResolver struct{ *Resolver }
 | 
			
		||||
type taskChecklistItemResolver struct{ *Resolver }
 | 
			
		||||
type taskCommentResolver struct{ *Resolver }
 | 
			
		||||
type taskGroupResolver struct{ *Resolver }
 | 
			
		||||
type taskLabelResolver struct{ *Resolver }
 | 
			
		||||
type teamResolver 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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
  id: ID!
 | 
			
		||||
  taskGroup: TaskGroup!
 | 
			
		||||
@@ -181,23 +149,6 @@ type Task {
 | 
			
		||||
  labels: [TaskLabel!]!
 | 
			
		||||
  checklists: [TaskChecklist!]!
 | 
			
		||||
  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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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/graph"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/logger"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// FrontendHandler serves an embed React client through chi
 | 
			
		||||
@@ -65,7 +64,7 @@ type TaskcafeHandler struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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.TimestampFormat = "02-01-2006 15:04:05"
 | 
			
		||||
	formatter.FullTimestamp = true
 | 
			
		||||
@@ -95,7 +94,7 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, jwtKey []by
 | 
			
		||||
	r.Group(func(mux chi.Router) {
 | 
			
		||||
		mux.Use(auth.Middleware)
 | 
			
		||||
		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"}
 | 
			
		||||
 
 | 
			
		||||
@@ -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