feat(MyTasks): allow filtering by task complete status

This commit is contained in:
Jordan Knott 2021-01-03 17:04:15 -06:00
parent a1c9251a1f
commit f051bebd48
8 changed files with 343 additions and 33 deletions

View File

@ -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<MyTasksStatusProps> = ({ status: initialStatus, onChangeStatus }) => {
const [status, setStatus] = useState(initialStatus);
const handleStatusChange = (f: MyTasksStatus) => {
setStatus(f);
onChangeStatus(f);
};
return (
<Popup tab={0} title={null}>
<ActionsList>
<ActionItem onClick={() => handleStatusChange(MyTasksStatus.Incomplete)}>
{status === MyTasksStatus.Incomplete && <ActiveIcon width={12} height={12} />}
<ActionTitle>Incomplete Tasks</ActionTitle>
</ActionItem>
<ActionItem>
{status !== MyTasksStatus.Incomplete && status !== MyTasksStatus.All && <ActiveIcon width={12} height={12} />}
<ActionTitle>Compelete Tasks</ActionTitle>
<ActionExtraMenuContainer>
<ActionExtraMenu>
<ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteAll)}>
{status === MyTasksStatus.CompleteAll && <ActiveIcon width={12} height={12} />}
<ActionTitle>All completed tasks</ActionTitle>
</ActionExtraMenuItem>
<ActionExtraMenuSeparator>Marked complete since</ActionExtraMenuSeparator>
<ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteToday)}>
{status === MyTasksStatus.CompleteToday && <ActiveIcon width={12} height={12} />}
<ActionTitle>Today</ActionTitle>
</ActionExtraMenuItem>
<ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteYesterday)}>
{status === MyTasksStatus.CompleteYesterday && <ActiveIcon width={12} height={12} />}
<ActionTitle>Yesterday</ActionTitle>
</ActionExtraMenuItem>
<ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteOneWeek)}>
{status === MyTasksStatus.CompleteOneWeek && <ActiveIcon width={12} height={12} />}
<ActionTitle>1 week</ActionTitle>
</ActionExtraMenuItem>
<ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteTwoWeek)}>
{status === MyTasksStatus.CompleteTwoWeek && <ActiveIcon width={12} height={12} />}
<ActionTitle>2 weeks</ActionTitle>
</ActionExtraMenuItem>
<ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteThreeWeek)}>
{status === MyTasksStatus.CompleteThreeWeek && <ActiveIcon width={12} height={12} />}
<ActionTitle>3 weeks</ActionTitle>
</ActionExtraMenuItem>
</ActionExtraMenu>
</ActionExtraMenuContainer>
</ActionItem>
<ActionItem onClick={() => handleStatusChange(MyTasksStatus.All)}>
{status === MyTasksStatus.All && <ActiveIcon width={12} height={12} />}
<ActionTitle>All Tasks</ActionTitle>
</ActionItem>
</ActionsList>
</Popup>
);
};
export default MyTasksStatusPopup;

View File

@ -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<DateEditorState>({ open: false, pos: null, task: null });
const onEditDueDate = (task: Task, $target: React.RefObject<HTMLElement>) => {
if ($target && $target.current && data) {
@ -524,7 +551,7 @@ const Projects = () => {
});
}
};
const { showPopup } = usePopup();
const { showPopup, hidePopup } = usePopup();
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
const $editorContents = useRef<HTMLDivElement>(null);
const $dateContents = useRef<HTMLDivElement>(null);
@ -653,9 +680,23 @@ const Projects = () => {
<ProjectBar>
<ProjectActions />
<ProjectActions>
<ProjectAction disabled>
<CheckCircle width={13} height={13} />
<ProjectActionText>All Tasks</ProjectActionText>
<ProjectAction
onClick={$target => {
showPopup(
$target,
<MyTasksStatusPopup
status={filters.status}
onChangeStatus={status => {
setFilters(prev => ({ ...prev, status }));
hidePopup();
}}
/>,
{ width: 185 },
);
}}
>
<CheckCircleOutline width={13} height={13} />
<ProjectActionText>{prettyStatus(filters.status)}</ProjectActionText>
</ProjectAction>
<ProjectAction
onClick={$target => {
@ -663,8 +704,12 @@ const Projects = () => {
$target,
<MyTasksSortPopup
sort={filters.sort}
onChangeSort={sort => setFilters(prev => ({ ...prev, sort }))}
onChangeSort={sort => {
setFilters(prev => ({ ...prev, sort }));
hidePopup();
}}
/>,
{ width: 185 },
);
}}
>

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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)

View File

@ -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;

View File

@ -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
}

View File

@ -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
}