feat: expand buckets

This commit is contained in:
kolaente
2025-01-24 11:01:26 +01:00
parent bc0c0b103f
commit 7f6cb1e06e
6 changed files with 140 additions and 40 deletions

View File

@@ -38,7 +38,7 @@ type Bucket struct {
// The project view this bucket belongs to.
ProjectViewID int64 `xorm:"bigint not null" json:"project_view_id" param:"view"`
// All tasks which belong to this bucket.
Tasks []*Task `xorm:"-" json:"tasks"`
Tasks []*Task `xorm:"-" json:"tasks,omitempty"`
// How many tasks can be at the same time on this board max
Limit int64 `xorm:"default 0" json:"limit" minimum:"0" valid:"range(0|9223372036854775807)"`
@@ -271,7 +271,7 @@ func GetTasksInBucketsForView(s *xorm.Session, view *ProjectView, projects []*Pr
taskMap[t.ID] = t
}
err = addMoreInfoToTasks(s, taskMap, auth, view)
err = addMoreInfoToTasks(s, taskMap, auth, view, opts.expand)
if err != nil {
return nil, err
}

View File

@@ -29,8 +29,8 @@ import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/api/pkg/web"
"xorm.io/builder"
"xorm.io/xorm"
)
@@ -175,6 +175,41 @@ var FavoritesPseudoProject = Project{
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects [get]
func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
prs, resultCount, totalItems, err := getAllRawProjects(s, a, search, page, perPage, p.IsArchived)
if err != nil {
return nil, 0, 0, err
}
/////////////////
// Add project details (favorite state, among other things)
err = addProjectDetails(s, prs, a)
if err != nil {
return
}
if p.Expand == ProjectExpandableRights {
var doer *user.User
doer, err = user.GetFromAuth(a)
if err != nil {
return
}
err = addMaxRightToProjects(s, prs, doer)
if err != nil {
return
}
} else {
for _, pr := range prs {
pr.MaxRight = RightUnknown
}
}
//////////////////////////
// Putting it all together
return prs, resultCount, totalItems, err
}
func getAllRawProjects(s *xorm.Session, a web.Auth, search string, page int, perPage int, isArchived bool) (projects []*Project, resultCount int, totalItems int64, err error) {
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
@@ -202,7 +237,7 @@ func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
user: doer,
page: page,
perPage: perPage,
getArchived: p.IsArchived,
getArchived: isArchived,
})
if err != nil {
return nil, 0, 0, err
@@ -220,27 +255,6 @@ func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
prs = append(prs, savedFiltersProject...)
}
/////////////////
// Add project details (favorite state, among other things)
err = addProjectDetails(s, prs, a)
if err != nil {
return
}
if p.Expand == ProjectExpandableRights {
err = addMaxRightToProjects(s, prs, doer)
if err != nil {
return
}
} else {
for _, pr := range prs {
pr.MaxRight = RightUnknown
}
}
//////////////////////////
// Putting it all together
return prs, resultCount, totalItems, err
}

View File

@@ -52,7 +52,9 @@ type TaskCollection struct {
// If set to `subtasks`, Vikunja will fetch only tasks which do not have subtasks and then in a
// second step, will fetch all of these subtasks. This may result in more tasks than the
// pagination limit being returned, but all subtasks will be present in the response.
Expand TaskCollectionExpandable `query:"expand" json:"-"`
// If set to `buckets`, the buckets of each task will be present in the response.
// You can set this multiple times with different values.
Expand []TaskCollectionExpandable `query:"expand" json:"-"`
isSavedFilter bool
@@ -63,15 +65,18 @@ type TaskCollection struct {
type TaskCollectionExpandable string
const TaskCollectionExpandSubtasks TaskCollectionExpandable = `subtasks`
const TaskCollectionExpandBuckets TaskCollectionExpandable = `buckets`
// Validate validates if the TaskCollectionExpandable value is valid.
func (t TaskCollectionExpandable) Validate() error {
switch t {
case TaskCollectionExpandSubtasks:
return nil
case TaskCollectionExpandBuckets:
return nil
}
return InvalidFieldErrorWithMessage([]string{"expand"}, "Expand must be one of the following values: subtasks")
return InvalidFieldErrorWithMessage([]string{"expand"}, "Expand must be one of the following values: subtasks, buckets")
}
func validateTaskField(fieldName string) error {
@@ -341,9 +346,11 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
return nil, 0, 0, err
}
err = tf.Expand.Validate()
if err != nil {
return nil, 0, 0, err
for _, expandValue := range tf.Expand {
err = expandValue.Validate()
if err != nil {
return nil, 0, 0, err
}
}
opts.search = search

View File

@@ -319,7 +319,15 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
distinct += ", task_positions.position"
}
if opts.expand == TaskCollectionExpandSubtasks {
var expandSubtasks = false
for _, expandable := range opts.expand {
if expandable == TaskCollectionExpandSubtasks {
expandSubtasks = true
break
}
}
if expandSubtasks {
cond = builder.And(cond, builder.IsNull{"task_relations.id"})
}
@@ -340,7 +348,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
if joinTaskBuckets {
query = query.Join("LEFT", "task_buckets", "task_buckets.task_id = tasks.id")
}
if opts.expand == TaskCollectionExpandSubtasks {
if expandSubtasks {
query = query.Join("LEFT", "task_relations", "tasks.id = task_relations.task_id and task_relations.relation_kind = 'parenttask'")
}
@@ -354,7 +362,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
}
// fetch subtasks when expanding
if opts.expand == TaskCollectionExpandSubtasks {
if expandSubtasks {
subtasks := []*Task{}
taskIDs := []int64{}
@@ -407,7 +415,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
if joinTaskBuckets {
queryCount = queryCount.Join("LEFT", "task_buckets", "task_buckets.task_id = tasks.id")
}
if opts.expand == TaskCollectionExpandSubtasks {
if expandSubtasks {
queryCount = queryCount.Join("LEFT", "task_relations", "tasks.id = task_relations.task_id and task_relations.relation_kind = 'parenttask'")
}
totalCount, err = queryCount.

View File

@@ -119,6 +119,9 @@ type Task struct {
// Can be used to move a task between buckets. In that case, the new bucket must be in the same view as the old one.
BucketID int64 `xorm:"-" json:"bucket_id"`
// All buckets across all views this task is part of. Only present when fetching tasks with the `expand` parameter set to `buckets`.
Buckets []*Bucket `xorm:"-" json:"buckets,omitempty"`
// The position of the task - any task project can be sorted as usual by this parameter.
// When accessing tasks via views with buckets, this is primarily used to sort them based on a range.
// Positions are always saved per view. They will automatically be set if you request the tasks through a view
@@ -185,7 +188,7 @@ type taskSearchOptions struct {
filterTimezone string
isSavedFilter bool
projectIDs []int64
expand TaskCollectionExpandable
expand []TaskCollectionExpandable
}
// ReadAll is a dummy function to still have that endpoint documented
@@ -345,7 +348,7 @@ func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts
taskMap[t.ID] = t
}
err = addMoreInfoToTasks(s, taskMap, a, view)
err = addMoreInfoToTasks(s, taskMap, a, view, opts.expand)
if err != nil {
return nil, 0, 0, err
}
@@ -422,7 +425,7 @@ func GetTasksByUIDs(s *xorm.Session, uids []string, a web.Auth) (tasks []*Task,
taskMap[t.ID] = t
}
err = addMoreInfoToTasks(s, taskMap, a, nil)
err = addMoreInfoToTasks(s, taskMap, a, nil, nil)
return
}
@@ -561,9 +564,59 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]
return
}
func addBucketsToTasks(s *xorm.Session, a web.Auth, taskIDs []int64, taskMap map[int64]*Task) (err error) {
if len(taskIDs) == 0 {
return nil
}
taskBuckets := []*TaskBucket{}
err = s.
In("task_id", taskIDs).
Find(&taskBuckets)
if err != nil {
return err
}
// We need to fetch all projects for that user to make sure they only
// get to see buckets that they have permission to see.
projectIDs := []int64{}
allProjects, _, _, err := getAllRawProjects(s, a, "", 0, -1, false)
if err != nil {
return err
}
for _, project := range allProjects {
projectIDs = append(projectIDs, project.ID)
}
buckets := make(map[int64]*Bucket)
err = s.
Where(builder.In("id", builder.Select("bucket_id").
From("task_buckets").
Where(builder.In("task_id", taskIDs)))).
And(builder.In("project_view_id", builder.Select("id").
From("project_views").
Where(builder.In("project_id", projectIDs)))).
Find(&buckets)
if err != nil {
return err
}
for _, tb := range taskBuckets {
if taskMap[tb.TaskID].Buckets == nil {
taskMap[tb.TaskID].Buckets = []*Bucket{}
}
if bucket, exists := buckets[tb.BucketID]; exists {
taskMap[tb.TaskID].Buckets = append(taskMap[tb.TaskID].Buckets, bucket)
}
}
return nil
}
// This function takes a map with pointers and returns a slice with pointers to tasks
// It adds more stuff like assignees/labels/etc to a bunch of tasks
func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth, view *ProjectView) (err error) {
func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth, view *ProjectView, expand []TaskCollectionExpandable) (err error) {
// No need to iterate over users and stuff if the project doesn't have tasks
if len(taskMap) == 0 {
@@ -632,6 +685,23 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth, vi
}
}
if expand != nil {
expanded := make(map[TaskCollectionExpandable]bool)
for _, expandable := range expand {
if expanded[expandable] {
continue
}
if expandable == TaskCollectionExpandBuckets {
err = addBucketsToTasks(s, a, taskIDs, taskMap)
if err != nil {
return err
}
}
expanded[expandable] = true
}
}
// Add all objects to their tasks
for _, task := range taskMap {
@@ -1618,7 +1688,8 @@ func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) {
taskMap := make(map[int64]*Task, 1)
taskMap[t.ID] = t
err = addMoreInfoToTasks(s, taskMap, a, nil)
// TODO add expand here as well
err = addMoreInfoToTasks(s, taskMap, a, nil, nil)
if err != nil {
return
}

View File

@@ -270,7 +270,7 @@ func reindexTasksInTypesense(s *xorm.Session, tasks map[int64]*Task) (err error)
return
}
err = addMoreInfoToTasks(s, tasks, &user.User{ID: 1}, nil)
err = addMoreInfoToTasks(s, tasks, &user.User{ID: 1}, nil, nil)
if err != nil {
return fmt.Errorf("could not fetch more task info: %s", err.Error())
}