diff --git a/frontend/src/MyTasks/MyTasksStatus.tsx b/frontend/src/MyTasks/MyTasksStatus.tsx new file mode 100644 index 0000000..77ff323 --- /dev/null +++ b/frontend/src/MyTasks/MyTasksStatus.tsx @@ -0,0 +1,151 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { Checkmark } from 'shared/icons'; +import { TaskStatusFilter, TaskStatus, TaskSince } from 'shared/components/Lists'; +import { MyTasksStatus } from 'shared/generated/graphql'; +import { Popup } from 'shared/components/PopupMenu'; + +export const ActionsList = styled.ul` + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +`; + +export const ActionExtraMenuContainer = styled.div` + visibility: hidden; + position: absolute; + left: 100%; + top: -4px; + padding-left: 2px; + width: 100%; +`; + +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}; + } + &:hover ${ActionExtraMenuContainer} { + visibility: visible; + } +`; + +export const ActionTitle = styled.span` + margin-left: 20px; +`; + +export const ActionExtraMenu = styled.ul` + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + + padding: 5px; + padding-top: 8px; + border-radius: 5px; + box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1); + + color: #c2c6dc; + background: #262c49; + border: 1px solid rgba(0, 0, 0, 0.1); + border-color: #414561; +`; + +export const ActionExtraMenuItem = 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: rgb(${props => props.theme.colors.primary}); + } +`; +const ActionExtraMenuSeparator = styled.li` + color: ${props => props.theme.colors.text.primary}; + font-size: 12px; + padding-left: 4px; + padding-right: 4px; + padding-top: 0.25rem; + padding-bottom: 0.25rem; +`; + +const ActiveIcon = styled(Checkmark)` + position: absolute; +`; + +type MyTasksStatusProps = { + status: MyTasksStatus; + onChangeStatus: (status: MyTasksStatus) => void; +}; + +const MyTasksStatusPopup: React.FC = ({ status: initialStatus, onChangeStatus }) => { + const [status, setStatus] = useState(initialStatus); + const handleStatusChange = (f: MyTasksStatus) => { + setStatus(f); + onChangeStatus(f); + }; + return ( + + + handleStatusChange(MyTasksStatus.Incomplete)}> + {status === MyTasksStatus.Incomplete && } + Incomplete Tasks + + + {status !== MyTasksStatus.Incomplete && status !== MyTasksStatus.All && } + Compelete Tasks + + + handleStatusChange(MyTasksStatus.CompleteAll)}> + {status === MyTasksStatus.CompleteAll && } + All completed tasks + + Marked complete since + handleStatusChange(MyTasksStatus.CompleteToday)}> + {status === MyTasksStatus.CompleteToday && } + Today + + handleStatusChange(MyTasksStatus.CompleteYesterday)}> + {status === MyTasksStatus.CompleteYesterday && } + + Yesterday + + handleStatusChange(MyTasksStatus.CompleteOneWeek)}> + {status === MyTasksStatus.CompleteOneWeek && } + 1 week + + handleStatusChange(MyTasksStatus.CompleteTwoWeek)}> + {status === MyTasksStatus.CompleteTwoWeek && } + 2 weeks + + handleStatusChange(MyTasksStatus.CompleteThreeWeek)}> + {status === MyTasksStatus.CompleteThreeWeek && } + 3 weeks + + + + + handleStatusChange(MyTasksStatus.All)}> + {status === MyTasksStatus.All && } + All Tasks + + + + ); +}; + +export default MyTasksStatusPopup; diff --git a/frontend/src/MyTasks/index.tsx b/frontend/src/MyTasks/index.tsx index b61e3b3..1038a10 100644 --- a/frontend/src/MyTasks/index.tsx +++ b/frontend/src/MyTasks/index.tsx @@ -19,7 +19,7 @@ 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 { Sort, Cogs, CaretDown, CheckCircle, CaretRight, CheckCircleOutline } from 'shared/icons'; import Select from 'react-select'; import { editorColourStyles } from 'shared/components/Select'; import useOnOutsideClick from 'shared/hooks/onOutsideClick'; @@ -27,12 +27,36 @@ import DueDateManager from 'shared/components/DueDateManager'; import dayjs from 'dayjs'; import useStickyState from 'shared/hooks/useStickyState'; import MyTasksSortPopup from './MyTasksSort'; +import MyTasksStatusPopup from './MyTasksStatus'; import TaskEntry from './TaskEntry'; type TaskRouteProps = { taskID: string; }; +function prettyStatus(status: MyTasksStatus) { + switch (status) { + case MyTasksStatus.All: + return 'All tasks'; + case MyTasksStatus.Incomplete: + return 'Incomplete tasks'; + case MyTasksStatus.CompleteAll: + return 'All completed tasks'; + case MyTasksStatus.CompleteToday: + return 'Completed tasks: today'; + case MyTasksStatus.CompleteYesterday: + return 'Completed tasks: yesterday'; + case MyTasksStatus.CompleteOneWeek: + return 'Completed tasks: 1 week'; + case MyTasksStatus.CompleteTwoWeek: + return 'Completed tasks: 2 weeks'; + case MyTasksStatus.CompleteThreeWeek: + return 'Completed tasks: 3 weeks'; + default: + return 'unknown tasks'; + } +} + function prettySort(sort: MyTasksSort) { if (sort === MyTasksSort.None) { return 'Sort'; @@ -492,7 +516,10 @@ const Projects = () => { { sort: MyTasksSort.None, status: MyTasksStatus.All }, 'my_tasks_filter', ); - const { data } = useMyTasksQuery({ variables: { sort: filters.sort, status: filters.status } }); + const { data } = useMyTasksQuery({ + variables: { sort: filters.sort, status: filters.status }, + fetchPolicy: 'cache-and-network', + }); const [dateEditor, setDateEditor] = useState({ open: false, pos: null, task: null }); const onEditDueDate = (task: Task, $target: React.RefObject) => { if ($target && $target.current && data) { @@ -524,7 +551,7 @@ const Projects = () => { }); } }; - const { showPopup } = usePopup(); + const { showPopup, hidePopup } = usePopup(); const [updateTaskDueDate] = useUpdateTaskDueDateMutation(); const $editorContents = useRef(null); const $dateContents = useRef(null); @@ -653,9 +680,23 @@ const Projects = () => { - - - All Tasks + { + showPopup( + $target, + { + setFilters(prev => ({ ...prev, status })); + hidePopup(); + }} + />, + { width: 185 }, + ); + }} + > + + {prettyStatus(filters.status)} { @@ -663,8 +704,12 @@ const Projects = () => { $target, setFilters(prev => ({ ...prev, sort }))} + onChangeSort={sort => { + setFilters(prev => ({ ...prev, sort })); + hidePopup(); + }} />, + { width: 185 }, ); }} > diff --git a/go.mod b/go.mod index c7a4d33..b6aa1b4 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/go-chi/chi v3.3.2+incompatible github.com/golang-migrate/migrate/v4 v4.11.0 github.com/google/uuid v1.1.1 + github.com/jinzhu/now v1.1.1 github.com/jmoiron/sqlx v1.2.0 github.com/lib/pq v1.3.0 github.com/lithammer/fuzzysearch v1.1.0 diff --git a/go.sum b/go.sum index 5cbf910..28ebd6d 100644 --- a/go.sum +++ b/go.sum @@ -338,6 +338,8 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ= github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= diff --git a/internal/db/querier.go b/internal/db/querier.go index 2465ebd..8d005c4 100644 --- a/internal/db/querier.go +++ b/internal/db/querier.go @@ -69,8 +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) + GetAssignedTasksDueDateForUserID(ctx context.Context, arg GetAssignedTasksDueDateForUserIDParams) ([]Task, error) + GetAssignedTasksProjectForUserID(ctx context.Context, arg GetAssignedTasksProjectForUserIDParams) ([]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) @@ -99,7 +99,7 @@ type Querier interface { 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) + GetRecentlyAssignedTaskForUserID(ctx context.Context, arg GetRecentlyAssignedTaskForUserIDParams) ([]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 59aad2d..359e8d8 100644 --- a/internal/db/query/task.sql +++ b/internal/db/query/task.sql @@ -59,14 +59,25 @@ UPDATE task_comment SET message = $2, updated_at = $3 WHERE task_comment_id = $1 -- 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; + task ON task.task_id = task_assigned.task_id WHERE user_id = $1 + AND $4::boolean = true OR ( + $4::boolean = false AND complete = $2 AND ( + $2 = false OR ($2 = true AND completed_at > $3) + ) + ) + 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 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; + WHERE user_id = $1 + AND $4::boolean = true OR ( + $4::boolean = false AND complete = $2 AND ( + $2 = false OR ($2 = true AND completed_at > $3) + ) + ) + ORDER BY task_group.project_id DESC, task_assigned.assigned_date DESC; -- name: GetProjectIdMappings :many SELECT project_id, task_id FROM task @@ -75,7 +86,12 @@ INNER JOIN task_group ON task_group.task_group_id = task.task_group_id -- name: GetAssignedTasksDueDateForUserID :many SELECT task.* FROM task_assigned - INNER JOIN task ON task.task_id = task_assigned.task_id + 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; + WHERE user_id = $1 + AND $4::boolean = true OR ( + $4::boolean = false AND complete = $2 AND ( + $2 = false OR ($2 = true AND completed_at > $3) + ) + ) + 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 dc4cc20..8cab7ef 100644 --- a/internal/db/task.sql.go +++ b/internal/db/task.sql.go @@ -200,14 +200,31 @@ func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) { 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 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 + WHERE user_id = $1 + AND $4::boolean = true OR ( + $4::boolean = false AND complete = $2 AND ( + $2 = false OR ($2 = true AND completed_at > $3) + ) + ) + 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) +type GetAssignedTasksDueDateForUserIDParams struct { + UserID uuid.UUID `json:"user_id"` + Complete bool `json:"complete"` + CompletedAt sql.NullTime `json:"completed_at"` + Column4 bool `json:"column_4"` +} + +func (q *Queries) GetAssignedTasksDueDateForUserID(ctx context.Context, arg GetAssignedTasksDueDateForUserIDParams) ([]Task, error) { + rows, err := q.db.QueryContext(ctx, getAssignedTasksDueDateForUserID, + arg.UserID, + arg.Complete, + arg.CompletedAt, + arg.Column4, + ) if err != nil { return nil, err } @@ -242,14 +259,31 @@ func (q *Queries) GetAssignedTasksDueDateForUserID(ctx context.Context, userID u 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 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 + WHERE user_id = $1 + AND $4::boolean = true OR ( + $4::boolean = false AND complete = $2 AND ( + $2 = false OR ($2 = true AND completed_at > $3) + ) + ) + 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) +type GetAssignedTasksProjectForUserIDParams struct { + UserID uuid.UUID `json:"user_id"` + Complete bool `json:"complete"` + CompletedAt sql.NullTime `json:"completed_at"` + Column4 bool `json:"column_4"` +} + +func (q *Queries) GetAssignedTasksProjectForUserID(ctx context.Context, arg GetAssignedTasksProjectForUserIDParams) ([]Task, error) { + rows, err := q.db.QueryContext(ctx, getAssignedTasksProjectForUserID, + arg.UserID, + arg.Complete, + arg.CompletedAt, + arg.Column4, + ) if err != nil { return nil, err } @@ -366,11 +400,29 @@ func (q *Queries) GetProjectIdMappings(ctx context.Context, dollar_1 []uuid.UUID 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 + task ON task.task_id = task_assigned.task_id WHERE user_id = $1 + AND $4::boolean = true OR ( + $4::boolean = false AND complete = $2 AND ( + $2 = false OR ($2 = true AND completed_at > $3) + ) + ) + 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) +type GetRecentlyAssignedTaskForUserIDParams struct { + UserID uuid.UUID `json:"user_id"` + Complete bool `json:"complete"` + CompletedAt sql.NullTime `json:"completed_at"` + Column4 bool `json:"column_4"` +} + +func (q *Queries) GetRecentlyAssignedTaskForUserID(ctx context.Context, arg GetRecentlyAssignedTaskForUserIDParams) ([]Task, error) { + rows, err := q.db.QueryContext(ctx, getRecentlyAssignedTaskForUserID, + arg.UserID, + arg.Complete, + arg.CompletedAt, + arg.Column4, + ) if err != nil { return nil, err } diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index cfbdbef..731ea25 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -12,6 +12,7 @@ import ( "time" "github.com/google/uuid" + "github.com/jinzhu/now" "github.com/jordanknott/taskcafe/internal/auth" "github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/logger" @@ -1411,18 +1412,60 @@ func (r *queryResolver) MyTasks(ctx context.Context, input MyTasks) (*MyTasksPay projects := []ProjectTaskMapping{} var tasks []db.Task var err error + showAll := false + if input.Status == MyTasksStatusAll { + showAll = true + } + complete := false + completedAt := sql.NullTime{Valid: false, Time: time.Time{}} + switch input.Status { + case MyTasksStatusCompleteAll: + complete = true + completedAt = sql.NullTime{Valid: true, Time: time.Time{}} + case MyTasksStatusCompleteToday: + complete = true + completedAt = sql.NullTime{Valid: true, Time: now.BeginningOfDay()} + case MyTasksStatusCompleteYesterday: + complete = true + completedAt = sql.NullTime{Valid: true, Time: now.With(time.Now().AddDate(0, 0, -1)).BeginningOfDay()} + case MyTasksStatusCompleteOneWeek: + complete = true + completedAt = sql.NullTime{Valid: true, Time: now.With(time.Now().AddDate(0, 0, -7)).BeginningOfDay()} + case MyTasksStatusCompleteTwoWeek: + complete = true + completedAt = sql.NullTime{Valid: true, Time: now.With(time.Now().AddDate(0, 0, -14)).BeginningOfDay()} + case MyTasksStatusCompleteThreeWeek: + complete = true + completedAt = sql.NullTime{Valid: true, Time: now.With(time.Now().AddDate(0, 0, -21)).BeginningOfDay()} + } + if input.Sort == MyTasksSortNone { - tasks, err = r.Repository.GetRecentlyAssignedTaskForUserID(ctx, userID) + tasks, err = r.Repository.GetRecentlyAssignedTaskForUserID(ctx, db.GetRecentlyAssignedTaskForUserIDParams{ + UserID: userID, + Complete: complete, + CompletedAt: completedAt, + Column4: showAll, + }) if err != nil && err != sql.ErrNoRows { return &MyTasksPayload{}, err } } else if input.Sort == MyTasksSortProject { - tasks, err = r.Repository.GetAssignedTasksProjectForUserID(ctx, userID) + tasks, err = r.Repository.GetAssignedTasksProjectForUserID(ctx, db.GetAssignedTasksProjectForUserIDParams{ + UserID: userID, + Complete: complete, + CompletedAt: completedAt, + Column4: showAll, + }) if err != nil && err != sql.ErrNoRows { return &MyTasksPayload{}, err } } else if input.Sort == MyTasksSortDueDate { - tasks, err = r.Repository.GetAssignedTasksDueDateForUserID(ctx, userID) + tasks, err = r.Repository.GetAssignedTasksDueDateForUserID(ctx, db.GetAssignedTasksDueDateForUserIDParams{ + UserID: userID, + Complete: complete, + CompletedAt: completedAt, + Column4: showAll, + }) if err != nil && err != sql.ErrNoRows { return &MyTasksPayload{}, err }