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:
parent
a38e768895
commit
48676050d7
@ -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',
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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{})
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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": {
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user