diff --git a/frontend/src/App/Routes.tsx b/frontend/src/App/Routes.tsx
index 6e7a4b0..480c16e 100644
--- a/frontend/src/App/Routes.tsx
+++ b/frontend/src/App/Routes.tsx
@@ -4,6 +4,7 @@ import * as H from 'history';
import Dashboard from 'Dashboard';
import Admin from 'Admin';
+import MyTasks from 'MyTasks';
import Confirm from 'Confirm';
import Projects from 'Projects';
import Project from 'Projects/Project';
@@ -69,6 +70,7 @@ const AuthorizedRoutes = () => {
+
);
diff --git a/frontend/src/App/TopNavbar.tsx b/frontend/src/App/TopNavbar.tsx
index 4186c53..e887a27 100644
--- a/frontend/src/App/TopNavbar.tsx
+++ b/frontend/src/App/TopNavbar.tsx
@@ -439,6 +439,9 @@ const GlobalTopNavbar: React.FC = ({
onDashboardClick={() => {
history.push('/');
}}
+ onMyTasksClick={() => {
+ history.push('/tasks');
+ }}
projectMembers={projectMembers}
projectInvitedMembers={projectInvitedMembers}
onProfileClick={onProfileClick}
diff --git a/frontend/src/MyTasks/MyTasksSort.tsx b/frontend/src/MyTasks/MyTasksSort.tsx
new file mode 100644
index 0000000..bb9e72e
--- /dev/null
+++ b/frontend/src/MyTasks/MyTasksSort.tsx
@@ -0,0 +1,145 @@
+import React, { useState, useEffect } from 'react';
+import styled, { css } from 'styled-components';
+import { Checkmark, User, Calendar, Tags, Clock } from 'shared/icons';
+import { TaskMetaFilters, TaskMeta, TaskMetaMatch, DueDateFilterType } from 'shared/components/Lists';
+import Input from 'shared/components/ControlledInput';
+import { Popup, usePopup } from 'shared/components/PopupMenu';
+import produce from 'immer';
+import { mixin } from 'shared/utils/styles';
+import Member from 'shared/components/Member';
+import { MyTasksSort } from 'shared/generated/graphql';
+
+const FilterMember = styled(Member)`
+ margin: 2px 0;
+ &:hover {
+ cursor: pointer;
+ background: ${props => props.theme.colors.primary};
+ }
+`;
+
+export const Labels = styled.ul`
+ list-style: none;
+ margin: 0 8px;
+ padding: 0;
+ margin-bottom: 8px;
+`;
+
+export const Label = styled.li`
+ position: relative;
+`;
+
+export const CardLabel = styled.span<{ active: boolean; color: string }>`
+ ${props =>
+ props.active &&
+ css`
+ margin-left: 4px;
+ box-shadow: -8px 0 ${mixin.darken(props.color, 0.12)};
+ border-radius: 3px;
+ `}
+
+ cursor: pointer;
+ font-weight: 700;
+ margin: 0 0 4px;
+ min-height: 20px;
+ padding: 6px 12px;
+ position: relative;
+ transition: padding 85ms, margin 85ms, box-shadow 85ms;
+ background-color: ${props => props.color};
+ color: #fff;
+ display: block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ min-height: 31px;
+`;
+
+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 ActionItemSeparator = styled.li`
+ color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
+ font-size: 12px;
+ padding-left: 4px;
+ padding-right: 4px;
+ padding-top: 0.75rem;
+ padding-bottom: 0.25rem;
+`;
+
+const ActiveIcon = styled(Checkmark)`
+ position: absolute;
+ right: 4px;
+`;
+
+const ItemIcon = styled.div`
+ position: absolute;
+`;
+
+const TaskNameInput = styled(Input)`
+ margin: 0;
+`;
+
+const ActionItemLine = styled.div`
+ height: 1px;
+ border-top: 1px solid #414561;
+ margin: 0.25rem !important;
+`;
+
+type MyTasksSortProps = {
+ sort: MyTasksSort;
+ onChangeSort: (sort: MyTasksSort) => void;
+};
+
+const MyTasksSortPopup: React.FC = ({ sort: initialSort, onChangeSort }) => {
+ const [sort, setSort] = useState(initialSort);
+ const handleChangeSort = (f: MyTasksSort) => {
+ setSort(f);
+ onChangeSort(f);
+ };
+
+ return (
+ <>
+
+
+ handleChangeSort(MyTasksSort.None)}>
+ {sort === MyTasksSort.None && }
+ None
+
+ handleChangeSort(MyTasksSort.Project)}>
+ {sort === MyTasksSort.Project && }
+ Project
+
+ handleChangeSort(MyTasksSort.DueDate)}>
+ {sort === MyTasksSort.DueDate && }
+ Due Date
+
+
+
+ >
+ );
+};
+
+export default MyTasksSortPopup;
diff --git a/frontend/src/MyTasks/TaskEntry.tsx b/frontend/src/MyTasks/TaskEntry.tsx
new file mode 100644
index 0000000..7c6a7c7
--- /dev/null
+++ b/frontend/src/MyTasks/TaskEntry.tsx
@@ -0,0 +1,413 @@
+import React, { useState, useRef } from 'react';
+import styled, { css } from 'styled-components/macro';
+import dayjs from 'dayjs';
+import { CheckCircleOutline, CheckCircle, Cross, Briefcase, ChevronRight } from 'shared/icons';
+import { mixin } from 'shared/utils/styles';
+
+const RIGHT_ROW_WIDTH = 327;
+const TaskName = styled.div<{ focused: boolean }>`
+ flex: 0 1 auto;
+ min-width: 1px;
+ overflow: hidden;
+ margin-right: 4px;
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ height: 20px;
+ padding: 0 1px;
+
+ max-height: 100%;
+ position: relative;
+ &:hover {
+ ${props =>
+ !props.focused &&
+ css`
+ border-color: #9ca6af !important;
+ border: 1px solid ${props.theme.colors.primary} !important;
+ `}
+ }
+`;
+
+const DueDateCell = styled.div`
+ align-items: center;
+ align-self: stretch;
+ display: flex;
+ flex-grow: 1;
+`;
+
+const CellPlaceholder = styled.div<{ width: number }>`
+ min-width: ${p => p.width}px;
+ width: ${p => p.width}px;
+`;
+const DueDateCellDisplay = styled.div`
+ align-items: center;
+ cursor: pointer;
+ display: flex;
+ flex-grow: 1;
+ height: 100%;
+`;
+
+const DueDateCellLabel = styled.div`
+ align-items: center;
+ color: ${props => props.theme.colors.text.primary};
+
+ font-size: 11px;
+ flex: 0 1 auto;
+ min-width: 1px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: flex;
+ flex-flow: row wrap;
+ white-space: pre-wrap;
+`;
+
+const DueDateRemoveButton = styled.div`
+ align-items: center;
+ bottom: 0;
+ cursor: pointer;
+ display: flex;
+ height: 100%;
+ padding-left: 4px;
+ padding-right: 8px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ visibility: hidden;
+ svg {
+ fill: ${props => props.theme.colors.text.primary};
+ }
+ &:hover svg {
+ fill: ${props => props.theme.colors.text.secondary};
+ }
+`;
+const TaskGroupItemCell = styled.div<{ width: number; focused: boolean }>`
+ width: ${p => p.width}px;
+ background: transparent;
+ position: relative;
+
+ border: 1px solid #414561;
+ justify-content: space-between;
+ margin-right: -1px;
+ z-index: 0;
+ padding: 0 8px;
+ align-items: center;
+ display: flex;
+ height: 37px;
+ overflow: hidden;
+ &:hover ${DueDateRemoveButton} {
+ visibility: visible;
+ }
+ &:hover ${TaskName} {
+ ${props =>
+ !props.focused &&
+ css`
+ background: ${props.theme.colors.bg.secondary};
+ border: 1px solid ${mixin.darken(props.theme.colors.bg.secondary, 0.25)};
+ border-radius: 2px;
+ cursor: text;
+ `}
+ }
+`;
+
+const TaskGroupItem = styled.div`
+ padding-right: 24px;
+ contain: style;
+ display: flex;
+ margin-bottom: -1px;
+ margin-top: -1px;
+ height: 37px;
+ &:hover {
+ background-color: #161d31;
+ }
+ & ${TaskGroupItemCell}:first-child {
+ position: absolute;
+ padding: 0 4px 0 0;
+ margin-left: 24px;
+ left: 0;
+ flex: 1 1 auto;
+ min-width: 1px;
+ border-right: 0;
+ border-left: 0;
+ }
+ & ${TaskGroupItemCell}:last-child {
+ border-right: 0;
+ }
+`;
+
+const TaskItemComplete = styled.div`
+ flex: 0 0 auto;
+ margin: 0 3px 0 0;
+ align-items: center;
+ box-sizing: border-box;
+ display: inline-flex;
+ height: 16px;
+ justify-content: center;
+ overflow: visible;
+ width: 16px;
+ cursor: pointer;
+ svg {
+ transition: all 0.2 ease;
+ }
+ &:hover svg {
+ fill: ${props => props.theme.colors.primary};
+ }
+`;
+
+const TaskDetailsButton = styled.div`
+ align-items: center;
+ cursor: pointer;
+ display: flex;
+ font-size: 12px;
+ height: 100%;
+ justify-content: flex-end;
+ margin-left: auto;
+ opacity: 0;
+ padding-left: 4px;
+ color: ${props => props.theme.colors.text.primary};
+ svg {
+ fill: ${props => props.theme.colors.text.primary};
+ }
+`;
+
+const TaskDetailsArea = styled.div`
+ align-items: center;
+ cursor: pointer;
+ display: flex;
+ flex: 1 0 auto;
+ height: 100%;
+ margin-right: 24px;
+ &:hover ${TaskDetailsButton} {
+ opacity: 1;
+ }
+`;
+
+const TaskDetailsWorkpace = styled(Briefcase)`
+ flex: 0 0 auto;
+ margin-right: 8px;
+`;
+const TaskDetailsLabel = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
+const TaskDetailsChevron = styled(ChevronRight)`
+ margin-left: 4px;
+ flex: 0 0 auto;
+`;
+
+const TaskNameShadow = styled.div`
+ box-sizing: border-box;
+ min-height: 1em;
+ overflow: hidden;
+ visibility: hidden;
+ white-space: pre;
+ border: 0;
+ font-size: 13px;
+ line-height: 20px;
+ margin: 0;
+ min-width: 20px;
+ padding: 0 4px;
+ text-rendering: optimizeSpeed;
+`;
+
+const TaskNameInput = styled.textarea`
+ white-space: pre;
+ background: transparent;
+ border-radius: 0;
+ display: block;
+ color: ${props => props.theme.colors.text.primary};
+ height: 100%;
+ outline: 0;
+ overflow: hidden;
+ position: absolute;
+ resize: none;
+ top: 0;
+ width: 100%;
+ border: 0;
+ font-size: 13px;
+ line-height: 20px;
+ margin: 0;
+ min-width: 20px;
+ padding: 0 4px;
+ text-rendering: optimizeSpeed;
+`;
+
+const ProjectPill = styled.div`
+ background-color: ${props => props.theme.colors.bg.primary};
+ text-overflow: ellipsis;
+ border-radius: 10px;
+ box-sizing: border-box;
+ display: block;
+ font-size: 12px;
+ font-weight: 400;
+ height: 20px;
+ line-height: 20px;
+ overflow: hidden;
+ padding: 0 8px;
+ text-align: left;
+ white-space: nowrap;
+`;
+
+const ProjectPillContents = styled.div`
+ align-items: center;
+ display: flex;
+`;
+
+const ProjectPillName = styled.span`
+ flex: 0 1 auto;
+ min-width: 1px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: ${props => props.theme.colors.text.primary};
+`;
+
+const ProjectPillColor = styled.svg`
+ overflow: hidden;
+ flex: 0 0 auto;
+ margin-right: 4px;
+ fill: #0064fb;
+ height: 12px;
+ width: 12px;
+`;
+
+type TaskEntryProps = {
+ name: string;
+ dueDate?: string | null;
+ onEditName: (name: string) => void;
+ project: string;
+ hasTime: boolean;
+ autoFocus?: boolean;
+ onEditProject: ($target: React.RefObject) => void;
+ onToggleComplete: (complete: boolean) => void;
+ complete: boolean;
+ onEditDueDate: ($target: React.RefObject) => void;
+ onTaskDetails: () => void;
+ onRemoveDueDate: () => void;
+};
+
+const TaskEntry: React.FC = ({
+ autoFocus = false,
+ onToggleComplete,
+ onEditName,
+ onTaskDetails,
+ name: initialName,
+ complete,
+ project,
+ dueDate,
+ hasTime,
+ onEditProject,
+ onEditDueDate,
+ onRemoveDueDate,
+}) => {
+ const leftRow = window.innerWidth - RIGHT_ROW_WIDTH;
+ const [focused, setFocused] = useState(autoFocus);
+ const [name, setName] = useState(initialName);
+ const $projects = useRef(null);
+ const $dueDate = useRef(null);
+ const $nameInput = useRef(null);
+ return (
+
+
+ onToggleComplete(!complete)}>
+ {complete ? : }
+
+
+ {name}
+ setFocused(true)}
+ ref={$nameInput}
+ onBlur={() => {
+ setFocused(false);
+ onEditName(name);
+ }}
+ onKeyDown={e => {
+ if (e.keyCode === 13) {
+ e.preventDefault();
+ if ($nameInput.current) {
+ $nameInput.current.blur();
+ }
+ }
+ }}
+ onChange={e => setName(e.currentTarget.value)}
+ wrap="off"
+ rows={1}
+ >
+ {name}
+
+
+ onTaskDetails()}>
+
+
+
+ Details
+
+
+
+
+
+
+
+ onEditDueDate($dueDate)}>
+
+
+ {dueDate ? dayjs(dueDate).format(hasTime ? 'MMM D [at] h:mm A' : 'MMM D') : ''}
+
+
+
+ {dueDate && (
+ onRemoveDueDate()}>
+
+
+ )}
+
+
+ {
+ onEditProject($projects);
+ }}
+ >
+
+
+
+
+ {project}
+
+
+
+
+
+ );
+};
+export default TaskEntry;
+type NewTaskEntryProps = {
+ onClick: () => void;
+};
+const AddTaskLabel = styled.span`
+ font-size: 14px;
+ position: relative;
+
+ color: ${props => props.theme.colors.text.primary};
+
+ justify-content: space-between;
+ z-index: 0;
+ padding: 0 8px;
+ align-items: center;
+ display: flex;
+ height: 37px;
+ flex: 1 1;
+ cursor: pointer;
+ margin-left: 24px;
+`;
+
+const NewTaskEntry: React.FC = ({ onClick }) => {
+ const leftRow = window.innerWidth - RIGHT_ROW_WIDTH;
+ return (
+
+ Add task...
+
+ );
+};
+
+export { NewTaskEntry };
diff --git a/frontend/src/MyTasks/index.tsx b/frontend/src/MyTasks/index.tsx
new file mode 100644
index 0000000..b61e3b3
--- /dev/null
+++ b/frontend/src/MyTasks/index.tsx
@@ -0,0 +1,868 @@
+import React, { useState, useEffect, useRef } from 'react';
+import styled, { css } from 'styled-components/macro';
+import GlobalTopNavbar from 'App/TopNavbar';
+import Details from 'Projects/Project/Details';
+import {
+ useMyTasksQuery,
+ MyTasksSort,
+ MyTasksStatus,
+ useCreateTaskMutation,
+ MyTasksQuery,
+ MyTasksDocument,
+ useUpdateTaskNameMutation,
+ useSetTaskCompleteMutation,
+ useUpdateTaskDueDateMutation,
+} from 'shared/generated/graphql';
+
+import { Route, useRouteMatch, useHistory, RouteComponentProps } from 'react-router-dom';
+import { usePopup, Popup } from 'shared/components/PopupMenu';
+import updateApolloCache from 'shared/utils/cache';
+import produce from 'immer';
+import NOOP from 'shared/utils/noop';
+import { Sort, Cogs, CaretDown, CheckCircle, CaretRight } from 'shared/icons';
+import Select from 'react-select';
+import { editorColourStyles } from 'shared/components/Select';
+import useOnOutsideClick from 'shared/hooks/onOutsideClick';
+import DueDateManager from 'shared/components/DueDateManager';
+import dayjs from 'dayjs';
+import useStickyState from 'shared/hooks/useStickyState';
+import MyTasksSortPopup from './MyTasksSort';
+import TaskEntry from './TaskEntry';
+
+type TaskRouteProps = {
+ taskID: string;
+};
+
+function prettySort(sort: MyTasksSort) {
+ if (sort === MyTasksSort.None) {
+ return 'Sort';
+ }
+ return `Sort: ${sort.charAt(0) +
+ sort
+ .slice(1)
+ .toLowerCase()
+ .replace(/_/gi, ' ')}`;
+}
+
+type Group = {
+ id: string;
+ name: string | null;
+ tasks: Array;
+};
+const DueDateEditorLabel = styled.div`
+ align-items: center;
+ color: ${props => props.theme.colors.text.primary};
+
+ font-size: 11px;
+ padding: 0 8px;
+ flex: 0 1 auto;
+ min-width: 1px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: flex;
+ flex-flow: row wrap;
+ white-space: pre-wrap;
+ height: 35px;
+`;
+
+const ProjectBar = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 40px;
+ padding: 0 12px;
+`;
+
+const ProjectActions = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
+const ProjectActionWrapper = styled.div<{ disabled?: boolean }>`
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ font-size: 15px;
+ color: ${props => props.theme.colors.text.primary};
+
+ &:not(:last-of-type) {
+ margin-right: 16px;
+ }
+
+ &:hover {
+ color: ${props => props.theme.colors.text.secondary};
+ }
+ ${props =>
+ props.disabled &&
+ css`
+ opacity: 0.5;
+ cursor: default;
+ pointer-events: none;
+ `}
+`;
+
+const ProjectActionText = styled.span`
+ padding-left: 4px;
+`;
+
+type ProjectActionProps = {
+ onClick?: (target: React.RefObject) => void;
+ disabled?: boolean;
+};
+
+const ProjectAction: React.FC = ({ onClick, disabled = false, children }) => {
+ const $container = useRef(null);
+ const handleClick = () => {
+ if (onClick) {
+ onClick($container);
+ }
+ };
+ return (
+
+ {children}
+
+ );
+};
+
+const EditorPositioner = styled.div<{ top: number; left: number }>`
+ position: absolute;
+ top: ${p => p.top}px;
+ justify-content: flex-end;
+ margin-left: -100vw;
+ z-index: 10000;
+ align-items: flex-start;
+ display: flex;
+ font-size: 13px;
+ height: 0;
+ position: fixed;
+ width: 100vw;
+ left: ${p => p.left}px;
+`;
+
+const EditorPositionerContents = styled.div`
+ position: relative;
+`;
+
+const EditorContainer = styled.div<{ width: number }>`
+ border: 1px solid ${props => props.theme.colors.primary};
+ background: ${props => props.theme.colors.bg.secondary};
+ position: relative;
+ width: ${p => p.width}px;
+`;
+
+const EditorCell = styled.div<{ width: number }>`
+ display: flex;
+ width: ${p => p.width}px;
+`;
+
+// TABLE
+const VerticalScoller = styled.div`
+ contain: strict;
+ flex: 1 1 auto;
+ overflow-x: hidden;
+ padding-bottom: 1px;
+ position: relative;
+
+ min-height: 1px;
+ overflow-y: auto;
+`;
+
+const VerticalScollerInner = styled.div`
+ min-height: 100%;
+ overflow-y: hidden;
+ min-width: 1px;
+ overflow-x: auto;
+`;
+
+const VerticalScollerInnerBar = styled.div`
+ display: flex;
+ margin: 0 24px;
+ margin-bottom: 1px;
+ border-top: 1px solid #414561;
+`;
+
+const TableContents = styled.div`
+ box-sizing: border-box;
+ display: inline-block;
+ margin-bottom: 32px;
+ min-width: 100%;
+`;
+
+const TaskGroupContainer = styled.div``;
+
+const TaskGroupHeader = styled.div`
+ height: 50px;
+ width: 100%;
+`;
+
+const TaskGroupItems = styled.div`
+ overflow: unset;
+`;
+
+const ProjectPill = styled.div`
+ background-color: ${props => props.theme.colors.bg.primary};
+ text-overflow: ellipsis;
+ border-radius: 10px;
+ box-sizing: border-box;
+ display: block;
+ font-size: 12px;
+ font-weight: 400;
+ height: 20px;
+ line-height: 20px;
+ overflow: hidden;
+ padding: 0 8px;
+ text-align: left;
+ white-space: nowrap;
+`;
+
+const ProjectPillContents = styled.div`
+ align-items: center;
+ display: flex;
+`;
+
+const ProjectPillName = styled.span`
+ flex: 0 1 auto;
+ min-width: 1px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: ${props => props.theme.colors.text.primary};
+`;
+
+const ProjectPillColor = styled.svg`
+ overflow: hidden;
+ flex: 0 0 auto;
+ margin-right: 4px;
+ fill: #0064fb;
+ height: 12px;
+ width: 12px;
+`;
+
+const SingleValue = ({ children, ...props }: any) => {
+ return (
+
+
+
+
+
+ {children}
+
+
+ );
+};
+
+const OptionWrapper = styled.div`
+ align-items: center;
+ display: flex;
+ height: 40px;
+ padding: 0 16px;
+ cursor: pointer;
+ &:hover {
+ background: #414561;
+ }
+`;
+
+const OptionLabel = styled.div`
+ align-items: baseline;
+ display: flex;
+ min-width: 1px;
+`;
+
+const OptionTitle = styled.div`
+ min-width: 50px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+const OptionSubTitle = styled.div`
+ color: ${props => props.theme.colors.text.primary};
+ font-size: 11px;
+ margin-left: 8px;
+ min-width: 50px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+const Option = ({ innerProps, data }: any) => {
+ return (
+
+
+ {data.label}
+ {data.label}
+
+
+ );
+};
+
+const TaskGroupHeaderContents = styled.div<{ width: number }>`
+ width: ${p => p.width}px;
+ left: 0;
+ position: absolute;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ font-weight: 500;
+ margin-left: 24px;
+ line-height: 20px;
+ align-items: center;
+ box-sizing: border-box;
+ display: flex;
+ flex: 1 1 auto;
+ min-height: 30px;
+ padding-right: 32px;
+ position: relative;
+ border-bottom: 1px solid transparent;
+ border-top: 1px solid transparent;
+`;
+
+const TaskGroupMinify = styled.div`
+ height: 28px;
+ min-height: 28px;
+ min-width: 28px;
+ width: 28px;
+ border-radius: 6px;
+ user-select: none;
+
+ margin-right: 4px;
+
+ align-items: center;
+ box-sizing: border-box;
+ display: inline-flex;
+ justify-content: center;
+ transition-duration: 0.2s;
+ transition-property: background, border, box-shadow, fill;
+ cursor: pointer;
+ svg {
+ fill: ${props => props.theme.colors.text.primary};
+ transition-duration: 0.2s;
+ transition-property: background, border, box-shadow, fill;
+ }
+
+ &:hover svg {
+ fill: ${props => props.theme.colors.text.secondary};
+ }
+`;
+const TaskGroupName = styled.div`
+ flex-grow: 1;
+ align-items: center;
+ display: flex;
+ height: 50px;
+ min-width: 1px;
+ color: ${props => props.theme.colors.text.secondary};
+ font-weight: 400;
+`;
+
+// HEADER
+const ScrollContainer = styled.div`
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+ min-height: 1px;
+ position: relative;
+ width: 100%;
+`;
+
+const Row = styled.div`
+ box-sizing: border-box;
+ flex: 0 0 auto;
+ height: 37px;
+ position: relative;
+`;
+
+const RowHeaderLeft = styled.div<{ width: number }>`
+ width: ${p => p.width}px;
+
+ align-items: stretch;
+ display: flex;
+ flex-direction: column;
+ height: 37px;
+ left: 0;
+ position: absolute;
+ z-index: 100;
+`;
+const RowHeaderLeftInner = styled.div`
+ align-items: stretch;
+ color: ${props => props.theme.colors.text.primary};
+ display: flex;
+ flex: 1 0 auto;
+ font-size: 12px;
+ margin-right: -1px;
+ padding-left: 24px;
+`;
+const RowHeaderLeftName = styled.div`
+ position: relative;
+ align-items: center;
+ border-right: 1px solid #414561;
+ border-top: 1px solid #414561;
+ border-bottom: 1px solid #414561;
+ display: flex;
+ flex: 1 0 auto;
+ justify-content: space-between;
+`;
+
+const RowHeaderLeftNameText = styled.div`
+ align-items: center;
+ display: flex;
+`;
+
+const RowHeaderRight = styled.div<{ left: number }>`
+ left: ${p => p.left}px;
+ right: 0px;
+ height: 37px;
+ position: absolute;
+`;
+
+const RowScrollable = styled.div`
+ min-width: 1px;
+ overflow-x: auto;
+ overflow-y: hidden;
+ width: 100%;
+`;
+
+const RowScrollContent = styled.div`
+ align-items: center;
+ display: inline-flex;
+ height: 37px;
+ width: 100%;
+`;
+
+const RowHeaderRightContainer = styled.div`
+ padding-right: 24px;
+
+ align-items: stretch;
+ display: flex;
+ flex: 1 0 auto;
+ height: 37px;
+ justify-content: flex-end;
+ margin: -1px 0;
+`;
+
+const ItemWrapper = styled.div<{ width: number }>`
+ width: ${p => p.width}px;
+ align-items: center;
+ border: 1px solid #414561;
+ border-bottom: 0;
+ box-sizing: border-box;
+ cursor: pointer;
+ display: inline-flex;
+ flex: 0 0 auto;
+ font-size: 12px;
+ justify-content: space-between;
+ margin-right: -1px;
+ padding: 0 8px;
+ position: relative;
+ color: ${props => props.theme.colors.text.primary};
+ border-bottom: 1px solid #414561;
+ &:hover {
+ background: ${props => props.theme.colors.primary};
+ color: ${props => props.theme.colors.text.secondary};
+ }
+`;
+const ItemsContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ width: 100%;
+ & ${ItemWrapper}:last-child {
+ border-right: 0;
+ }
+`;
+
+const ItemName = styled.div`
+ align-items: center;
+ display: flex;
+ overflow: hidden;
+`;
+type DateEditorState = {
+ open: boolean;
+ pos: { top: number; left: number } | null;
+ task: null | Task;
+};
+
+type ProjectEditorState = {
+ open: boolean;
+ pos: { top: number; left: number } | null;
+ task: null | Task;
+};
+const RIGHT_ROW_WIDTH = 327;
+
+const Projects = () => {
+ const leftRow = window.innerWidth - RIGHT_ROW_WIDTH;
+ const [menuOpen, setMenuOpen] = useState(false);
+ const [filters, setFilters] = useStickyState<{ sort: MyTasksSort; status: MyTasksStatus }>(
+ { sort: MyTasksSort.None, status: MyTasksStatus.All },
+ 'my_tasks_filter',
+ );
+ const { data } = useMyTasksQuery({ variables: { sort: filters.sort, status: filters.status } });
+ const [dateEditor, setDateEditor] = useState({ open: false, pos: null, task: null });
+ const onEditDueDate = (task: Task, $target: React.RefObject) => {
+ if ($target && $target.current && data) {
+ const pos = $target.current.getBoundingClientRect();
+ setDateEditor({
+ open: true,
+ pos: {
+ top: pos.top,
+ left: pos.right,
+ },
+ task,
+ });
+ }
+ };
+ const [newTask, setNewTask] = useState<{ open: boolean }>({ open: false });
+ const match = useRouteMatch();
+ const history = useHistory();
+ const [projectEditor, setProjectEditor] = useState({ open: false, pos: null, task: null });
+ const onEditProject = ($target: React.RefObject) => {
+ if ($target && $target.current) {
+ const pos = $target.current.getBoundingClientRect();
+ setProjectEditor({
+ open: true,
+ pos: {
+ top: pos.top,
+ left: pos.right,
+ },
+ task: null,
+ });
+ }
+ };
+ const { showPopup } = usePopup();
+ const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
+ const $editorContents = useRef(null);
+ const $dateContents = useRef(null);
+ useEffect(() => {
+ if (dateEditor.open && $dateContents.current && dateEditor.task) {
+ showPopup(
+ $dateContents,
+
+ null}
+ onDueDateChange={(task, dueDate, hasTime) => {
+ if (dateEditor.task) {
+ updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate, hasTime } });
+ setDateEditor(prev => ({ ...prev, task: { ...task, dueDate: dueDate.toISOString(), hasTime } }));
+ }
+ }}
+ onRemoveDueDate={task => {
+ if (dateEditor.task) {
+ updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate: null, hasTime: false } });
+ setDateEditor(prev => ({ ...prev, task: { ...task, hasTime: false } }));
+ }
+ }}
+ />
+ ,
+ { onClose: () => setDateEditor({ open: false, task: null, pos: null }) },
+ );
+ }
+ }, [dateEditor]);
+
+ const [createTask] = useCreateTaskMutation({
+ update: (client, newTaskData) => {
+ updateApolloCache(
+ client,
+ MyTasksDocument,
+ cache =>
+ produce(cache, draftCache => {
+ if (newTaskData.data) {
+ draftCache.myTasks.tasks.unshift(newTaskData.data.createTask);
+ }
+ }),
+ { status: MyTasksStatus.All, sort: MyTasksSort.None },
+ );
+ },
+ });
+
+ const [setTaskComplete] = useSetTaskCompleteMutation();
+ const [updateTaskName] = useUpdateTaskNameMutation();
+ const [minified, setMinified] = useStickyState>([], 'my_tasks_minified');
+ useOnOutsideClick(
+ $editorContents,
+ projectEditor.open,
+ () =>
+ setProjectEditor({
+ open: false,
+ task: null,
+ pos: null,
+ }),
+ null,
+ );
+ if (data) {
+ const groups: Array = [];
+ if (filters.sort === MyTasksSort.None) {
+ groups.push({
+ id: 'recently-assigned',
+ name: 'Recently Assigned',
+ tasks: data.myTasks.tasks.map(task => ({
+ ...task,
+ labels: [],
+ position: 0,
+ })),
+ });
+ } else {
+ let { tasks } = data.myTasks;
+ if (filters.sort === MyTasksSort.DueDate) {
+ const group: Group = { id: 'due_date', name: null, tasks: [] };
+ data.myTasks.tasks.forEach(task => {
+ if (task.dueDate) {
+ group.tasks.push({ ...task, labels: [], position: 0 });
+ }
+ });
+ groups.push(group);
+ tasks = tasks.filter(t => t.dueDate === null);
+ }
+ const projects = new Map>();
+ data.myTasks.projects.forEach(p => {
+ if (!projects.has(p.projectID)) {
+ projects.set(p.projectID, []);
+ }
+ const prev = projects.get(p.projectID);
+ const task = tasks.find(t => t.id === p.taskID);
+ if (prev && task) {
+ projects.set(p.projectID, [...prev, { ...task, labels: [], position: 0 }]);
+ }
+ });
+ for (const [id, pTasks] of projects) {
+ const project = data.projects.find(c => c.id === id);
+ if (pTasks.length === 0) continue;
+ if (project) {
+ groups.push({
+ id,
+ name: project.name,
+ tasks: pTasks.sort((a, b) => {
+ if (a.dueDate === null && b.dueDate === null) return 0;
+ if (a.dueDate === null && b.dueDate !== null) return 1;
+ if (a.dueDate !== null && b.dueDate === null) return -1;
+ const first = dayjs(a.dueDate);
+ const second = dayjs(b.dueDate);
+ if (first.isSame(second, 'minute')) return 0;
+ if (first.isAfter(second)) return -1;
+ return 1;
+ }),
+ });
+ }
+ }
+ groups.sort((a, b) => {
+ if (a.name === null && b.name === null) return 0;
+ if (a.name === null) return -1;
+ if (b.name === null) return 1;
+ return a.name.localeCompare(b.name);
+ });
+ }
+ return (
+ <>
+
+
+
+
+
+
+ All Tasks
+
+ {
+ showPopup(
+ $target,
+ setFilters(prev => ({ ...prev, sort }))}
+ />,
+ );
+ }}
+ >
+
+ {prettySort(filters.sort)}
+
+
+
+ Customize
+
+
+
+
+
+
+
+
+ Task name
+
+
+
+
+
+
+
+
+
+ Due date
+
+
+ Project
+
+
+
+
+
+
+
+
+
+
+
+ {groups.map(group => {
+ const isMinified = minified.find(m => m === group.id) ?? false;
+ return (
+
+ {group.name && (
+
+
+ {
+ setMinified(prev => {
+ if (isMinified) {
+ return prev.filter(c => c !== group.id);
+ }
+ return [...prev, group.id];
+ });
+ }}
+ >
+ {isMinified ? (
+
+ ) : (
+
+ )}
+
+ {group.name}
+
+
+ )}
+
+ {!isMinified &&
+ group.tasks.map(task => {
+ const projectID = data.myTasks.projects.find(t => t.taskID === task.id)?.projectID;
+ const projectName = data.projects.find(p => p.id === projectID)?.name;
+ return (
+ {
+ setTaskComplete({ variables: { taskID: task.id, complete } });
+ }}
+ onTaskDetails={() => {
+ history.push(`${match.url}/c/${task.id}`);
+ }}
+ onRemoveDueDate={() => {
+ updateTaskDueDate({ variables: { taskID: task.id, dueDate: null, hasTime: false } });
+ }}
+ project={projectName ?? 'none'}
+ dueDate={task.dueDate}
+ hasTime={task.hasTime ?? false}
+ name={task.name}
+ onEditName={name => updateTaskName({ variables: { taskID: task.id, name } })}
+ onEditProject={onEditProject}
+ onEditDueDate={$target => onEditDueDate({ ...task, position: 0, labels: [] }, $target)}
+ />
+ );
+ })}
+
+
+ );
+ })}
+
+
+
+
+ {dateEditor.open && dateEditor.pos !== null && dateEditor.task && (
+
+
+
+
+
+ {dateEditor.task.dueDate
+ ? dayjs(dateEditor.task.dueDate).format(dateEditor.task.hasTime ? 'MMM D [at] h:mm A' : 'MMM D')
+ : ''}
+
+
+
+
+
+ )}
+ {projectEditor.open && projectEditor.pos !== null && (
+
+
+
+
+
+
+
+
+ )}
+ ) => (
+ {
+ updateTaskName({ variables: { taskID: updatedTask.id, name: newName } });
+ }}
+ onTaskDescriptionChange={(updatedTask, newDescription) => {
+ /*
+ updateTaskDescription({
+ variables: { taskID: updatedTask.id, description: newDescription },
+ optimisticResponse: {
+ __typename: 'Mutation',
+ updateTaskDescription: {
+ __typename: 'Task',
+ id: updatedTask.id,
+ description: newDescription,
+ },
+ },
+ });
+ */
+ }}
+ onDeleteTask={deletedTask => {
+ // deleteTask({ variables: { taskID: deletedTask.id } });
+ history.push(`${match.url}`);
+ }}
+ onOpenAddLabelPopup={(task, $targetRef) => {
+ /*
+ taskLabelsRef.current = task.labels;
+ showPopup(
+ $targetRef,
+ {
+ toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
+ }}
+ labelColors={data.labelColors}
+ labels={labelsRef}
+ taskLabels={taskLabelsRef}
+ projectID={projectID}
+ />,
+ );
+ */
+ }}
+ />
+ )}
+ />
+ >
+ );
+ }
+ return null;
+};
+
+export default Projects;
diff --git a/frontend/src/Projects/Project/Board/index.tsx b/frontend/src/Projects/Project/Board/index.tsx
index 80bc478..1ef8217 100644
--- a/frontend/src/Projects/Project/Board/index.tsx
+++ b/frontend/src/Projects/Project/Board/index.tsx
@@ -426,6 +426,7 @@ const ProjectBoard: React.FC = ({ projectID, onCardLabelClick
name,
complete: false,
completedAt: null,
+ hasTime: false,
taskGroup: {
__typename: 'TaskGroup',
id: taskGroup.id,
diff --git a/frontend/src/shared/components/PopupMenu/index.tsx b/frontend/src/shared/components/PopupMenu/index.tsx
index 48d4254..ce07fbc 100644
--- a/frontend/src/shared/components/PopupMenu/index.tsx
+++ b/frontend/src/shared/components/PopupMenu/index.tsx
@@ -17,7 +17,7 @@ import {
} from './Styles';
function getPopupOptions(options?: PopupOptions) {
- const popupOptions = {
+ const popupOptions: PopupOptionsInternal = {
borders: true,
diamondColor: theme.colors.bg.secondary,
targetPadding: '10px',
@@ -40,6 +40,9 @@ function getPopupOptions(options?: PopupOptions) {
if (options.diamondColor) {
popupOptions.diamondColor = options.diamondColor;
}
+ if (options.onClose) {
+ popupOptions.onClose = options.onClose;
+ }
}
return popupOptions;
}
@@ -136,6 +139,7 @@ type PopupOptionsInternal = {
targetPadding: string;
diamondColor: string;
showDiamond: boolean;
+ onClose?: () => void;
};
type PopupOptions = {
@@ -144,6 +148,7 @@ type PopupOptions = {
width?: number | null;
borders?: boolean | null;
diamondColor?: string | null;
+ onClose?: () => void;
};
const defaultState = {
isOpen: false,
@@ -239,7 +244,12 @@ export const PopupProvider: React.FC = ({ children }) => {
top={currentState.top}
targetPadding={currentState.options.targetPadding}
left={currentState.left}
- onClose={() => setState(defaultState)}
+ onClose={() => {
+ if (currentState.options && currentState.options.onClose) {
+ currentState.options.onClose();
+ }
+ setState(defaultState);
+ }}
width={currentState.options.width}
>
{currentState.content}
diff --git a/frontend/src/shared/components/Select/index.tsx b/frontend/src/shared/components/Select/index.tsx
index cbadef4..e0acedb 100644
--- a/frontend/src/shared/components/Select/index.tsx
+++ b/frontend/src/shared/components/Select/index.tsx
@@ -84,6 +84,58 @@ export const colourStyles = {
},
};
+export const editorColourStyles = {
+ ...colourStyles,
+ input: (styles: any) => ({
+ ...styles,
+ color: '#000',
+ }),
+ singleValue: (styles: any) => {
+ return {
+ ...styles,
+ color: '#000',
+ };
+ },
+ menu: (styles: any) => {
+ return {
+ ...styles,
+ backgroundColor: '#fff',
+ };
+ },
+ indicatorsContainer: (styles: any) => {
+ return {
+ ...styles,
+ display: 'none',
+ };
+ },
+ container: (styles: any) => {
+ return {
+ ...styles,
+ display: 'flex',
+ flex: '1 1',
+ };
+ },
+ control: (styles: any, data: any) => {
+ return {
+ ...styles,
+ flex: '1 1',
+ backgroundColor: 'transparent',
+ boxShadow: 'none',
+ borderRadius: '0',
+ minHeight: '35px',
+ border: '0',
+ ':hover': {
+ boxShadow: 'none',
+ borderRadius: '0',
+ },
+ ':active': {
+ boxShadow: 'none',
+ borderRadius: '0',
+ },
+ };
+ },
+};
+
const InputLabel = styled.span<{ width: string }>`
width: ${props => props.width};
padding-left: 0.7rem;
diff --git a/frontend/src/shared/components/TopNavbar/TopNavbar.stories.tsx b/frontend/src/shared/components/TopNavbar/TopNavbar.stories.tsx
index 10a73fa..89b9356 100644
--- a/frontend/src/shared/components/TopNavbar/TopNavbar.stories.tsx
+++ b/frontend/src/shared/components/TopNavbar/TopNavbar.stories.tsx
@@ -43,6 +43,7 @@ export const Default = () => {
onDashboardClick={action('open dashboard')}
onRemoveFromBoard={action('remove project')}
onProfileClick={action('profile click')}
+ onMyTasksClick={action('profile click')}
/>
>
);
diff --git a/frontend/src/shared/components/TopNavbar/index.tsx b/frontend/src/shared/components/TopNavbar/index.tsx
index 69cdded..e711837 100644
--- a/frontend/src/shared/components/TopNavbar/index.tsx
+++ b/frontend/src/shared/components/TopNavbar/index.tsx
@@ -179,6 +179,7 @@ type NavBarProps = {
onRemoveFromBoard?: (userID: string) => void;
onMemberProfile?: ($targetRef: React.RefObject, memberID: string) => void;
onInvitedMemberProfile?: ($targetRef: React.RefObject, email: string) => void;
+ onMyTasksClick: () => void;
};
const NavBar: React.FC = ({
@@ -201,6 +202,7 @@ const NavBar: React.FC = ({
onProfileClick,
onNotificationClick,
onDashboardClick,
+ onMyTasksClick,
user,
projectMembers,
onOpenSettings,
@@ -306,7 +308,7 @@ const NavBar: React.FC = ({
onDashboardClick()}>
-
+ onMyTasksClick()}>
diff --git a/frontend/src/shared/generated/graphql.tsx b/frontend/src/shared/generated/graphql.tsx
index 87182ef..2a2ec75 100644
--- a/frontend/src/shared/generated/graphql.tsx
+++ b/frontend/src/shared/generated/graphql.tsx
@@ -302,6 +302,7 @@ export type Query = {
invitedUsers: Array;
labelColors: Array;
me: MePayload;
+ myTasks: MyTasksPayload;
notifications: Array;
organizations: Array;
projects: Array;
@@ -332,6 +333,11 @@ export type QueryFindUserArgs = {
};
+export type QueryMyTasksArgs = {
+ input: MyTasks;
+};
+
+
export type QueryProjectsArgs = {
input?: Maybe;
};
@@ -682,6 +688,40 @@ export type MutationUpdateUserRoleArgs = {
input: UpdateUserRole;
};
+export enum MyTasksStatus {
+ All = 'ALL',
+ Incomplete = 'INCOMPLETE',
+ CompleteAll = 'COMPLETE_ALL',
+ CompleteToday = 'COMPLETE_TODAY',
+ CompleteYesterday = 'COMPLETE_YESTERDAY',
+ CompleteOneWeek = 'COMPLETE_ONE_WEEK',
+ CompleteTwoWeek = 'COMPLETE_TWO_WEEK',
+ CompleteThreeWeek = 'COMPLETE_THREE_WEEK'
+}
+
+export enum MyTasksSort {
+ None = 'NONE',
+ Project = 'PROJECT',
+ DueDate = 'DUE_DATE'
+}
+
+export type MyTasks = {
+ status: MyTasksStatus;
+ sort: MyTasksSort;
+};
+
+export type ProjectTaskMapping = {
+ __typename?: 'ProjectTaskMapping';
+ projectID: Scalars['UUID'];
+ taskID: Scalars['UUID'];
+};
+
+export type MyTasksPayload = {
+ __typename?: 'MyTasksPayload';
+ tasks: Array;
+ projects: Array;
+};
+
export type TeamRole = {
__typename?: 'TeamRole';
teamID: Scalars['UUID'];
@@ -859,6 +899,7 @@ export type NewTask = {
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
+ assigned?: Maybe>;
};
export type AssignTaskInput = {
@@ -1529,7 +1570,7 @@ export type FindTaskQuery = (
export type TaskFieldsFragment = (
{ __typename?: 'Task' }
- & Pick
+ & Pick
& { badges: (
{ __typename?: 'TaskBadges' }
& { checklist?: Maybe<(
@@ -1605,6 +1646,33 @@ export type MeQuery = (
) }
);
+export type MyTasksQueryVariables = Exact<{
+ status: MyTasksStatus;
+ sort: MyTasksSort;
+}>;
+
+
+export type MyTasksQuery = (
+ { __typename?: 'Query' }
+ & { projects: Array<(
+ { __typename?: 'Project' }
+ & Pick
+ )>, myTasks: (
+ { __typename?: 'MyTasksPayload' }
+ & { tasks: Array<(
+ { __typename?: 'Task' }
+ & Pick
+ & { taskGroup: (
+ { __typename?: 'TaskGroup' }
+ & Pick
+ ) }
+ )>, projects: Array<(
+ { __typename?: 'ProjectTaskMapping' }
+ & Pick
+ )> }
+ ) }
+);
+
export type DeleteProjectMutationVariables = Exact<{
projectID: Scalars['UUID'];
}>;
@@ -1712,6 +1780,7 @@ export type CreateTaskMutationVariables = Exact<{
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
+ assigned?: Maybe>;
}>;
@@ -2559,6 +2628,7 @@ export const TaskFieldsFragmentDoc = gql`
name
description
dueDate
+ hasTime
complete
completedAt
position
@@ -3238,6 +3308,59 @@ export function useMeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptio
export type MeQueryHookResult = ReturnType;
export type MeLazyQueryHookResult = ReturnType;
export type MeQueryResult = ApolloReactCommon.QueryResult;
+export const MyTasksDocument = gql`
+ query myTasks($status: MyTasksStatus!, $sort: MyTasksSort!) {
+ projects {
+ id
+ name
+ }
+ myTasks(input: {status: $status, sort: $sort}) {
+ tasks {
+ id
+ taskGroup {
+ id
+ name
+ }
+ name
+ dueDate
+ hasTime
+ complete
+ completedAt
+ }
+ projects {
+ projectID
+ taskID
+ }
+ }
+}
+ `;
+
+/**
+ * __useMyTasksQuery__
+ *
+ * To run a query within a React component, call `useMyTasksQuery` and pass it any options that fit your needs.
+ * When your component renders, `useMyTasksQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useMyTasksQuery({
+ * variables: {
+ * status: // value for 'status'
+ * sort: // value for 'sort'
+ * },
+ * });
+ */
+export function useMyTasksQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) {
+ return ApolloReactHooks.useQuery(MyTasksDocument, baseOptions);
+ }
+export function useMyTasksLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) {
+ return ApolloReactHooks.useLazyQuery(MyTasksDocument, baseOptions);
+ }
+export type MyTasksQueryHookResult = ReturnType;
+export type MyTasksLazyQueryHookResult = ReturnType;
+export type MyTasksQueryResult = ApolloReactCommon.QueryResult;
export const DeleteProjectDocument = gql`
mutation deleteProject($projectID: UUID!) {
deleteProject(input: {projectID: $projectID}) {
@@ -3440,8 +3563,10 @@ export type UpdateProjectMemberRoleMutationHookResult = ReturnType;
export type UpdateProjectMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions;
export const CreateTaskDocument = gql`
- mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!) {
- createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) {
+ mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!, $assigned: [UUID!]) {
+ createTask(
+ input: {taskGroupID: $taskGroupID, name: $name, position: $position, assigned: $assigned}
+ ) {
...TaskFields
}
}
@@ -3464,6 +3589,7 @@ export type CreateTaskMutationFn = ApolloReactCommon.MutationFunction;
export type UsersLazyQueryHookResult = ReturnType;
-export type UsersQueryResult = ApolloReactCommon.QueryResult;
\ No newline at end of file
+export type UsersQueryResult = ApolloReactCommon.QueryResult;
diff --git a/frontend/src/shared/graphql/fragments/task.ts b/frontend/src/shared/graphql/fragments/task.ts
index 85f5b3f..d72aa36 100644
--- a/frontend/src/shared/graphql/fragments/task.ts
+++ b/frontend/src/shared/graphql/fragments/task.ts
@@ -6,6 +6,7 @@ const TASK_FRAGMENT = gql`
name
description
dueDate
+ hasTime
complete
completedAt
position
diff --git a/frontend/src/shared/graphql/myTasks.graphqls b/frontend/src/shared/graphql/myTasks.graphqls
new file mode 100644
index 0000000..dbba950
--- /dev/null
+++ b/frontend/src/shared/graphql/myTasks.graphqls
@@ -0,0 +1,24 @@
+query myTasks($status: MyTasksStatus!, $sort: MyTasksSort!) {
+ projects {
+ id
+ name
+ }
+ myTasks(input: { status: $status, sort: $sort }) {
+ tasks {
+ id
+ taskGroup {
+ id
+ name
+ }
+ name
+ dueDate
+ hasTime
+ complete
+ completedAt
+ }
+ projects {
+ projectID
+ taskID
+ }
+ }
+}
diff --git a/frontend/src/shared/graphql/task/createTask.ts b/frontend/src/shared/graphql/task/createTask.ts
index 13dd654..d5bbbca 100644
--- a/frontend/src/shared/graphql/task/createTask.ts
+++ b/frontend/src/shared/graphql/task/createTask.ts
@@ -2,8 +2,8 @@ import gql from 'graphql-tag';
import TASK_FRAGMENT from '../fragments/task';
const CREATE_TASK_MUTATION = gql`
- mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!) {
- createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) {
+ mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!, $assigned: [UUID!]) {
+ createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position, assigned: $assigned }) {
...TaskFields
}
}
diff --git a/frontend/src/shared/hooks/useStickyState.ts b/frontend/src/shared/hooks/useStickyState.ts
new file mode 100644
index 0000000..c13e15c
--- /dev/null
+++ b/frontend/src/shared/hooks/useStickyState.ts
@@ -0,0 +1,14 @@
+import React from 'react';
+
+function useStickyState(defaultValue: any, key: string): [T, React.Dispatch>] {
+ const [value, setValue] = React.useState(() => {
+ const stickyValue = window.localStorage.getItem(key);
+ return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue;
+ });
+ React.useEffect(() => {
+ window.localStorage.setItem(key, JSON.stringify(value));
+ }, [key, value]);
+ return [value, setValue];
+}
+
+export default useStickyState;
diff --git a/frontend/src/shared/icons/Briefcase.tsx b/frontend/src/shared/icons/Briefcase.tsx
new file mode 100644
index 0000000..4423a9e
--- /dev/null
+++ b/frontend/src/shared/icons/Briefcase.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import Icon, { IconProps } from './Icon';
+
+const Briefcase: React.FC = ({ width = '16px', height = '16px', className }) => {
+ return (
+
+
+
+ );
+};
+
+export default Briefcase;
diff --git a/frontend/src/shared/icons/CheckCircleOutline.tsx b/frontend/src/shared/icons/CheckCircleOutline.tsx
new file mode 100644
index 0000000..dbab2b0
--- /dev/null
+++ b/frontend/src/shared/icons/CheckCircleOutline.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import Icon, { IconProps } from './Icon';
+
+const CheckCircleOutline: React.FC = ({ width = '16px', height = '16px', className }) => {
+ return (
+
+
+
+ );
+};
+
+export default CheckCircleOutline;
diff --git a/frontend/src/shared/icons/ChevronRight.tsx b/frontend/src/shared/icons/ChevronRight.tsx
new file mode 100644
index 0000000..356430a
--- /dev/null
+++ b/frontend/src/shared/icons/ChevronRight.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import Icon, { IconProps } from './Icon';
+
+const ChevronRight: React.FC = ({ width = '16px', height = '16px', className }) => {
+ return (
+
+
+
+ );
+};
+
+export default ChevronRight;
diff --git a/frontend/src/shared/icons/Cogs.tsx b/frontend/src/shared/icons/Cogs.tsx
new file mode 100644
index 0000000..eb89e72
--- /dev/null
+++ b/frontend/src/shared/icons/Cogs.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import Icon, { IconProps } from './Icon';
+
+const Cogs: React.FC = ({ width = '16px', height = '16px', className }) => {
+ return (
+
+
+
+ );
+};
+
+export default Cogs;
diff --git a/frontend/src/shared/icons/index.ts b/frontend/src/shared/icons/index.ts
index 9ecad17..1644bb4 100644
--- a/frontend/src/shared/icons/index.ts
+++ b/frontend/src/shared/icons/index.ts
@@ -1,7 +1,11 @@
import Cross from './Cross';
import Cog from './Cog';
+import Cogs from './Cogs';
import ArrowDown from './ArrowDown';
+import CheckCircleOutline from './CheckCircleOutline';
+import Briefcase from './Briefcase';
import ListUnordered from './ListUnordered';
+import ChevronRight from './ChevronRight';
import Dot from './Dot';
import CaretDown from './CaretDown';
import Eye from './Eye';
@@ -102,5 +106,9 @@ export {
Dot,
ArrowDown,
CaretRight,
+ CheckCircleOutline,
+ Briefcase,
DotCircle,
+ ChevronRight,
+ Cogs,
};
diff --git a/go.sum b/go.sum
index d0a463b..5cbf910 100644
--- a/go.sum
+++ b/go.sum
@@ -381,8 +381,6 @@ github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lithammer/fuzzysearch v1.1.0 h1:go9v8tLCrNTTlH42OAaq4eHFe81TDHEnlrMEb6R4f+A=
github.com/lithammer/fuzzysearch v1.1.0/go.mod h1:Bqx4wo8lTOFcJr3ckpY6HA9lEIOO0H5HrkJ5CsN56HQ=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
-github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
-github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
diff --git a/internal/db/querier.go b/internal/db/querier.go
index 80916bc..2465ebd 100644
--- a/internal/db/querier.go
+++ b/internal/db/querier.go
@@ -69,6 +69,8 @@ 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)
+ GetAssignedTasksDueDateForUserID(ctx context.Context, userID uuid.UUID) ([]Task, error)
+ GetAssignedTasksProjectForUserID(ctx context.Context, userID uuid.UUID) ([]Task, 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)
@@ -90,12 +92,14 @@ type Querier interface {
GetProjectIDForTaskChecklist(ctx context.Context, taskChecklistID uuid.UUID) (uuid.UUID, error)
GetProjectIDForTaskChecklistItem(ctx context.Context, taskChecklistItemID uuid.UUID) (uuid.UUID, error)
GetProjectIDForTaskGroup(ctx context.Context, taskGroupID uuid.UUID) (uuid.UUID, error)
+ GetProjectIdMappings(ctx context.Context, dollar_1 []uuid.UUID) ([]GetProjectIdMappingsRow, error)
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
GetProjectMemberInvitedIDByEmail(ctx context.Context, email string) (GetProjectMemberInvitedIDByEmailRow, error)
GetProjectMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectMember, error)
GetProjectRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetProjectRolesForUserIDRow, error)
GetProjectsForInvitedMember(ctx context.Context, email string) ([]uuid.UUID, error)
+ GetRecentlyAssignedTaskForUserID(ctx context.Context, userID uuid.UUID) ([]Task, error)
GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
GetRoleForProjectMemberByUserID(ctx context.Context, arg GetRoleForProjectMemberByUserIDParams) (Role, error)
GetRoleForTeamMember(ctx context.Context, arg GetRoleForTeamMemberParams) (Role, error)
diff --git a/internal/db/query/task.sql b/internal/db/query/task.sql
index a944a12..59aad2d 100644
--- a/internal/db/query/task.sql
+++ b/internal/db/query/task.sql
@@ -56,3 +56,26 @@ 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 *;
+
+-- name: GetRecentlyAssignedTaskForUserID :many
+SELECT task.* FROM task_assigned INNER JOIN
+ task ON task.task_id = task_assigned.task_id WHERE user_id = $1 ORDER BY task_assigned.assigned_date DESC;
+
+-- name: GetAssignedTasksProjectForUserID :many
+SELECT task.* FROM task_assigned
+ INNER JOIN task ON task.task_id = task_assigned.task_id
+ INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
+ WHERE user_id = $1
+ ORDER BY task_group.project_id DESC, task_assigned.assigned_date DESC;
+
+-- name: GetProjectIdMappings :many
+SELECT project_id, task_id FROM task
+INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
+ WHERE task_id = ANY($1::uuid[]);
+
+-- name: GetAssignedTasksDueDateForUserID :many
+SELECT task.* FROM task_assigned
+ INNER JOIN task ON task.task_id = task_assigned.task_id
+ INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
+ WHERE user_id = $1
+ ORDER BY task.due_date DESC, task_group.project_id DESC;
diff --git a/internal/db/task.sql.go b/internal/db/task.sql.go
index b74f7b2..dc4cc20 100644
--- a/internal/db/task.sql.go
+++ b/internal/db/task.sql.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/google/uuid"
+ "github.com/lib/pq"
)
const createTask = `-- name: CreateTask :one
@@ -197,6 +198,90 @@ func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
return items, nil
}
+const getAssignedTasksDueDateForUserID = `-- name: GetAssignedTasksDueDateForUserID :many
+SELECT task.task_id, task.task_group_id, task.created_at, task.name, task.position, task.description, task.due_date, task.complete, task.completed_at, task.has_time FROM task_assigned
+ INNER JOIN task ON task.task_id = task_assigned.task_id
+ INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
+ WHERE user_id = $1
+ ORDER BY task.due_date DESC, task_group.project_id DESC
+`
+
+func (q *Queries) GetAssignedTasksDueDateForUserID(ctx context.Context, userID uuid.UUID) ([]Task, error) {
+ rows, err := q.db.QueryContext(ctx, getAssignedTasksDueDateForUserID, userID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Task
+ for rows.Next() {
+ var i Task
+ if err := rows.Scan(
+ &i.TaskID,
+ &i.TaskGroupID,
+ &i.CreatedAt,
+ &i.Name,
+ &i.Position,
+ &i.Description,
+ &i.DueDate,
+ &i.Complete,
+ &i.CompletedAt,
+ &i.HasTime,
+ ); 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 getAssignedTasksProjectForUserID = `-- name: GetAssignedTasksProjectForUserID :many
+SELECT task.task_id, task.task_group_id, task.created_at, task.name, task.position, task.description, task.due_date, task.complete, task.completed_at, task.has_time FROM task_assigned
+ INNER JOIN task ON task.task_id = task_assigned.task_id
+ INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
+ WHERE user_id = $1
+ ORDER BY task_group.project_id DESC, task_assigned.assigned_date DESC
+`
+
+func (q *Queries) GetAssignedTasksProjectForUserID(ctx context.Context, userID uuid.UUID) ([]Task, error) {
+ rows, err := q.db.QueryContext(ctx, getAssignedTasksProjectForUserID, userID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Task
+ for rows.Next() {
+ var i Task
+ if err := rows.Scan(
+ &i.TaskID,
+ &i.TaskGroupID,
+ &i.CreatedAt,
+ &i.Name,
+ &i.Position,
+ &i.Description,
+ &i.DueDate,
+ &i.Complete,
+ &i.CompletedAt,
+ &i.HasTime,
+ ); 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 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
`
@@ -245,6 +330,79 @@ func (q *Queries) GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uu
return project_id, err
}
+const getProjectIdMappings = `-- name: GetProjectIdMappings :many
+SELECT project_id, task_id FROM task
+INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
+ WHERE task_id = ANY($1::uuid[])
+`
+
+type GetProjectIdMappingsRow struct {
+ ProjectID uuid.UUID `json:"project_id"`
+ TaskID uuid.UUID `json:"task_id"`
+}
+
+func (q *Queries) GetProjectIdMappings(ctx context.Context, dollar_1 []uuid.UUID) ([]GetProjectIdMappingsRow, error) {
+ rows, err := q.db.QueryContext(ctx, getProjectIdMappings, pq.Array(dollar_1))
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []GetProjectIdMappingsRow
+ for rows.Next() {
+ var i GetProjectIdMappingsRow
+ if err := rows.Scan(&i.ProjectID, &i.TaskID); 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 getRecentlyAssignedTaskForUserID = `-- name: GetRecentlyAssignedTaskForUserID :many
+SELECT task.task_id, task.task_group_id, task.created_at, task.name, task.position, task.description, task.due_date, task.complete, task.completed_at, task.has_time FROM task_assigned INNER JOIN
+ task ON task.task_id = task_assigned.task_id WHERE user_id = $1 ORDER BY task_assigned.assigned_date DESC
+`
+
+func (q *Queries) GetRecentlyAssignedTaskForUserID(ctx context.Context, userID uuid.UUID) ([]Task, error) {
+ rows, err := q.db.QueryContext(ctx, getRecentlyAssignedTaskForUserID, userID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Task
+ for rows.Next() {
+ var i Task
+ if err := rows.Scan(
+ &i.TaskID,
+ &i.TaskGroupID,
+ &i.CreatedAt,
+ &i.Name,
+ &i.Position,
+ &i.Description,
+ &i.DueDate,
+ &i.Complete,
+ &i.CompletedAt,
+ &i.HasTime,
+ ); 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 getTaskByID = `-- name: GetTaskByID :one
SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time FROM task WHERE task_id = $1
`
@@ -334,7 +492,7 @@ func (q *Queries) SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams
}
const updateTaskComment = `-- name: UpdateTaskComment :one
-UPDATE task_comment SET message = $2, updated_at = COALESCE($3, updated_at) WHERE task_comment_id = $1 RETURNING task_comment_id, task_id, created_at, updated_at, created_by, pinned, message
+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 {
diff --git a/internal/graph/generated.go b/internal/graph/generated.go
index dbfed7e..28bc28d 100644
--- a/internal/graph/generated.go
+++ b/internal/graph/generated.go
@@ -273,6 +273,11 @@ type ComplexityRoot struct {
UpdateUserRole func(childComplexity int, input UpdateUserRole) int
}
+ MyTasksPayload struct {
+ Projects func(childComplexity int) int
+ Tasks func(childComplexity int) int
+ }
+
Notification struct {
ActionType func(childComplexity int) int
Actor func(childComplexity int) int
@@ -338,6 +343,11 @@ type ComplexityRoot struct {
RoleCode func(childComplexity int) int
}
+ ProjectTaskMapping struct {
+ ProjectID func(childComplexity int) int
+ TaskID func(childComplexity int) int
+ }
+
Query struct {
FindProject func(childComplexity int, input FindProject) int
FindTask func(childComplexity int, input FindTask) int
@@ -346,6 +356,7 @@ type ComplexityRoot struct {
InvitedUsers func(childComplexity int) int
LabelColors func(childComplexity int) int
Me func(childComplexity int) int
+ MyTasks func(childComplexity int, input MyTasks) int
Notifications func(childComplexity int) int
Organizations func(childComplexity int) int
Projects func(childComplexity int, input *ProjectsFilter) int
@@ -622,6 +633,7 @@ type QueryResolver interface {
Projects(ctx context.Context, input *ProjectsFilter) ([]db.Project, error)
FindTeam(ctx context.Context, input FindTeam) (*db.Team, error)
Teams(ctx context.Context) ([]db.Team, error)
+ MyTasks(ctx context.Context, input MyTasks) (*MyTasksPayload, error)
LabelColors(ctx context.Context) ([]db.LabelColor, error)
TaskGroups(ctx context.Context) ([]db.TaskGroup, error)
Me(ctx context.Context) (*MePayload, error)
@@ -1878,6 +1890,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.UpdateUserRole(childComplexity, args["input"].(UpdateUserRole)), true
+ case "MyTasksPayload.projects":
+ if e.complexity.MyTasksPayload.Projects == nil {
+ break
+ }
+
+ return e.complexity.MyTasksPayload.Projects(childComplexity), true
+
+ case "MyTasksPayload.tasks":
+ if e.complexity.MyTasksPayload.Tasks == nil {
+ break
+ }
+
+ return e.complexity.MyTasksPayload.Tasks(childComplexity), true
+
case "Notification.actionType":
if e.complexity.Notification.ActionType == nil {
break
@@ -2123,6 +2149,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ProjectRole.RoleCode(childComplexity), true
+ case "ProjectTaskMapping.projectID":
+ if e.complexity.ProjectTaskMapping.ProjectID == nil {
+ break
+ }
+
+ return e.complexity.ProjectTaskMapping.ProjectID(childComplexity), true
+
+ case "ProjectTaskMapping.taskID":
+ if e.complexity.ProjectTaskMapping.TaskID == nil {
+ break
+ }
+
+ return e.complexity.ProjectTaskMapping.TaskID(childComplexity), true
+
case "Query.findProject":
if e.complexity.Query.FindProject == nil {
break
@@ -2192,6 +2232,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.Me(childComplexity), true
+ case "Query.myTasks":
+ if e.complexity.Query.MyTasks == nil {
+ break
+ }
+
+ args, err := ec.field_Query_myTasks_args(context.TODO(), rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Query.MyTasks(childComplexity, args["input"].(MyTasks)), true
+
case "Query.notifications":
if e.complexity.Query.Notifications == nil {
break
@@ -3229,13 +3281,47 @@ type Query {
projects(input: ProjectsFilter): [Project!]!
findTeam(input: FindTeam!): Team!
teams: [Team!]!
+ myTasks(input: MyTasks!): MyTasksPayload!
labelColors: [LabelColor!]!
taskGroups: [TaskGroup!]!
me: MePayload!
}
+
type Mutation
+enum MyTasksStatus {
+ ALL
+ INCOMPLETE
+ COMPLETE_ALL
+ COMPLETE_TODAY
+ COMPLETE_YESTERDAY
+ COMPLETE_ONE_WEEK
+ COMPLETE_TWO_WEEK
+ COMPLETE_THREE_WEEK
+}
+
+enum MyTasksSort {
+ NONE
+ PROJECT
+ DUE_DATE
+}
+
+input MyTasks {
+ status: MyTasksStatus!
+ sort: MyTasksSort!
+}
+
+type ProjectTaskMapping {
+ projectID: UUID!
+ taskID: UUID!
+}
+
+type MyTasksPayload {
+ tasks: [Task!]!
+ projects: [ProjectTaskMapping!]!
+}
+
type TeamRole {
teamID: UUID!
roleCode: RoleCode!
@@ -3462,6 +3548,7 @@ input NewTask {
taskGroupID: UUID!
name: String!
position: Float!
+ assigned: [UUID!]
}
input AssignTaskInput {
@@ -4806,6 +4893,20 @@ func (ec *executionContext) field_Query_findUser_args(ctx context.Context, rawAr
return args, nil
}
+func (ec *executionContext) field_Query_myTasks_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+ var err error
+ args := map[string]interface{}{}
+ var arg0 MyTasks
+ if tmp, ok := rawArgs["input"]; ok {
+ arg0, err = ec.unmarshalNMyTasks2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMyTasks(ctx, tmp)
+ if err != nil {
+ return nil, err
+ }
+ }
+ args["input"] = arg0
+ return args, nil
+}
+
func (ec *executionContext) field_Query_projects_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@@ -11270,6 +11371,74 @@ func (ec *executionContext) _Mutation_updateUserInfo(ctx context.Context, field
return ec.marshalNUpdateUserInfoPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserInfoPayload(ctx, field.Selections, res)
}
+func (ec *executionContext) _MyTasksPayload_tasks(ctx context.Context, field graphql.CollectedField, obj *MyTasksPayload) (ret graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ fc := &graphql.FieldContext{
+ Object: "MyTasksPayload",
+ Field: field,
+ Args: nil,
+ IsMethod: false,
+ }
+
+ ctx = graphql.WithFieldContext(ctx, fc)
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Tasks, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.([]db.Task)
+ fc.Result = res
+ return ec.marshalNTask2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTaskᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) _MyTasksPayload_projects(ctx context.Context, field graphql.CollectedField, obj *MyTasksPayload) (ret graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ fc := &graphql.FieldContext{
+ Object: "MyTasksPayload",
+ Field: field,
+ Args: nil,
+ IsMethod: false,
+ }
+
+ ctx = graphql.WithFieldContext(ctx, fc)
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Projects, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.([]ProjectTaskMapping)
+ fc.Result = res
+ return ec.marshalNProjectTaskMapping2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProjectTaskMappingᚄ(ctx, field.Selections, res)
+}
+
func (ec *executionContext) _Notification_id(ctx context.Context, field graphql.CollectedField, obj *db.Notification) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -12445,6 +12614,74 @@ func (ec *executionContext) _ProjectRole_roleCode(ctx context.Context, field gra
return ec.marshalNRoleCode2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleCode(ctx, field.Selections, res)
}
+func (ec *executionContext) _ProjectTaskMapping_projectID(ctx context.Context, field graphql.CollectedField, obj *ProjectTaskMapping) (ret graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ fc := &graphql.FieldContext{
+ Object: "ProjectTaskMapping",
+ Field: field,
+ Args: nil,
+ IsMethod: false,
+ }
+
+ ctx = graphql.WithFieldContext(ctx, fc)
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.ProjectID, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(uuid.UUID)
+ fc.Result = res
+ return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) _ProjectTaskMapping_taskID(ctx context.Context, field graphql.CollectedField, obj *ProjectTaskMapping) (ret graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ fc := &graphql.FieldContext{
+ Object: "ProjectTaskMapping",
+ Field: field,
+ Args: nil,
+ IsMethod: false,
+ }
+
+ ctx = graphql.WithFieldContext(ctx, fc)
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.TaskID, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(uuid.UUID)
+ fc.Result = res
+ return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res)
+}
+
func (ec *executionContext) _Query_organizations(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -12818,6 +13055,47 @@ func (ec *executionContext) _Query_teams(ctx context.Context, field graphql.Coll
return ec.marshalNTeam2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeamᚄ(ctx, field.Selections, res)
}
+func (ec *executionContext) _Query_myTasks(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ fc := &graphql.FieldContext{
+ Object: "Query",
+ Field: field,
+ Args: nil,
+ IsMethod: true,
+ }
+
+ ctx = graphql.WithFieldContext(ctx, fc)
+ rawArgs := field.ArgumentMap(ec.Variables)
+ args, err := ec.field_Query_myTasks_args(ctx, rawArgs)
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ fc.Args = args
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Query().MyTasks(rctx, args["input"].(MyTasks))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*MyTasksPayload)
+ fc.Result = res
+ return ec.marshalNMyTasksPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMyTasksPayload(ctx, field.Selections, res)
+}
+
func (ec *executionContext) _Query_labelColors(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -17902,6 +18180,30 @@ func (ec *executionContext) unmarshalInputMemberSearchFilter(ctx context.Context
return it, nil
}
+func (ec *executionContext) unmarshalInputMyTasks(ctx context.Context, obj interface{}) (MyTasks, error) {
+ var it MyTasks
+ var asMap = obj.(map[string]interface{})
+
+ for k, v := range asMap {
+ switch k {
+ case "status":
+ var err error
+ it.Status, err = ec.unmarshalNMyTasksStatus2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMyTasksStatus(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ case "sort":
+ var err error
+ it.Sort, err = ec.unmarshalNMyTasksSort2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMyTasksSort(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ }
+ }
+
+ return it, nil
+}
+
func (ec *executionContext) unmarshalInputNewProject(ctx context.Context, obj interface{}) (NewProject, error) {
var it NewProject
var asMap = obj.(map[string]interface{})
@@ -17998,6 +18300,12 @@ func (ec *executionContext) unmarshalInputNewTask(ctx context.Context, obj inter
if err != nil {
return it, err
}
+ case "assigned":
+ var err error
+ it.Assigned, err = ec.unmarshalOUUID2ᚕgithubᚗcomᚋgoogleᚋuuidᚐUUIDᚄ(ctx, v)
+ if err != nil {
+ return it, err
+ }
}
}
@@ -20086,6 +20394,38 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
return out
}
+var myTasksPayloadImplementors = []string{"MyTasksPayload"}
+
+func (ec *executionContext) _MyTasksPayload(ctx context.Context, sel ast.SelectionSet, obj *MyTasksPayload) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, myTasksPayloadImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ var invalids uint32
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("MyTasksPayload")
+ case "tasks":
+ out.Values[i] = ec._MyTasksPayload_tasks(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ invalids++
+ }
+ case "projects":
+ out.Values[i] = ec._MyTasksPayload_projects(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch()
+ if invalids > 0 {
+ return graphql.Null
+ }
+ return out
+}
+
var notificationImplementors = []string{"Notification"}
func (ec *executionContext) _Notification(ctx context.Context, sel ast.SelectionSet, obj *db.Notification) graphql.Marshaler {
@@ -20601,6 +20941,38 @@ func (ec *executionContext) _ProjectRole(ctx context.Context, sel ast.SelectionS
return out
}
+var projectTaskMappingImplementors = []string{"ProjectTaskMapping"}
+
+func (ec *executionContext) _ProjectTaskMapping(ctx context.Context, sel ast.SelectionSet, obj *ProjectTaskMapping) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, projectTaskMappingImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ var invalids uint32
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("ProjectTaskMapping")
+ case "projectID":
+ out.Values[i] = ec._ProjectTaskMapping_projectID(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ invalids++
+ }
+ case "taskID":
+ out.Values[i] = ec._ProjectTaskMapping_taskID(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch()
+ if invalids > 0 {
+ return graphql.Null
+ }
+ return out
+}
+
var queryImplementors = []string{"Query"}
func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@@ -20742,6 +21114,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
}
return res
})
+ case "myTasks":
+ field := field
+ out.Concurrently(i, func() (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._Query_myTasks(ctx, field)
+ if res == graphql.Null {
+ atomic.AddUint32(&invalids, 1)
+ }
+ return res
+ })
case "labelColors":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
@@ -23147,6 +23533,42 @@ func (ec *executionContext) marshalNMemberSearchResult2ᚕgithubᚗcomᚋjordank
return ret
}
+func (ec *executionContext) unmarshalNMyTasks2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMyTasks(ctx context.Context, v interface{}) (MyTasks, error) {
+ return ec.unmarshalInputMyTasks(ctx, v)
+}
+
+func (ec *executionContext) marshalNMyTasksPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMyTasksPayload(ctx context.Context, sel ast.SelectionSet, v MyTasksPayload) graphql.Marshaler {
+ return ec._MyTasksPayload(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNMyTasksPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMyTasksPayload(ctx context.Context, sel ast.SelectionSet, v *MyTasksPayload) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ return ec._MyTasksPayload(ctx, sel, v)
+}
+
+func (ec *executionContext) unmarshalNMyTasksSort2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMyTasksSort(ctx context.Context, v interface{}) (MyTasksSort, error) {
+ var res MyTasksSort
+ return res, res.UnmarshalGQL(v)
+}
+
+func (ec *executionContext) marshalNMyTasksSort2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMyTasksSort(ctx context.Context, sel ast.SelectionSet, v MyTasksSort) graphql.Marshaler {
+ return v
+}
+
+func (ec *executionContext) unmarshalNMyTasksStatus2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMyTasksStatus(ctx context.Context, v interface{}) (MyTasksStatus, error) {
+ var res MyTasksStatus
+ return res, res.UnmarshalGQL(v)
+}
+
+func (ec *executionContext) marshalNMyTasksStatus2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMyTasksStatus(ctx context.Context, sel ast.SelectionSet, v MyTasksStatus) graphql.Marshaler {
+ return v
+}
+
func (ec *executionContext) unmarshalNNewProject2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐNewProject(ctx context.Context, v interface{}) (NewProject, error) {
return ec.unmarshalInputNewProject(ctx, v)
}
@@ -23473,6 +23895,47 @@ func (ec *executionContext) marshalNProjectRole2ᚕgithubᚗcomᚋjordanknottᚋ
return ret
}
+func (ec *executionContext) marshalNProjectTaskMapping2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProjectTaskMapping(ctx context.Context, sel ast.SelectionSet, v ProjectTaskMapping) graphql.Marshaler {
+ return ec._ProjectTaskMapping(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNProjectTaskMapping2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProjectTaskMappingᚄ(ctx context.Context, sel ast.SelectionSet, v []ProjectTaskMapping) graphql.Marshaler {
+ ret := make(graphql.Array, len(v))
+ var wg sync.WaitGroup
+ isLen1 := len(v) == 1
+ if !isLen1 {
+ wg.Add(len(v))
+ }
+ for i := range v {
+ i := i
+ fc := &graphql.FieldContext{
+ Index: &i,
+ Result: &v[i],
+ }
+ ctx := graphql.WithFieldContext(ctx, fc)
+ f := func(i int) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ if !isLen1 {
+ defer wg.Done()
+ }
+ ret[i] = ec.marshalNProjectTaskMapping2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProjectTaskMapping(ctx, sel, v[i])
+ }
+ if isLen1 {
+ f(i)
+ } else {
+ go f(i)
+ }
+
+ }
+ wg.Wait()
+ return ret
+}
+
func (ec *executionContext) marshalNRefreshToken2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐRefreshToken(ctx context.Context, sel ast.SelectionSet, v db.RefreshToken) graphql.Marshaler {
return ec._RefreshToken(ctx, sel, &v)
}
@@ -24875,6 +25338,38 @@ func (ec *executionContext) marshalOUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx
return MarshalUUID(v)
}
+func (ec *executionContext) unmarshalOUUID2ᚕgithubᚗcomᚋgoogleᚋuuidᚐUUIDᚄ(ctx context.Context, v interface{}) ([]uuid.UUID, error) {
+ var vSlice []interface{}
+ if v != nil {
+ if tmp1, ok := v.([]interface{}); ok {
+ vSlice = tmp1
+ } else {
+ vSlice = []interface{}{v}
+ }
+ }
+ var err error
+ res := make([]uuid.UUID, len(vSlice))
+ for i := range vSlice {
+ res[i], err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, vSlice[i])
+ if err != nil {
+ return nil, err
+ }
+ }
+ return res, nil
+}
+
+func (ec *executionContext) marshalOUUID2ᚕgithubᚗcomᚋgoogleᚋuuidᚐUUIDᚄ(ctx context.Context, sel ast.SelectionSet, v []uuid.UUID) graphql.Marshaler {
+ if v == nil {
+ return graphql.Null
+ }
+ ret := make(graphql.Array, len(v))
+ for i := range v {
+ ret[i] = ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, sel, v[i])
+ }
+
+ return ret
+}
+
func (ec *executionContext) unmarshalOUUID2ᚖgithubᚗcomᚋgoogleᚋuuidᚐUUID(ctx context.Context, v interface{}) (*uuid.UUID, error) {
if v == nil {
return nil, nil
diff --git a/internal/graph/models_gen.go b/internal/graph/models_gen.go
index 6181b4c..1f48204 100644
--- a/internal/graph/models_gen.go
+++ b/internal/graph/models_gen.go
@@ -291,6 +291,16 @@ type MemberSearchResult struct {
Status ShareStatus `json:"status"`
}
+type MyTasks struct {
+ Status MyTasksStatus `json:"status"`
+ Sort MyTasksSort `json:"sort"`
+}
+
+type MyTasksPayload struct {
+ Tasks []db.Task `json:"tasks"`
+ Projects []ProjectTaskMapping `json:"projects"`
+}
+
type NewProject struct {
TeamID *uuid.UUID `json:"teamID"`
Name string `json:"name"`
@@ -307,9 +317,10 @@ type NewRefreshToken struct {
}
type NewTask struct {
- TaskGroupID uuid.UUID `json:"taskGroupID"`
- Name string `json:"name"`
- Position float64 `json:"position"`
+ TaskGroupID uuid.UUID `json:"taskGroupID"`
+ Name string `json:"name"`
+ Position float64 `json:"position"`
+ Assigned []uuid.UUID `json:"assigned"`
}
type NewTaskGroup struct {
@@ -376,6 +387,11 @@ type ProjectRole struct {
RoleCode RoleCode `json:"roleCode"`
}
+type ProjectTaskMapping struct {
+ ProjectID uuid.UUID `json:"projectID"`
+ TaskID uuid.UUID `json:"taskID"`
+}
+
type ProjectsFilter struct {
TeamID *uuid.UUID `json:"teamID"`
}
@@ -797,6 +813,102 @@ func (e EntityType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
+type MyTasksSort string
+
+const (
+ MyTasksSortNone MyTasksSort = "NONE"
+ MyTasksSortProject MyTasksSort = "PROJECT"
+ MyTasksSortDueDate MyTasksSort = "DUE_DATE"
+)
+
+var AllMyTasksSort = []MyTasksSort{
+ MyTasksSortNone,
+ MyTasksSortProject,
+ MyTasksSortDueDate,
+}
+
+func (e MyTasksSort) IsValid() bool {
+ switch e {
+ case MyTasksSortNone, MyTasksSortProject, MyTasksSortDueDate:
+ return true
+ }
+ return false
+}
+
+func (e MyTasksSort) String() string {
+ return string(e)
+}
+
+func (e *MyTasksSort) UnmarshalGQL(v interface{}) error {
+ str, ok := v.(string)
+ if !ok {
+ return fmt.Errorf("enums must be strings")
+ }
+
+ *e = MyTasksSort(str)
+ if !e.IsValid() {
+ return fmt.Errorf("%s is not a valid MyTasksSort", str)
+ }
+ return nil
+}
+
+func (e MyTasksSort) MarshalGQL(w io.Writer) {
+ fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+type MyTasksStatus string
+
+const (
+ MyTasksStatusAll MyTasksStatus = "ALL"
+ MyTasksStatusIncomplete MyTasksStatus = "INCOMPLETE"
+ MyTasksStatusCompleteAll MyTasksStatus = "COMPLETE_ALL"
+ MyTasksStatusCompleteToday MyTasksStatus = "COMPLETE_TODAY"
+ MyTasksStatusCompleteYesterday MyTasksStatus = "COMPLETE_YESTERDAY"
+ MyTasksStatusCompleteOneWeek MyTasksStatus = "COMPLETE_ONE_WEEK"
+ MyTasksStatusCompleteTwoWeek MyTasksStatus = "COMPLETE_TWO_WEEK"
+ MyTasksStatusCompleteThreeWeek MyTasksStatus = "COMPLETE_THREE_WEEK"
+)
+
+var AllMyTasksStatus = []MyTasksStatus{
+ MyTasksStatusAll,
+ MyTasksStatusIncomplete,
+ MyTasksStatusCompleteAll,
+ MyTasksStatusCompleteToday,
+ MyTasksStatusCompleteYesterday,
+ MyTasksStatusCompleteOneWeek,
+ MyTasksStatusCompleteTwoWeek,
+ MyTasksStatusCompleteThreeWeek,
+}
+
+func (e MyTasksStatus) IsValid() bool {
+ switch e {
+ case MyTasksStatusAll, MyTasksStatusIncomplete, MyTasksStatusCompleteAll, MyTasksStatusCompleteToday, MyTasksStatusCompleteYesterday, MyTasksStatusCompleteOneWeek, MyTasksStatusCompleteTwoWeek, MyTasksStatusCompleteThreeWeek:
+ return true
+ }
+ return false
+}
+
+func (e MyTasksStatus) String() string {
+ return string(e)
+}
+
+func (e *MyTasksStatus) UnmarshalGQL(v interface{}) error {
+ str, ok := v.(string)
+ if !ok {
+ return fmt.Errorf("enums must be strings")
+ }
+
+ *e = MyTasksStatus(str)
+ if !e.IsValid() {
+ return fmt.Errorf("%s is not a valid MyTasksStatus", str)
+ }
+ return nil
+}
+
+func (e MyTasksStatus) MarshalGQL(w io.Writer) {
+ fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
type ObjectType string
const (
diff --git a/internal/graph/schema.graphqls b/internal/graph/schema.graphqls
index 5a5e275..30369da 100644
--- a/internal/graph/schema.graphqls
+++ b/internal/graph/schema.graphqls
@@ -261,13 +261,47 @@ type Query {
projects(input: ProjectsFilter): [Project!]!
findTeam(input: FindTeam!): Team!
teams: [Team!]!
+ myTasks(input: MyTasks!): MyTasksPayload!
labelColors: [LabelColor!]!
taskGroups: [TaskGroup!]!
me: MePayload!
}
+
type Mutation
+enum MyTasksStatus {
+ ALL
+ INCOMPLETE
+ COMPLETE_ALL
+ COMPLETE_TODAY
+ COMPLETE_YESTERDAY
+ COMPLETE_ONE_WEEK
+ COMPLETE_TWO_WEEK
+ COMPLETE_THREE_WEEK
+}
+
+enum MyTasksSort {
+ NONE
+ PROJECT
+ DUE_DATE
+}
+
+input MyTasks {
+ status: MyTasksStatus!
+ sort: MyTasksSort!
+}
+
+type ProjectTaskMapping {
+ projectID: UUID!
+ taskID: UUID!
+}
+
+type MyTasksPayload {
+ tasks: [Task!]!
+ projects: [ProjectTaskMapping!]!
+}
+
type TeamRole {
teamID: UUID!
roleCode: RoleCode!
@@ -494,6 +528,7 @@ input NewTask {
taskGroupID: UUID!
name: String!
position: Float!
+ assigned: [UUID!]
}
input AssignTaskInput {
@@ -945,4 +980,3 @@ type DeleteUserAccountPayload {
ok: Boolean!
userAccount: UserAccount!
}
-
diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go
index 6f6c020..cfbdbef 100644
--- a/internal/graph/schema.resolvers.go
+++ b/internal/graph/schema.resolvers.go
@@ -314,6 +314,21 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.T
ActivityTypeID: 1,
})
+ if len(input.Assigned) != 0 {
+ assignedDate := time.Now().UTC()
+ for _, assigned := range input.Assigned {
+ assignedTask, err := r.Repository.CreateTaskAssigned(ctx, db.CreateTaskAssignedParams{TaskID: task.TaskID, UserID: assigned, AssignedDate: assignedDate})
+ logger.New(ctx).WithFields(log.Fields{
+ "assignedUserID": assignedTask.UserID,
+ "taskID": assignedTask.TaskID,
+ "assignedTaskID": assignedTask.TaskAssignedID,
+ }).Info("assigned task")
+ if err != nil {
+ return &db.Task{}, err
+ }
+ }
+ }
+
if err != nil {
logger.New(ctx).WithError(err).Error("issue while creating task")
return &db.Task{}, err
@@ -1391,6 +1406,38 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
return foundTeams, nil
}
+func (r *queryResolver) MyTasks(ctx context.Context, input MyTasks) (*MyTasksPayload, error) {
+ userID, _ := GetUserID(ctx)
+ projects := []ProjectTaskMapping{}
+ var tasks []db.Task
+ var err error
+ if input.Sort == MyTasksSortNone {
+ tasks, err = r.Repository.GetRecentlyAssignedTaskForUserID(ctx, userID)
+ if err != nil && err != sql.ErrNoRows {
+ return &MyTasksPayload{}, err
+ }
+ } else if input.Sort == MyTasksSortProject {
+ tasks, err = r.Repository.GetAssignedTasksProjectForUserID(ctx, userID)
+ if err != nil && err != sql.ErrNoRows {
+ return &MyTasksPayload{}, err
+ }
+ } else if input.Sort == MyTasksSortDueDate {
+ tasks, err = r.Repository.GetAssignedTasksDueDateForUserID(ctx, userID)
+ if err != nil && err != sql.ErrNoRows {
+ return &MyTasksPayload{}, err
+ }
+ }
+ taskIds := []uuid.UUID{}
+ for _, task := range tasks {
+ taskIds = append(taskIds, task.TaskID)
+ }
+ mappings, err := r.Repository.GetProjectIdMappings(ctx, taskIds)
+ for _, mapping := range mappings {
+ projects = append(projects, ProjectTaskMapping{ProjectID: mapping.ProjectID, TaskID: mapping.TaskID})
+ }
+ return &MyTasksPayload{Tasks: tasks, Projects: projects}, err
+}
+
func (r *queryResolver) LabelColors(ctx context.Context) ([]db.LabelColor, error) {
return r.Repository.GetLabelColors(ctx)
}
diff --git a/internal/graph/schema/_root.gql b/internal/graph/schema/_root.gql
index 38e479b..091e442 100644
--- a/internal/graph/schema/_root.gql
+++ b/internal/graph/schema/_root.gql
@@ -37,13 +37,47 @@ type Query {
projects(input: ProjectsFilter): [Project!]!
findTeam(input: FindTeam!): Team!
teams: [Team!]!
+ myTasks(input: MyTasks!): MyTasksPayload!
labelColors: [LabelColor!]!
taskGroups: [TaskGroup!]!
me: MePayload!
}
+
type Mutation
+enum MyTasksStatus {
+ ALL
+ INCOMPLETE
+ COMPLETE_ALL
+ COMPLETE_TODAY
+ COMPLETE_YESTERDAY
+ COMPLETE_ONE_WEEK
+ COMPLETE_TWO_WEEK
+ COMPLETE_THREE_WEEK
+}
+
+enum MyTasksSort {
+ NONE
+ PROJECT
+ DUE_DATE
+}
+
+input MyTasks {
+ status: MyTasksStatus!
+ sort: MyTasksSort!
+}
+
+type ProjectTaskMapping {
+ projectID: UUID!
+ taskID: UUID!
+}
+
+type MyTasksPayload {
+ tasks: [Task!]!
+ projects: [ProjectTaskMapping!]!
+}
+
type TeamRole {
teamID: UUID!
roleCode: RoleCode!
diff --git a/internal/graph/schema/task.gql b/internal/graph/schema/task.gql
index e223f26..ded5c21 100644
--- a/internal/graph/schema/task.gql
+++ b/internal/graph/schema/task.gql
@@ -25,6 +25,7 @@ input NewTask {
taskGroupID: UUID!
name: String!
position: Float!
+ assigned: [UUID!]
}
input AssignTaskInput {