From d11f097eee800ad50195d35188f085bc6f01d3bf Mon Sep 17 00:00:00 2001 From: Tink Date: Thu, 19 Mar 2026 10:18:11 +0100 Subject: [PATCH] fix(tasks): support both expand and expand[] query parameter formats (#2415) The `expand` query parameter only supported the `expand[]=foo` array format, but the swagger docs described it as a plain string parameter. This adds support for both formats (`expand=foo` and `expand[]=foo`), matching the existing pattern used by `sort_by` and `order_by` parameters. Closes #2408 --------- Co-authored-by: kolaente --- pkg/models/task_collection.go | 12 +++++++++--- pkg/models/tasks.go | 8 +++++--- pkg/models/tasks_permissions.go | 1 + 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index c4841804e..06a1799e1 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -56,7 +56,8 @@ type TaskCollection struct { // If set to `reactions`, the reactions of each task will be present in the response. // If set to `comments`, the first 50 comments of each task will be present in the response. // You can set this multiple times with different values. - Expand []TaskCollectionExpandable `query:"expand[]" json:"-"` + Expand []TaskCollectionExpandable `query:"expand" json:"-"` + ExpandArr []TaskCollectionExpandable `query:"expand[]" json:"-"` isSavedFilter bool @@ -114,6 +115,10 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection, projectView *ProjectVie tf.OrderBy = append(tf.OrderBy, tf.OrderByArr...) } + if len(tf.ExpandArr) > 0 { + tf.Expand = append(tf.Expand, tf.ExpandArr...) + } + var sort = make([]*sortParam, 0, len(tf.SortBy)) for i, s := range tf.SortBy { param := &sortParam{ @@ -241,7 +246,7 @@ func getFilterValueForBucketFilter(filter string, view *ProjectView) (newFilter // @Param filter query string false "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature." // @Param filter_timezone query string false "The time zone which should be used for date match (statements like "now" resolve to different actual times)" // @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`." -// @Param expand query array false "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. If set to `buckets`, the buckets of each task will be present in the response. If set to `reactions`, the reactions of each task will be present in the response. If set to `comments`, the first 50 comments of each task will be present in the response. You can set this multiple times with different values." +// @Param expand query string false "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. If set to `buckets`, the buckets of each task will be present in the response. If set to `reactions`, the reactions of each task will be present in the response. If set to `comments`, the first 50 comments of each task will be present in the response. You can set this multiple times with different values." // @Security JWTKeyAuth // @Success 200 {array} models.Task "The tasks" // @Failure 500 {object} models.Message "Internal error" @@ -292,7 +297,8 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa tc.ProjectViewID = tf.ProjectViewID tc.ProjectID = tf.ProjectID tc.isSavedFilter = true - tc.Expand = tf.Expand + tc.Expand = append(tf.Expand, tf.ExpandArr...) + tc.ExpandArr = nil if tf.Filter != "" { if tc.Filter != "" { diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 6eb4f8746..a555b3cce 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -129,7 +129,8 @@ type Task struct { CommentCount *int64 `xorm:"-" json:"comment_count,omitempty"` // Behaves exactly the same as with the TaskCollection.Expand parameter - Expand []TaskCollectionExpandable `xorm:"-" json:"-" query:"expand[]"` + Expand []TaskCollectionExpandable `xorm:"-" json:"-" query:"expand"` + ExpandArr []TaskCollectionExpandable `xorm:"-" json:"-" query:"expand[]"` // 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. @@ -215,7 +216,7 @@ type taskSearchOptions struct { // @Param filter query string false "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature." // @Param filter_timezone query string false "The time zone which should be used for date match (statements like "now" resolve to different actual times)" // @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`." -// @Param expand query []string false "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. If set to `buckets`, the buckets of each task will be present in the response. If set to `reactions`, the reactions of each task will be present in the response. If set to `comments`, the first 50 comments of each task will be present in the response. You can set this multiple times with different values." +// @Param expand query string false "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. If set to `buckets`, the buckets of each task will be present in the response. If set to `reactions`, the reactions of each task will be present in the response. If set to `comments`, the first 50 comments of each task will be present in the response. You can set this multiple times with different values." // @Security JWTKeyAuth // @Success 200 {array} models.Task "The tasks" // @Failure 500 {object} models.Message "Internal error" @@ -1836,7 +1837,7 @@ func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) { // @Accept json // @Produce json // @Param id path int true "The task ID" -// @Param expand query []string false "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. If set to `buckets`, the buckets of each task will be present in the response. If set to `reactions`, the reactions of each task will be present in the response. If set to `comments`, the first 50 comments of each task will be present in the response. You can set this multiple times with different values." +// @Param expand query string false "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. If set to `buckets`, the buckets of each task will be present in the response. If set to `reactions`, the reactions of each task will be present in the response. If set to `comments`, the first 50 comments of each task will be present in the response. You can set this multiple times with different values." // @Security JWTKeyAuth // @Success 200 {object} models.Task "The task" // @Failure 404 {object} models.Message "Task not found" @@ -1844,6 +1845,7 @@ func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) { // @Router /tasks/{id} [get] func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) { + t.Expand = append(t.Expand, t.ExpandArr...) expand := t.Expand *t, err = GetTaskByIDSimple(s, t.ID) if err != nil { diff --git a/pkg/models/tasks_permissions.go b/pkg/models/tasks_permissions.go index cfae002f1..bd3ef778f 100644 --- a/pkg/models/tasks_permissions.go +++ b/pkg/models/tasks_permissions.go @@ -40,6 +40,7 @@ func (t *Task) CanCreate(s *xorm.Session, a web.Auth) (bool, error) { // CanRead determines if a user can read a task func (t *Task) CanRead(s *xorm.Session, a web.Auth) (canRead bool, maxPermission int, err error) { + t.Expand = append(t.Expand, t.ExpandArr...) expand := t.Expand // Get the task, error out if it doesn't exist *t, err = GetTaskByIDSimple(s, t.ID)