1
0

feat(tasks): expand subtasks (#2345)

This change adds a parameter to expand subtasks - if provided, Vikunja will ensure all subtasks are present in the results list.

Resolves https://community.vikunja.io/t/subtasks-show-on-different-pages/2292
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2345
Co-authored-by: kolaente <k@knt.li>
Co-committed-by: kolaente <k@knt.li>
This commit is contained in:
kolaente 2024-06-04 10:27:23 +00:00 committed by konrad
parent a38e768895
commit 48676050d7
7 changed files with 96 additions and 3 deletions

View File

@ -12,6 +12,7 @@ export interface TaskFilterParams {
filter_timezone?: string,
s: string,
per_page?: number,
expand?: 'subtasks' | null,
}
export function getDefaultTaskFilterParams(): TaskFilterParams {
@ -22,6 +23,7 @@ export function getDefaultTaskFilterParams(): TaskFilterParams {
filter_include_nulls: false,
filter_timezone: '',
s: '',
expand: 'subtasks',
}
}

View File

@ -45,12 +45,21 @@ type TaskCollection struct {
// If set to true, the result will also include null values
FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`
// 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:"-"`
isSavedFilter bool
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
type TaskCollectionExpandable string
const TaskCollectionExpandSubtasks TaskCollectionExpandable = `subtasks`
func validateTaskField(fieldName string) error {
switch fieldName {
case
@ -181,6 +190,7 @@ func getRelevantProjectsFromCollection(s *xorm.Session, a web.Auth, tf *TaskColl
// @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. You can only set this to `subtasks`."
// @Security JWTKeyAuth
// @Success 200 {array} models.Task "The tasks"
// @Failure 500 {object} models.Message "Internal error"
@ -251,6 +261,7 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
opts.search = search
opts.page = page
opts.perPage = perPage
opts.expand = tf.Expand
if view != nil {
var hasOrderByPosition bool

View File

@ -266,6 +266,10 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
distinct += ", task_positions.position"
}
if opts.expand == TaskCollectionExpandSubtasks {
cond = builder.And(cond, builder.IsNull{"task_relations.id"})
}
query := d.s.
Distinct(distinct).
Where(cond)
@ -283,6 +287,9 @@ 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 {
query = query.Join("LEFT", "task_relations", "tasks.id = task_relations.task_id and task_relations.relation_kind = 'parenttask'")
}
tasks = []*Task{}
err = query.
@ -292,10 +299,63 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
return nil, totalCount, err
}
// fetch subtasks when expanding
if opts.expand == TaskCollectionExpandSubtasks {
subtasks := []*Task{}
taskIDs := []int64{}
for _, task := range tasks {
taskIDs = append(taskIDs, task.ID)
}
inQuery := builder.Dialect(db.GetDialect()).
Select("*").
From("task_relations").
Where(builder.In("task_id", taskIDs))
inSQL, inArgs, err := inQuery.ToSQL()
if err != nil {
return nil, totalCount, err
}
inSQL = strings.TrimPrefix(inSQL, "SELECT * FROM task_relations WHERE")
err = d.s.SQL(`SELECT * FROM tasks WHERE id IN (WITH RECURSIVE sub_tasks AS (
SELECT task_id,
other_task_id,
relation_kind,
created_by_id,
created
FROM task_relations
WHERE `+inSQL+`
AND relation_kind = '`+string(RelationKindSubtask)+`'
UNION ALL
SELECT tr.task_id,
tr.other_task_id,
tr.relation_kind,
tr.created_by_id,
tr.created
FROM task_relations tr
INNER JOIN
sub_tasks st ON tr.task_id = st.other_task_id
WHERE tr.relation_kind = '`+string(RelationKindSubtask)+`')
SELECT other_task_id
FROM sub_tasks)`, inArgs...).Find(&subtasks)
if err != nil {
return nil, totalCount, err
}
tasks = append(tasks, subtasks...)
}
queryCount := d.s.Where(cond)
if joinTaskBuckets {
queryCount = queryCount.Join("LEFT", "task_buckets", "task_buckets.task_id = tasks.id")
}
if opts.expand == TaskCollectionExpandSubtasks {
queryCount = queryCount.Join("LEFT", "task_relations", "tasks.id = task_relations.task_id and task_relations.relation_kind = 'parenttask'")
}
totalCount, err = queryCount.
Select("count(DISTINCT tasks.id)").
Count(&Task{})

View File

@ -176,6 +176,7 @@ type taskSearchOptions struct {
filter string
filterTimezone string
projectIDs []int64
expand TaskCollectionExpandable
}
// ReadAll is a dummy function to still have that endpoint documented

View File

@ -1,4 +1,5 @@
// Package swagger Code generated by swaggo/swag. DO NOT EDIT
// Code generated by swaggo/swag. DO NOT EDIT.
package swagger
import "github.com/swaggo/swag"
@ -2497,6 +2498,12 @@ const docTemplate = `{
"description": "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` + "`" + `.",
"name": "filter_include_nulls",
"in": "query"
},
{
"type": "string",
"description": "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. You can only set this to ` + "`" + `subtasks` + "`" + `.",
"name": "expand",
"in": "query"
}
],
"responses": {
@ -9641,8 +9648,6 @@ var SwaggerInfo = &swag.Spec{
Description: "# Pagination\nEvery endpoint capable of pagination will return two headers:\n* `x-pagination-total-pages`: The total number of available pages for this request\n* `x-pagination-result-count`: The number of items returned for this request.\n# Rights\nAll endpoints which return a single item (project, task, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.\nThis can be used to show or hide ui elements based on the rights the user has.\n# Errors\nAll errors have an error code and a human-readable error message in addition to the http status code. You should always check for the status code in the response, not only the http status code.\nDue to limitations in the swagger library we're using for this document, only one error per http status code is documented here. Make sure to check the [error docs](https://vikunja.io/docs/errors/) in Vikunja's documentation for a full list of available error codes.\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer <jwt-token>`-header to authenticate successfully.\n\n**API Token:** You can create scoped API tokens for your user and use the token to make authenticated requests in the context of that user. The token must be provided via an `Authorization: Bearer <token>` header, similar to jwt auth. See the documentation for the `api` group to manage token creation and revocation.\n\n**BasicAuth:** Only used when requesting tasks via CalDAV.\n<!-- ReDoc-Inject: <security-definitions> -->",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {

View File

@ -2489,6 +2489,12 @@
"description": "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`.",
"name": "filter_include_nulls",
"in": "query"
},
{
"type": "string",
"description": "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. You can only set this to `subtasks`.",
"name": "expand",
"in": "query"
}
],
"responses": {

View File

@ -3336,6 +3336,14 @@ paths:
in: query
name: filter_include_nulls
type: string
- description: 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. You can only set this
to `subtasks`.
in: query
name: expand
type: string
produces:
- application/json
responses: