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 <k@knt.li>
This commit is contained in:
Tink 2026-03-19 10:18:11 +01:00 committed by GitHub
parent 7b6b432301
commit d11f097eee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 15 additions and 6 deletions

View File

@ -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 != "" {

View File

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

View File

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