add project groups to backend.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/gissleh/stufflog/internal/slerrors"
|
||||
"github.com/gissleh/stufflog/models"
|
||||
"github.com/gissleh/stufflog/services"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -30,6 +31,12 @@ func Project(g *gin.RouterGroup, db database.Database) {
|
||||
favorite := setting == "true"
|
||||
filter.Favorite = &favorite
|
||||
}
|
||||
if setting := c.Query("ungrouped"); setting != "" {
|
||||
filter.Ungrouped = setting == "true"
|
||||
}
|
||||
if setting := c.Query("groups"); setting != "" {
|
||||
filter.ProjectGroupIDs = strings.Split(setting, ",")
|
||||
}
|
||||
|
||||
return l.ListProjects(c, filter)
|
||||
}))
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gissleh/stufflog/database"
|
||||
"github.com/gissleh/stufflog/internal/auth"
|
||||
"github.com/gissleh/stufflog/internal/generate"
|
||||
"github.com/gissleh/stufflog/internal/slerrors"
|
||||
"github.com/gissleh/stufflog/models"
|
||||
"github.com/gissleh/stufflog/services"
|
||||
)
|
||||
|
||||
func ProjectGroup(g *gin.RouterGroup, db database.Database) {
|
||||
l := services.Loader{DB: db}
|
||||
|
||||
g.GET("/", handler("projectGroups", func(c *gin.Context) (interface{}, error) {
|
||||
return l.ListProjectGroups(c)
|
||||
}))
|
||||
|
||||
g.GET("/:id", handler("projectGroup", func(c *gin.Context) (interface{}, error) {
|
||||
return l.FindProject(c, c.Param("id"))
|
||||
}))
|
||||
|
||||
g.POST("/", handler("projectGroup", func(c *gin.Context) (interface{}, error) {
|
||||
group := models.ProjectGroup{}
|
||||
err := c.BindJSON(&group)
|
||||
if err != nil {
|
||||
return nil, slerrors.BadRequest("Invalid JSON")
|
||||
}
|
||||
if group.Abbreviation == "" || group.Name == "" {
|
||||
return nil, slerrors.BadRequest("Abbreviation and name cannot be left empty.")
|
||||
}
|
||||
|
||||
group.ID = generate.ProjectGroupID()
|
||||
group.UserID = auth.UserID(c)
|
||||
if group.CategoryNames == nil {
|
||||
group.CategoryNames = make(map[string]string)
|
||||
}
|
||||
|
||||
err = db.ProjectGroups().Insert(c.Request.Context(), group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &group, nil
|
||||
}))
|
||||
|
||||
g.PUT("/:id", handler("projectGroup", func(c *gin.Context) (interface{}, error) {
|
||||
update := models.ProjectGroupUpdate{}
|
||||
err := c.BindJSON(&update)
|
||||
if err != nil {
|
||||
return nil, slerrors.BadRequest("Invalid JSON")
|
||||
}
|
||||
if (update.Name != nil && *update.Name == "") || (update.Abbreviation != nil && *update.Abbreviation == "") {
|
||||
return nil, slerrors.BadRequest("Abbreviation and name cannot be left empty.")
|
||||
}
|
||||
|
||||
group, err := db.ProjectGroups().Find(c, c.Param("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if group.UserID != auth.UserID(c) {
|
||||
return nil, slerrors.NotFound("Project group")
|
||||
}
|
||||
|
||||
group.Update(update)
|
||||
|
||||
err = db.ProjectGroups().Update(c.Request.Context(), *group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}))
|
||||
|
||||
g.DELETE("/:id", handler("project", func(c *gin.Context) (interface{}, error) {
|
||||
group, err := l.FindProjectGroup(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(group.Projects) > 0 {
|
||||
return nil, slerrors.Forbidden("You cannot delete non-empty project group.")
|
||||
}
|
||||
|
||||
err = db.ProjectGroups().Delete(c.Request.Context(), group.ProjectGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}))
|
||||
}
|
||||
@@ -40,6 +40,7 @@ func main() {
|
||||
api.Group(server.Group("/api/group"), db)
|
||||
api.Item(server.Group("/api/item"), db)
|
||||
api.Project(server.Group("/api/project"), db)
|
||||
api.ProjectGroup(server.Group("/api/projectgroup"), db)
|
||||
api.Task(server.Group("/api/task"), db)
|
||||
api.Log(server.Group("/api/log"), db)
|
||||
api.Goal(server.Group("/api/goal"), db)
|
||||
|
||||
@@ -45,6 +45,7 @@ func main() {
|
||||
api.Group(server.Group("/api/group"), db)
|
||||
api.Item(server.Group("/api/item"), db)
|
||||
api.Project(server.Group("/api/project"), db)
|
||||
api.ProjectGroup(server.Group("/api/projectgroup"), db)
|
||||
api.Task(server.Group("/api/task"), db)
|
||||
api.Log(server.Group("/api/log"), db)
|
||||
api.Goal(server.Group("/api/goal"), db)
|
||||
|
||||
@@ -16,6 +16,7 @@ type Database interface {
|
||||
Logs() models.LogRepository
|
||||
Projects() models.ProjectRepository
|
||||
Tasks() models.TaskRepository
|
||||
ProjectGroups() models.ProjectGroupRepository
|
||||
}
|
||||
|
||||
func Open(ctx context.Context, driver string, connect string) (Database, error) {
|
||||
|
||||
@@ -36,6 +36,10 @@ func (d *Database) Tasks() models.TaskRepository {
|
||||
return &taskRepository{db: d.db}
|
||||
}
|
||||
|
||||
func (d *Database) ProjectGroups() models.ProjectGroupRepository {
|
||||
return &projectGroupRepository{db: d.db}
|
||||
}
|
||||
|
||||
func Setup(ctx context.Context, connect string) (*Database, error) {
|
||||
db, err := sqlx.ConnectContext(ctx, "postgres", connect)
|
||||
if err != nil {
|
||||
|
||||
@@ -54,6 +54,11 @@ func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilte
|
||||
if filter.Favorite != nil {
|
||||
sq = sq.Where(squirrel.Eq{"favorite": *filter.Favorite})
|
||||
}
|
||||
if filter.Ungrouped == true {
|
||||
sq = sq.Where("project_group_id is NULL")
|
||||
} else if filter.ProjectGroupIDs != nil {
|
||||
sq = sq.Where(squirrel.Eq{"project_group_id": filter.ProjectGroupIDs})
|
||||
}
|
||||
|
||||
sq = sq.OrderBy("active", "created_time DESC")
|
||||
|
||||
@@ -86,9 +91,9 @@ func (r *projectRepository) Insert(ctx context.Context, project models.Project)
|
||||
|
||||
_, err := r.db.NamedExecContext(ctx, `
|
||||
INSERT INTO project(
|
||||
project_id, user_id, name, description, icon, active, created_time, start_time, end_time, subtract_amount, status_tag, favorite, tags
|
||||
project_id, user_id, project_group_id, name, description, icon, active, created_time, start_time, end_time, subtract_amount, status_tag, favorite, tags
|
||||
) VALUES (
|
||||
:project_id, :user_id, :name, :description, :icon, :active, :created_time, :start_time, :end_time, :subtract_amount, :status_tag, :favorite, :tags
|
||||
:project_id, :user_id, :project_group_id, :name, :description, :icon, :active, :created_time, :start_time, :end_time, :subtract_amount, :status_tag, :favorite, :tags
|
||||
)
|
||||
`, toProjectDBO(project))
|
||||
if err != nil {
|
||||
@@ -107,6 +112,7 @@ func (r *projectRepository) Update(ctx context.Context, project models.Project)
|
||||
UPDATE project SET
|
||||
name = :name,
|
||||
description = :description,
|
||||
project_group_id = :project_group_id,
|
||||
icon = :icon,
|
||||
active = :active,
|
||||
start_time = :start_time,
|
||||
@@ -144,6 +150,7 @@ func (r *projectRepository) Delete(ctx context.Context, project models.Project)
|
||||
type projectDBO struct {
|
||||
ID string `json:"id" db:"project_id"`
|
||||
UserID string `json:"-" db:"user_id"`
|
||||
ProjectGroupID *string `json:"projectGroupID" db:"project_group_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Icon string `json:"icon" db:"icon"`
|
||||
@@ -161,6 +168,7 @@ func toProjectDBO(project models.Project) *projectDBO {
|
||||
return &projectDBO{
|
||||
ID: project.ID,
|
||||
UserID: project.UserID,
|
||||
ProjectGroupID: project.GroupID,
|
||||
Name: project.Name,
|
||||
Description: project.Description,
|
||||
Icon: project.Icon,
|
||||
@@ -179,6 +187,7 @@ func (d *projectDBO) ToProject() *models.Project {
|
||||
return &models.Project{
|
||||
ID: d.ID,
|
||||
UserID: d.UserID,
|
||||
GroupID: d.ProjectGroupID,
|
||||
Name: d.Name,
|
||||
Description: d.Description,
|
||||
Icon: d.Icon,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/gissleh/stufflog/internal/slerrors"
|
||||
"github.com/gissleh/stufflog/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type projectGroupRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func (r *projectGroupRepository) Find(ctx context.Context, id string) (*models.ProjectGroup, error) {
|
||||
res := projectGroupDBO{}
|
||||
err := r.db.GetContext(ctx, &res, "SELECT * FROM project_group WHERE project_group_id=$1", id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, slerrors.NotFound("Project group")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.ToProjectGroup(), nil
|
||||
}
|
||||
|
||||
func (r *projectGroupRepository) List(ctx context.Context, filter models.ProjectGroupFilter) ([]*models.ProjectGroup, error) {
|
||||
res := make([]*projectGroupDBO, 0, 16)
|
||||
err := r.db.SelectContext(ctx, &res, "SELECT * FROM project_group WHERE user_id=$1", filter.UserID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return []*models.ProjectGroup{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res2 := make([]*models.ProjectGroup, 0, len(res))
|
||||
for _, pdo := range res {
|
||||
res2 = append(res2, pdo.ToProjectGroup())
|
||||
}
|
||||
|
||||
return res2, nil
|
||||
}
|
||||
|
||||
func (r *projectGroupRepository) Insert(ctx context.Context, group models.ProjectGroup) error {
|
||||
_, err := r.db.NamedExecContext(ctx, `
|
||||
INSERT INTO project_group (
|
||||
project_group_id, user_id, name, abbreviation, description, category_names
|
||||
) VALUES (
|
||||
:project_group_id, :user_id, :name, :abbreviation, :description, :category_names
|
||||
)
|
||||
`, toProjectGroupDBO(group))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *projectGroupRepository) Update(ctx context.Context, group models.ProjectGroup) error {
|
||||
if group.CategoryNames == nil {
|
||||
group.CategoryNames = make(map[string]string)
|
||||
}
|
||||
|
||||
_, err := r.db.NamedExecContext(ctx, `
|
||||
UPDATE project_group SET
|
||||
name = :name,
|
||||
abbreviation = :abbreviation,
|
||||
description = :description,
|
||||
category_names = :category_names
|
||||
WHERE project_group_id = :project_group_id
|
||||
`, toProjectGroupDBO(group))
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *projectGroupRepository) Delete(ctx context.Context, group models.ProjectGroup) error {
|
||||
_, err := r.db.ExecContext(ctx, `DELETE FROM project_group WHERE project_group_id=$1`, group.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.db.ExecContext(ctx, "UPDATE project SET project_group_id=NULL where project_group_id=$1", group.ID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type projectGroupDBO struct {
|
||||
ID string `db:"project_group_id"`
|
||||
UserID string `db:"user_id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
Abbreviation string `db:"abbreviation"`
|
||||
CategoryNames jsonMap `db:"category_names"`
|
||||
}
|
||||
|
||||
func (dbo *projectGroupDBO) ToProjectGroup() *models.ProjectGroup {
|
||||
return &models.ProjectGroup{
|
||||
ID: dbo.ID,
|
||||
UserID: dbo.UserID,
|
||||
Name: dbo.Name,
|
||||
Description: dbo.Description,
|
||||
Abbreviation: dbo.Abbreviation,
|
||||
CategoryNames: dbo.CategoryNames,
|
||||
}
|
||||
}
|
||||
|
||||
func toProjectGroupDBO(group models.ProjectGroup) projectGroupDBO {
|
||||
return projectGroupDBO{
|
||||
ID: group.ID,
|
||||
UserID: group.UserID,
|
||||
Name: group.Name,
|
||||
Description: group.Description,
|
||||
Abbreviation: group.Abbreviation,
|
||||
CategoryNames: group.CategoryNames,
|
||||
}
|
||||
}
|
||||
|
||||
type jsonMap map[string]string
|
||||
|
||||
func (m jsonMap) Value() (driver.Value, error) {
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func (m *jsonMap) Scan(value interface{}) error {
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("type assertion to []byte failed")
|
||||
}
|
||||
|
||||
return json.Unmarshal(b, m)
|
||||
}
|
||||
@@ -39,6 +39,10 @@ func ProjectID() string {
|
||||
return id("P", 16)
|
||||
}
|
||||
|
||||
func ProjectGroupID() string {
|
||||
return id("C", 16)
|
||||
}
|
||||
|
||||
func TaskID() string {
|
||||
return id("T", 16)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE project_group (
|
||||
project_group_id CHAR(16) NOT NULL PRIMARY KEY,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
abbreviation TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
category_names JSONB NOT NULL
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE IF EXISTS project_group;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,11 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE project
|
||||
ADD COLUMN project_group_id CHAR(16) DEFAULT NULL;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE project
|
||||
DROP COLUMN IF EXISTS project_group_id;
|
||||
-- +goose StatementEnd
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
type Project struct {
|
||||
ID string `json:"id" db:"project_id"`
|
||||
UserID string `json:"-" db:"user_id"`
|
||||
GroupID *string `json:"groupId" db:"project_group_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Icon string `json:"icon" db:"icon"`
|
||||
@@ -22,6 +23,12 @@ type Project struct {
|
||||
}
|
||||
|
||||
func (project *Project) Update(update ProjectUpdate) {
|
||||
if update.GroupID != nil {
|
||||
project.GroupID = update.GroupID
|
||||
if *project.GroupID == "" {
|
||||
project.GroupID = nil
|
||||
}
|
||||
}
|
||||
if update.Name != nil {
|
||||
project.Name = *update.Name
|
||||
}
|
||||
@@ -73,6 +80,7 @@ func (project *Project) Update(update ProjectUpdate) {
|
||||
}
|
||||
|
||||
type ProjectUpdate struct {
|
||||
GroupID *string `json:"groupId"`
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Icon *string `json:"icon"`
|
||||
@@ -95,11 +103,13 @@ type ProjectResult struct {
|
||||
|
||||
type ProjectFilter struct {
|
||||
UserID string
|
||||
ProjectGroupIDs []string
|
||||
Active *bool
|
||||
Favorite *bool
|
||||
Expiring bool
|
||||
IncludeSemiActive bool
|
||||
IDs []string
|
||||
Ungrouped bool
|
||||
}
|
||||
|
||||
type ProjectRepository interface {
|
||||
|
||||
+57
-2
@@ -3,24 +3,79 @@ package models
|
||||
import "context"
|
||||
|
||||
type ProjectGroup struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ID string `json:"id" db:"project_group_id"`
|
||||
UserID string `json:"-" db:"user_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Abbreviation string `json:"abbreviation" db:"abbreviation"`
|
||||
CategoryNames map[string]string `json:"categoryNames" db:"category_names"`
|
||||
}
|
||||
|
||||
func (group *ProjectGroup) Update(update ProjectGroupUpdate) {
|
||||
for key, value := range update.SetCategoryNames {
|
||||
if value == "" {
|
||||
delete(group.CategoryNames, key)
|
||||
} else {
|
||||
group.CategoryNames[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if update.Name != nil && *update.Name != "" {
|
||||
group.Name = *update.Name
|
||||
}
|
||||
if update.Abbreviation != nil && *update.Abbreviation != "" {
|
||||
group.Abbreviation = *update.Abbreviation
|
||||
}
|
||||
if update.Description != nil {
|
||||
group.Description = *update.Description
|
||||
}
|
||||
}
|
||||
|
||||
type ProjectGroupUpdate struct {
|
||||
Name *string `json:"name"`
|
||||
Abbreviation *string `json:"abbreviation"`
|
||||
Description *string `json:"description"`
|
||||
SetCategoryNames map[string]string `json:"setCategoryNames"`
|
||||
ResetCategoryName *string `json:"resetCategoryName"`
|
||||
}
|
||||
|
||||
type ProjectGroupFilter struct {
|
||||
UserID string `json:"userId"`
|
||||
}
|
||||
|
||||
type ProjectGroupResult struct {
|
||||
ProjectGroup
|
||||
|
||||
Projects []*ProjectResult `json:"projects"`
|
||||
ProjectCounts map[string]int `json:"projectCounts"`
|
||||
TaskCounts map[string]int `json:"taskCounts"`
|
||||
}
|
||||
|
||||
func (r *ProjectGroupResult) RecountTasks() {
|
||||
r.ProjectCounts = make(map[string]int)
|
||||
r.TaskCounts = make(map[string]int)
|
||||
|
||||
r.ProjectCounts["total"] = 0
|
||||
r.TaskCounts["total"] = 0
|
||||
|
||||
for _, project := range r.Projects {
|
||||
r.ProjectCounts["total"] += 1
|
||||
if project.StatusTag == nil {
|
||||
r.ProjectCounts["active"] += 1
|
||||
} else {
|
||||
r.ProjectCounts[*project.StatusTag] += 1
|
||||
}
|
||||
|
||||
for _, task := range project.Tasks {
|
||||
r.TaskCounts["total"] += 1
|
||||
if task.StatusTag == nil {
|
||||
r.TaskCounts["active"] += 1
|
||||
} else {
|
||||
r.TaskCounts[*task.StatusTag] += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ProjectGroupRepository interface {
|
||||
Find(ctx context.Context, id string) (*ProjectGroup, error)
|
||||
List(ctx context.Context, filter ProjectGroupFilter) ([]*ProjectGroup, error)
|
||||
|
||||
@@ -373,6 +373,89 @@ func (l *Loader) ListProjects(ctx context.Context, filter models.ProjectFilter)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (l *Loader) FindProjectGroup(ctx context.Context, id string) (*models.ProjectGroupResult, error) {
|
||||
group, err := l.DB.ProjectGroups().Find(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projects, err := l.ListProjects(ctx, models.ProjectFilter{
|
||||
UserID: auth.UserID(ctx),
|
||||
ProjectGroupIDs: []string{group.ID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &models.ProjectGroupResult{
|
||||
ProjectGroup: *group,
|
||||
Projects: projects,
|
||||
}
|
||||
result.RecountTasks()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (l *Loader) ListProjectGroups(ctx context.Context) ([]*models.ProjectGroupResult, error) {
|
||||
groups, err := l.DB.ProjectGroups().List(ctx, models.ProjectGroupFilter{UserID: auth.UserID(ctx)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
ids = append(ids, group.ID)
|
||||
}
|
||||
|
||||
projects, err := l.ListProjects(ctx, models.ProjectFilter{
|
||||
UserID: auth.UserID(ctx),
|
||||
ProjectGroupIDs: ids,
|
||||
})
|
||||
|
||||
results := make([]*models.ProjectGroupResult, 0, len(groups)+1)
|
||||
|
||||
for _, group := range groups {
|
||||
matchingProjects := make([]*models.ProjectResult, 0, len(projects)/len(groups))
|
||||
for _, project := range projects {
|
||||
if *project.GroupID == group.ID {
|
||||
matchingProjects = append(matchingProjects, project)
|
||||
}
|
||||
}
|
||||
|
||||
result := &models.ProjectGroupResult{
|
||||
ProjectGroup: *group,
|
||||
Projects: matchingProjects,
|
||||
}
|
||||
result.RecountTasks()
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
ungroupedProjects, err := l.ListProjects(ctx, models.ProjectFilter{
|
||||
UserID: auth.UserID(ctx),
|
||||
Ungrouped: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ungroupedProjects) > 0 {
|
||||
result := &models.ProjectGroupResult{
|
||||
ProjectGroup: models.ProjectGroup{
|
||||
ID: "META_UNGROUPED",
|
||||
Name: "Ungrouped",
|
||||
Abbreviation: "OTHER",
|
||||
CategoryNames: map[string]string{},
|
||||
},
|
||||
Projects: ungroupedProjects,
|
||||
}
|
||||
result.RecountTasks()
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (l *Loader) FindTask(ctx context.Context, id string) (*models.TaskResult, error) {
|
||||
task, err := l.DB.Tasks().Find(ctx, id)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user