From 531ae46f5bd355eff9f27ed84ab7db2d3ef0d74e Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sat, 4 Dec 2021 12:00:12 +0100 Subject: [PATCH] fix task sorting and individual task deadline issues on front page. --- api/task.go | 16 +++- database/postgres/tasks.go | 16 +++- ...33_add_project_column_task_sort_fields.sql | 11 +++ models/project.go | 20 ++++- models/task.go | 88 +++++++++++++++++++ services/loader.go | 4 + svelte-ui/src/clients/stufflog.ts | 5 +- svelte-ui/src/components/ProjectEntry.svelte | 4 +- svelte-ui/src/components/TaskList.svelte | 6 +- svelte-ui/src/models/task.ts | 1 + svelte-ui/src/pages/FrontPage.svelte | 5 +- 11 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 migrations/postgres/20211204105833_add_project_column_task_sort_fields.sql diff --git a/api/task.go b/api/task.go index 601efc1..e999729 100644 --- a/api/task.go +++ b/api/task.go @@ -8,6 +8,8 @@ import ( "github.com/gissleh/stufflog/internal/slerrors" "github.com/gissleh/stufflog/models" "github.com/gissleh/stufflog/services" + "sort" + "strings" "time" ) @@ -25,7 +27,19 @@ func Task(g *gin.RouterGroup, db database.Database) { filter.Expiring = &expiring } - return l.ListTasks(c, filter) + tasks, err := l.ListTasks(c, filter) + if err != nil { + return nil, err + } + + sort.Sort( + models.TaskSorter{ + Fields: strings.Split(c.Query("sort"), ","), + Data: tasks, + }, + ) + + return tasks, nil })) g.GET("/:id", handler("task", func(c *gin.Context) (interface{}, error) { diff --git a/database/postgres/tasks.go b/database/postgres/tasks.go index e69fa9c..38585ea 100644 --- a/database/postgres/tasks.go +++ b/database/postgres/tasks.go @@ -41,7 +41,21 @@ func (r *taskRepository) ListWithLinks(ctx context.Context, filter models.TaskFi sq := squirrel.Select("task.*", "tl.project_id as tl_project_id", "p.icon").From("task").PlaceholderFormat(squirrel.Dollar) sq = sq.Where(squirrel.Eq{"task.user_id": filter.UserID}) if filter.Active != nil { - sq = sq.Where(squirrel.Eq{"task.active": *filter.Active}) + if *filter.Active { + sq = sq.Where(squirrel.Or{ + squirrel.Eq{"task.active": true}, + squirrel.Eq{"task.status_tag": []string{ + "on hold", "to do", + }}, + }) + } else { + sq = sq.Where(squirrel.And{ + squirrel.Eq{"task.active": false}, + squirrel.Eq{"task.status_tag": []string{ + "failed", "completed", "declined", + }}, + }) + } } if filter.Expiring != nil { if *filter.Expiring { diff --git a/migrations/postgres/20211204105833_add_project_column_task_sort_fields.sql b/migrations/postgres/20211204105833_add_project_column_task_sort_fields.sql new file mode 100644 index 0000000..8ca53f4 --- /dev/null +++ b/migrations/postgres/20211204105833_add_project_column_task_sort_fields.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE project + ADD COLUMN task_sort_fields TEXT[] DEFAULT ARRAY[]::TEXT[]; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE project + DROP COLUMN task_sort_fields; +-- +goose StatementEnd diff --git a/models/project.go b/models/project.go index 3f2c5aa..7c74446 100644 --- a/models/project.go +++ b/models/project.go @@ -2,6 +2,7 @@ package models import ( "context" + "sort" "time" ) @@ -20,6 +21,7 @@ type Project struct { StatusTag *string `json:"statusTag" db:"status_tag"` Favorite bool `json:"favorite" db:"favorite"` Tags []string `json:"tags" db:"tags"` + TaskSortFields []string `json:"taskSortFields" db:"task_sort_fields"` } func (project *Project) Update(update ProjectUpdate) { @@ -74,6 +76,9 @@ func (project *Project) Update(update ProjectUpdate) { if update.SetTags != nil { project.Tags = update.SetTags } + if update.TaskSortFields != nil { + project.TaskSortFields = append(update.TaskSortFields[:0:0], update.TaskSortFields...) + } if project.StatusTag != nil && project.Active { project.StatusTag = nil @@ -95,14 +100,27 @@ type ProjectUpdate struct { ClearStatusTag bool `json:"clearStatusTag"` SetTags []string `json:"setTags"` Favorite *bool `json:"favorite"` + TaskSortFields []string `json:"taskSortFields"` } type ProjectResult struct { Project - Tasks []*TaskResult `json:"tasks"` + Tasks []*TaskResult `json:"tasks"` Subtractions []ProjectSubtraction `json:"subtractions"` } +func (result *ProjectResult) SortTasks() { + sorter := TaskSorter{ + Data: result.Tasks, + Fields: result.Project.TaskSortFields, + } + if len(sorter.Fields) == 0 { + sorter.Fields = []string{"status"} + } + + sort.Sort(sorter) +} + type ProjectSubtraction struct { Item Item `json:"item"` Amount int `json:"amount"` diff --git a/models/task.go b/models/task.go index 68ae6a7..927f13a 100644 --- a/models/task.go +++ b/models/task.go @@ -94,6 +94,94 @@ type TaskFilter struct { ProjectIDs []string } +var taskStatusOrder = []string{"", "to do", "on hold", "completed", "failed", "declined"} + +type TaskSorter struct { + Data []*TaskResult + Fields []string +} + +func (s *TaskSorter) Valid() bool { + for _, field := range s.Fields { + switch field { + case "name", "-name", "createdTime", "-createdTime", "amount", "-amount", "status", "-status": + default: + return false + } + } + + return true +} + +func (s TaskSorter) Len() int { + return len(s.Data) +} + +func (s TaskSorter) Less(i, j int) bool { + a := s.Data[i] + b := s.Data[j] + + for _, field := range s.Fields { + switch field { + case "status", "-status": + as := "" + if a.StatusTag != nil { + as = *a.StatusTag + } + bs := "" + if b.StatusTag != nil { + bs = *b.StatusTag + } + + if as != bs { + asi := 1000 + bsi := 1000 + + for i, sn := range taskStatusOrder { + if sn == as { + asi = i + } + if sn == bs { + bsi = i + } + } + + if field == "-status" { + return asi > bsi + } else { + return asi < bsi + } + } + case "amount": + if a.ItemAmount != b.ItemAmount { + return a.ItemAmount < b.ItemAmount + } + case "-amount": + if a.ItemAmount != b.ItemAmount { + return a.ItemAmount > b.ItemAmount + } + case "name": + if a.Name != b.Name { + return a.Name < b.Name + } + case "-name": + if a.Name != b.Name { + return a.Name > b.Name + } + case "-time": + return a.CreatedTime.After(b.CreatedTime) + case "time": + return a.CreatedTime.Before(b.CreatedTime) + } + } + + return a.CreatedTime.Before(b.CreatedTime) +} + +func (s TaskSorter) Swap(i, j int) { + s.Data[i], s.Data[j] = s.Data[j], s.Data[i] +} + type TaskRepository interface { Find(ctx context.Context, id string) (*Task, error) List(ctx context.Context, filter TaskFilter) ([]*Task, error) diff --git a/services/loader.go b/services/loader.go index 2d35b91..2226503 100644 --- a/services/loader.go +++ b/services/loader.go @@ -317,6 +317,8 @@ func (l *Loader) FindProject(ctx context.Context, id string) (*models.ProjectRes } } + result.SortTasks() + return result, nil } @@ -433,6 +435,8 @@ func (l *Loader) ListProjects(ctx context.Context, filter models.ProjectFilter) } results[i].Tasks = append(results[i].Tasks, taskResult) } + + results[i].SortTasks() } return results, nil diff --git a/svelte-ui/src/clients/stufflog.ts b/svelte-ui/src/clients/stufflog.ts index 65e3100..b7ed778 100644 --- a/svelte-ui/src/clients/stufflog.ts +++ b/svelte-ui/src/clients/stufflog.ts @@ -189,7 +189,7 @@ export class StufflogClient { return data.task; } - async listTasks({active, expiring}: TaskFilter): Promise { + async listTasks({active, expiring, sort}: TaskFilter): Promise { let queries = []; if (active != null) { queries.push(`active=${active}`); @@ -197,6 +197,9 @@ export class StufflogClient { if (expiring != null) { queries.push(`expiring=${expiring}`); } + if (sort != null) { + queries.push(`sort=${sort.join(",")}`) + } const query = queries.length > 0 ? `?${queries.join("&")}` : ""; diff --git a/svelte-ui/src/components/ProjectEntry.svelte b/svelte-ui/src/components/ProjectEntry.svelte index 590d742..e9676ac 100644 --- a/svelte-ui/src/components/ProjectEntry.svelte +++ b/svelte-ui/src/components/ProjectEntry.svelte @@ -1,5 +1,5 @@ {#if tasks.length > 0}

{header}

- {#each sortedTasks as task (task.id)} + {#each tasks as task (task.id)} p.id === task.id); + let fakeProject = fakeProjects.find(p => p.id === task.projectId); if (fakeProject == null) { fakeMap[task.projectId] = true; fakeProjects.push({ ...task.project, tasks: [task], + subtractions: [], }); } else { fakeProject.tasks.push(task);