fix(views): move bucket update to extra endpoint
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
BREAKING CHANGE: The bucket id of the task model is now only used internally and will not trigger a change in buckets when updating the task. This resolves a problem where the task update routine needs to know the view context it is in. Because that's not really what it should be used for, the extra endpoint takes all required parameters and handles the complexity of actually updating the bucket. This fixes a bug where it was impossible to move a task around between buckets of a saved filter view. In that case, the view of the bucket and the project the task was in would be different, hence the update failed.
This commit is contained in:
parent
e6ce1251f7
commit
359b07dabb
@ -306,6 +306,8 @@ import TaskPositionModel from '@/models/taskPosition'
|
||||
import {i18n} from '@/i18n'
|
||||
import ProjectViewService from '@/services/projectViews'
|
||||
import ProjectViewModel from '@/models/projectView'
|
||||
import TaskBucketService from '@/services/taskBucket'
|
||||
import TaskBucketModel from '@/models/taskBucket'
|
||||
|
||||
const {
|
||||
projectId,
|
||||
@ -333,6 +335,7 @@ const kanbanStore = useKanbanStore()
|
||||
const taskStore = useTaskStore()
|
||||
const projectStore = useProjectStore()
|
||||
const taskPositionService = ref(new TaskPositionService())
|
||||
const taskBucketService = ref(new TaskBucketService())
|
||||
|
||||
const taskContainerRefs = ref<{ [id: IBucket['id']]: HTMLElement }>({})
|
||||
const bucketLimitInputRef = ref<HTMLInputElement | null>(null)
|
||||
@ -519,7 +522,12 @@ async function updateTaskPosition(e) {
|
||||
await taskPositionService.value.update(newPosition)
|
||||
|
||||
if(bucketHasChanged) {
|
||||
await taskStore.update(newTask)
|
||||
await taskBucketService.value.update(new TaskBucketModel({
|
||||
taskId: newTask.id,
|
||||
bucketId: newTask.bucketId,
|
||||
projectViewId: viewId,
|
||||
projectId: project.value.id,
|
||||
}))
|
||||
}
|
||||
|
||||
// Make sure the first and second task don't both get position 0 assigned
|
||||
|
12
frontend/src/modelTypes/ITaskBucket.ts
Normal file
12
frontend/src/modelTypes/ITaskBucket.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
import type {IAbstract} from '@/modelTypes/IAbstract'
|
||||
import type {IBucket} from '@/modelTypes/IBucket'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
export interface ITaskBucket extends IAbstract {
|
||||
taskId: ITask['id']
|
||||
bucketId: IBucket['id']
|
||||
projectViewId: IProjectView['id']
|
||||
projectId: IProject['id']
|
||||
}
|
14
frontend/src/models/taskBucket.ts
Normal file
14
frontend/src/models/taskBucket.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import AbstractModel from '@/models/abstractModel'
|
||||
import type {ITaskBucket} from '@/modelTypes/ITaskBucket'
|
||||
|
||||
export default class TaskBucketModel extends AbstractModel<ITaskBucket> implements ITaskBucket {
|
||||
taskId = 0
|
||||
bucketId = 0
|
||||
projectViewId = 0
|
||||
projectId = 0
|
||||
|
||||
constructor(data: Partial<ITaskBucket>) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
}
|
||||
}
|
15
frontend/src/services/taskBucket.ts
Normal file
15
frontend/src/services/taskBucket.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import type {ITaskBucket} from '@/modelTypes/ITaskBucket'
|
||||
import TaskBucketModel from '@/models/taskBucket'
|
||||
|
||||
export default class TaskBucketService extends AbstractService<ITaskBucket> {
|
||||
constructor() {
|
||||
super({
|
||||
update: '/projects/{projectId}/views/{projectViewId}/buckets/{bucketId}/tasks',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data: Partial<ITaskBucket>) {
|
||||
return new TaskBucketModel(data)
|
||||
}
|
||||
}
|
@ -307,24 +307,6 @@ func TestTask(t *testing.T) {
|
||||
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
|
||||
})
|
||||
})
|
||||
t.Run("Bucket", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":3}`)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"bucket_id":3`)
|
||||
assert.NotContains(t, rec.Body.String(), `"bucket_id":1`)
|
||||
})
|
||||
t.Run("Different Project", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":4}`)
|
||||
require.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
|
||||
})
|
||||
t.Run("Nonexisting Bucket", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":9999}`)
|
||||
require.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
|
||||
})
|
||||
})
|
||||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
@ -490,24 +472,6 @@ func TestTask(t *testing.T) {
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
})
|
||||
t.Run("Bucket", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum","bucket_id":3}`)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"bucket_id":3`)
|
||||
assert.NotContains(t, rec.Body.String(), `"bucket_id":1`)
|
||||
})
|
||||
t.Run("Different Project", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum","bucket_id":4}`)
|
||||
require.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToProject)
|
||||
})
|
||||
t.Run("Nonexisting Bucket", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum","bucket_id":9999}`)
|
||||
require.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
|
||||
})
|
||||
})
|
||||
t.Run("Link Share", func(t *testing.T) {
|
||||
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"Lorem Ipsum"}`)
|
||||
require.NoError(t, err)
|
||||
|
@ -70,16 +70,6 @@ func (b *Bucket) TableName() string {
|
||||
return "buckets"
|
||||
}
|
||||
|
||||
type TaskBucket struct {
|
||||
BucketID int64 `xorm:"bigint not null index"`
|
||||
TaskID int64 `xorm:"bigint not null index"`
|
||||
ProjectViewID int64 `xorm:"bigint not null index"`
|
||||
}
|
||||
|
||||
func (b *TaskBucket) TableName() string {
|
||||
return "task_buckets"
|
||||
}
|
||||
|
||||
func getBucketByID(s *xorm.Session, id int64) (b *Bucket, err error) {
|
||||
b = &Bucket{}
|
||||
exists, err := s.Where("id = ?", id).Get(b)
|
||||
|
@ -53,6 +53,8 @@ func (b *Bucket) canDoBucket(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// TODO saved filter check
|
||||
|
||||
p := &Project{ID: pv.ProjectID}
|
||||
return p.CanUpdate(s, a)
|
||||
}
|
||||
|
179
pkg/models/kanban_task_bucket.go
Normal file
179
pkg/models/kanban_task_bucket.go
Normal file
@ -0,0 +1,179 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type TaskBucket struct {
|
||||
BucketID int64 `xorm:"bigint not null index" json:"bucket_id" param:"bucket"`
|
||||
TaskID int64 `xorm:"bigint not null index" json:"task_id"`
|
||||
ProjectViewID int64 `xorm:"bigint not null index" json:"project_view_id" param:"view"`
|
||||
ProjectID int64 `xorm:"-" json:"-" param:"project"`
|
||||
TaskDone bool `xorm:"-" json:"task_done,omitempty"`
|
||||
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (b *TaskBucket) TableName() string {
|
||||
return "task_buckets"
|
||||
}
|
||||
|
||||
func (b *TaskBucket) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
bucket := Bucket{
|
||||
ID: b.BucketID,
|
||||
ProjectID: b.ProjectID,
|
||||
ProjectViewID: b.ProjectViewID,
|
||||
}
|
||||
return bucket.canDoBucket(s, a)
|
||||
}
|
||||
|
||||
// Update is the handler to update a task bucket
|
||||
// @Summary Update a task bucket
|
||||
// @Description Updates a task in a bucket
|
||||
// @tags task
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param project path int true "Project ID"
|
||||
// @Param view path int true "Project View ID"
|
||||
// @Param bucket path int true "Bucket ID"
|
||||
// @Param taskBucket body models.TaskBucket true "The id of the task you want to move into the bucket."
|
||||
// @Success 200 {object} models.TaskBucket "The updated task bucket."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid task bucket object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{project}/views/{view}/buckets/{bucket}/tasks [post]
|
||||
func (b *TaskBucket) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
oldTaskBucket := &TaskBucket{}
|
||||
_, err = s.
|
||||
Where("task_id = ? AND project_view_id = ?", b.TaskID, b.ProjectViewID).
|
||||
Get(oldTaskBucket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if oldTaskBucket.BucketID == b.BucketID {
|
||||
// no need to do anything
|
||||
return
|
||||
}
|
||||
|
||||
view, err := GetProjectViewByIDAndProject(s, b.ProjectViewID, b.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bucket, err := getBucketByID(s, b.BucketID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If there is a bucket set, make sure they belong to the same project as the task
|
||||
if view.ID != bucket.ProjectViewID {
|
||||
return ErrBucketDoesNotBelongToProjectView{
|
||||
ProjectViewID: view.ID,
|
||||
BucketID: bucket.ID,
|
||||
}
|
||||
}
|
||||
|
||||
task, err := GetTaskByIDSimple(s, b.TaskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check the bucket limit
|
||||
// Only check the bucket limit if the task is being moved between buckets, allow reordering the task within a bucket
|
||||
if b.BucketID != 0 && b.BucketID != oldTaskBucket.BucketID {
|
||||
err = checkBucketLimit(s, &task, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var updateBucket = true
|
||||
|
||||
// mark task done if moved into the done bucket
|
||||
var doneChanged bool
|
||||
if view.DoneBucketID == b.BucketID {
|
||||
doneChanged = true
|
||||
task.Done = true
|
||||
if task.RepeatAfter > 0 {
|
||||
oldTask := task
|
||||
task.Done = false
|
||||
updateDone(&oldTask, &task)
|
||||
updateBucket = false
|
||||
}
|
||||
}
|
||||
|
||||
if oldTaskBucket.BucketID == view.DoneBucketID {
|
||||
doneChanged = true
|
||||
task.Done = false
|
||||
}
|
||||
|
||||
if doneChanged {
|
||||
_, err = s.Where("id = ?", task.ID).
|
||||
Cols(
|
||||
"done",
|
||||
"due_date",
|
||||
"start_date",
|
||||
"end_date",
|
||||
).
|
||||
Update(task)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = task.updateReminders(s, &task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
err = events.Dispatch(&TaskUpdatedEvent{
|
||||
Task: &task,
|
||||
Doer: doer,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if updateBucket {
|
||||
count, err := s.Where("task_id = ? AND project_view_id = ?", b.TaskID, b.ProjectViewID).
|
||||
Cols("bucket_id").
|
||||
Update(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
_, err = s.Insert(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.TaskDone = task.Done
|
||||
|
||||
return
|
||||
}
|
159
pkg/models/kanban_task_bucket_test.go
Normal file
159
pkg/models/kanban_task_bucket_test.go
Normal file
@ -0,0 +1,159 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTaskBucket_Update(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("full bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tb := &TaskBucket{
|
||||
TaskID: 1,
|
||||
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
||||
ProjectViewID: 4,
|
||||
ProjectID: 1, // In actual web requests set via the url
|
||||
}
|
||||
|
||||
err := tb.Update(s, u)
|
||||
require.Error(t, err)
|
||||
assert.True(t, IsErrBucketLimitExceeded(err))
|
||||
})
|
||||
t.Run("full bucket but not changing the bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tb := &TaskBucket{
|
||||
TaskID: 4,
|
||||
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
||||
ProjectViewID: 4,
|
||||
ProjectID: 1, // In actual web requests set via the url
|
||||
}
|
||||
err := tb.Update(s, u)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("bucket on other project view", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tb := &TaskBucket{
|
||||
TaskID: 1,
|
||||
BucketID: 4, // Bucket 4 belongs to project 2
|
||||
ProjectViewID: 4,
|
||||
ProjectID: 1, // In actual web requests set via the url
|
||||
}
|
||||
err := tb.Update(s, u)
|
||||
require.Error(t, err)
|
||||
assert.True(t, IsErrBucketDoesNotBelongToProject(err))
|
||||
})
|
||||
t.Run("moving a task to the done bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tb := &TaskBucket{
|
||||
TaskID: 1,
|
||||
BucketID: 3, // Bucket 3 is the done bucket
|
||||
ProjectViewID: 4,
|
||||
ProjectID: 1, // In actual web requests set via the url
|
||||
}
|
||||
err := tb.Update(s, u)
|
||||
require.NoError(t, err)
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, tb.TaskDone)
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 1,
|
||||
"done": true,
|
||||
}, false)
|
||||
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||
"task_id": 1,
|
||||
"bucket_id": 3,
|
||||
}, false)
|
||||
})
|
||||
t.Run("move done task out of done bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tb := &TaskBucket{
|
||||
TaskID: 2,
|
||||
BucketID: 1, // Bucket 1 is the default bucket
|
||||
ProjectViewID: 4,
|
||||
ProjectID: 1, // In actual web requests set via the url
|
||||
}
|
||||
err := tb.Update(s, u)
|
||||
require.NoError(t, err)
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
assert.False(t, tb.TaskDone)
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": tb.TaskID,
|
||||
"done": false,
|
||||
}, false)
|
||||
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||
"task_id": tb.TaskID,
|
||||
"bucket_id": 1,
|
||||
}, false)
|
||||
db.AssertMissing(t, "task_buckets", map[string]interface{}{
|
||||
"task_id": tb.TaskID,
|
||||
"bucket_id": 3,
|
||||
})
|
||||
})
|
||||
t.Run("moving a repeating task to the done bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tb := &TaskBucket{
|
||||
TaskID: 28,
|
||||
BucketID: 3, // Bucket 3 is the done bucket
|
||||
ProjectViewID: 4,
|
||||
ProjectID: 1, // In actual web requests set via the url
|
||||
}
|
||||
err := tb.Update(s, u)
|
||||
require.NoError(t, err)
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
assert.False(t, tb.TaskDone)
|
||||
assert.Equal(t, int64(3), tb.BucketID) // This should be the actual bucket
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 1,
|
||||
"done": false,
|
||||
}, false)
|
||||
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||
"task_id": 1,
|
||||
"bucket_id": 1,
|
||||
}, false)
|
||||
})
|
||||
}
|
@ -655,107 +655,6 @@ func checkBucketLimit(s *xorm.Session, t *Task, bucket *Bucket) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Contains all the task logic to figure out what bucket to use for this task.
|
||||
func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, view *ProjectView, targetBucketID int64) (err error) {
|
||||
if view.BucketConfigurationMode != BucketConfigurationModeManual {
|
||||
return
|
||||
}
|
||||
|
||||
var shouldChangeBucket = true
|
||||
targetBucket := &TaskBucket{
|
||||
BucketID: targetBucketID,
|
||||
TaskID: task.ID,
|
||||
ProjectViewID: view.ID,
|
||||
}
|
||||
|
||||
oldTaskBucket := &TaskBucket{}
|
||||
_, err = s.
|
||||
Where("task_id = ? AND project_view_id = ?", task.ID, view.ID).
|
||||
Get(oldTaskBucket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If the task was marked as done and the view has a done bucket, move the task to the done bucket
|
||||
if task.Done && originalTask != nil &&
|
||||
(!originalTask.Done || task.ProjectID != originalTask.ProjectID) {
|
||||
targetBucket.BucketID = view.DoneBucketID
|
||||
// …and also reset the position so that it shows up at the top
|
||||
// Note: this might result in an "off-looking" position when there is already a task with position 0.
|
||||
// This is done by design, because recalculating all positions is really costly and will happen
|
||||
// later anyway.
|
||||
_, err = s.
|
||||
Where("task_id = ? AND project_view_id = ?", task.ID, view.ID).
|
||||
Cols("position").
|
||||
Update(&TaskPosition{Position: 0})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if targetBucket.BucketID == 0 && oldTaskBucket.BucketID != 0 {
|
||||
shouldChangeBucket = false
|
||||
}
|
||||
|
||||
// Either no bucket was provided or the task was moved between projects
|
||||
// But if the task was moved between projects, don't update the done bucket
|
||||
// because then we have it already updated to the done bucket.
|
||||
if targetBucket.BucketID == 0 ||
|
||||
(originalTask != nil && task.ProjectID != 0 && originalTask.ProjectID != task.ProjectID && !task.Done) {
|
||||
targetBucket.BucketID, err = getDefaultBucketID(s, view)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bucket, err := getBucketByID(s, targetBucket.BucketID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If there is a bucket set, make sure they belong to the same project as the task
|
||||
if view.ID != bucket.ProjectViewID {
|
||||
return ErrBucketDoesNotBelongToProjectView{
|
||||
ProjectViewID: view.ID,
|
||||
BucketID: bucket.ID,
|
||||
}
|
||||
}
|
||||
|
||||
// Check the bucket limit
|
||||
// Only check the bucket limit if the task is being moved between buckets, allow reordering the task within a bucket
|
||||
if targetBucket.BucketID != 0 && targetBucket.BucketID != oldTaskBucket.BucketID {
|
||||
err = checkBucketLimit(s, task, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if bucket.ID == view.DoneBucketID && originalTask != nil && !originalTask.Done {
|
||||
task.Done = true
|
||||
}
|
||||
|
||||
// If the task was moved into the done bucket and the task has a repeating cycle we should not update
|
||||
// the bucket.
|
||||
if bucket.ID == view.DoneBucketID && task.RepeatAfter > 0 {
|
||||
task.Done = true // This will trigger the correct re-scheduling of the task (happening in updateDone later)
|
||||
shouldChangeBucket = false
|
||||
}
|
||||
|
||||
if shouldChangeBucket {
|
||||
_, err = s.
|
||||
Where("task_id = ? AND project_view_id = ?", task.ID, view.ID).
|
||||
Delete(&TaskBucket{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
targetBucket.BucketID = bucket.ID
|
||||
_, err = s.Insert(targetBucket)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func calculateDefaultPosition(entityID int64, position float64) float64 {
|
||||
if position == 0 {
|
||||
return float64(entityID) * math.Pow(2, 16)
|
||||
@ -795,7 +694,7 @@ func (t *Task) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
return createTask(s, t, a, true, true)
|
||||
}
|
||||
|
||||
func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool, updateBucket bool) (err error) {
|
||||
func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool, setBucket bool) (err error) {
|
||||
|
||||
t.ID = 0
|
||||
|
||||
@ -840,15 +739,26 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool, upda
|
||||
}
|
||||
|
||||
positions := []*TaskPosition{}
|
||||
taskBuckets := []*TaskBucket{}
|
||||
|
||||
for _, view := range views {
|
||||
if setBucket &&
|
||||
view.ViewKind == ProjectViewKindKanban &&
|
||||
view.BucketConfigurationMode == BucketConfigurationModeManual {
|
||||
|
||||
if updateBucket {
|
||||
// Get the default bucket and move the task there
|
||||
err = setTaskBucket(s, t, nil, view, t.BucketID)
|
||||
if err != nil {
|
||||
return
|
||||
bucketID := view.DoneBucketID
|
||||
if !t.Done || view.DoneBucketID == 0 {
|
||||
bucketID, err = getDefaultBucketID(s, view)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
taskBuckets = append(taskBuckets, &TaskBucket{
|
||||
BucketID: bucketID,
|
||||
TaskID: t.ID,
|
||||
ProjectViewID: view.ID,
|
||||
})
|
||||
}
|
||||
|
||||
positions = append(positions, &TaskPosition{
|
||||
@ -858,13 +768,20 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool, upda
|
||||
})
|
||||
}
|
||||
|
||||
if updateBucket {
|
||||
if len(positions) > 0 {
|
||||
_, err = s.Insert(&positions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(taskBuckets) > 0 {
|
||||
_, err = s.Insert(&taskBuckets)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.CreatedBy = createdBy
|
||||
|
||||
// Update the assignees
|
||||
@ -941,11 +858,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the reminders
|
||||
if err := ot.updateReminders(s, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// All columns to update in a separate variable to be able to add to them
|
||||
colsToUpdate := []string{
|
||||
"title",
|
||||
@ -975,44 +887,62 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
colsToUpdate = append(colsToUpdate, "index")
|
||||
}
|
||||
|
||||
views, err := getViewsForProject(s, t.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buckets := make(map[int64]*Bucket)
|
||||
err = s.In("project_view_id",
|
||||
builder.Select("id").
|
||||
From("project_views").
|
||||
Where(builder.Eq{"project_id": t.ProjectID}),
|
||||
).
|
||||
Find(&buckets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, view := range views {
|
||||
// Only update the bucket when the current view
|
||||
var targetBucketID int64
|
||||
if t.BucketID != 0 {
|
||||
bucket, has := buckets[t.BucketID]
|
||||
if !has {
|
||||
return ErrBucketDoesNotExist{BucketID: t.BucketID}
|
||||
}
|
||||
if has && bucket.ProjectViewID == view.ID {
|
||||
targetBucketID = t.BucketID
|
||||
}
|
||||
}
|
||||
|
||||
err = setTaskBucket(s, t, &ot, view, targetBucketID)
|
||||
// When a task was marked done or moved between projects, make sure it is in the correct bucket
|
||||
if t.Done != ot.Done || t.ProjectID != ot.ProjectID {
|
||||
views, err := getViewsForProject(s, t.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, view := range views {
|
||||
if view.ViewKind != ProjectViewKindKanban && view.BucketConfigurationMode != BucketConfigurationModeManual {
|
||||
continue
|
||||
}
|
||||
|
||||
var bucketID = view.DoneBucketID
|
||||
if bucketID == 0 || t.ProjectID != ot.ProjectID {
|
||||
bucketID, err = getDefaultBucketID(s, view)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Task is done and was moved between projects, should go into the done bucket of the new project
|
||||
if t.Done && t.ProjectID != ot.ProjectID {
|
||||
bucketID = view.DoneBucketID
|
||||
}
|
||||
|
||||
tb := &TaskBucket{
|
||||
BucketID: bucketID,
|
||||
TaskID: t.ID,
|
||||
ProjectViewID: view.ID,
|
||||
ProjectID: t.ProjectID,
|
||||
}
|
||||
err = tb.Update(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tp := TaskPosition{
|
||||
TaskID: t.ID,
|
||||
ProjectViewID: view.ID,
|
||||
Position: calculateDefaultPosition(t.Index, t.Position),
|
||||
}
|
||||
err = tp.Update(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
|
||||
updateDone(&ot, t)
|
||||
|
||||
// Update the reminders
|
||||
if err := ot.updateReminders(s, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If a task attachment is being set as cover image, check if the attachment actually belongs to the task
|
||||
if t.CoverImageAttachmentID != 0 {
|
||||
is, err := s.Exist(&TaskAttachment{
|
||||
|
@ -157,21 +157,6 @@ func TestTask_Create(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.True(t, user.IsErrUserDoesNotExist(err))
|
||||
})
|
||||
t.Run("full bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
Title: "Lorem",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ProjectID: 1,
|
||||
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
||||
}
|
||||
err := task.Create(s, usr)
|
||||
require.Error(t, err)
|
||||
assert.True(t, IsErrBucketLimitExceeded(err))
|
||||
})
|
||||
t.Run("default bucket different", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -232,137 +217,6 @@ func TestTask_Update(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.True(t, IsErrTaskDoesNotExist(err))
|
||||
})
|
||||
t.Run("full bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
Title: "test10000",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ProjectID: 1,
|
||||
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
require.Error(t, err)
|
||||
assert.True(t, IsErrBucketLimitExceeded(err))
|
||||
})
|
||||
t.Run("full bucket but not changing the bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 4,
|
||||
Title: "test10000",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ProjectID: 1,
|
||||
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("bucket on other project", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
Title: "test10000",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ProjectID: 1,
|
||||
BucketID: 4, // Bucket 4 belongs to project 2
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
require.Error(t, err)
|
||||
assert.True(t, IsErrBucketDoesNotExist(err))
|
||||
})
|
||||
t.Run("moving a task to the done bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
Title: "test",
|
||||
ProjectID: 1,
|
||||
BucketID: 3, // Bucket 3 is the done bucket
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
require.NoError(t, err)
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, task.Done)
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 1,
|
||||
"done": true,
|
||||
}, false)
|
||||
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||
"task_id": 1,
|
||||
"bucket_id": 3,
|
||||
}, false)
|
||||
})
|
||||
t.Run("move done task out of done bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 2,
|
||||
Title: "test",
|
||||
ProjectID: 1,
|
||||
BucketID: 1, // Bucket 1 is the default bucket
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
require.NoError(t, err)
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
assert.False(t, task.Done)
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": task.ID,
|
||||
"done": false,
|
||||
}, false)
|
||||
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||
"task_id": task.ID,
|
||||
"bucket_id": 1,
|
||||
}, false)
|
||||
db.AssertMissing(t, "task_buckets", map[string]interface{}{
|
||||
"task_id": task.ID,
|
||||
"bucket_id": 3,
|
||||
})
|
||||
})
|
||||
t.Run("moving a repeating task to the done bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 28,
|
||||
Title: "test updated",
|
||||
ProjectID: 1,
|
||||
BucketID: 3, // Bucket 3 is the done bucket
|
||||
RepeatAfter: 3600,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
require.NoError(t, err)
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
assert.False(t, task.Done)
|
||||
assert.Equal(t, int64(3), task.BucketID)
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 1,
|
||||
"done": false,
|
||||
}, false)
|
||||
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||
"task_id": 1,
|
||||
"bucket_id": 1,
|
||||
}, false)
|
||||
})
|
||||
t.Run("default bucket when moving a task between projects", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -407,7 +261,7 @@ func TestTask_Update(t *testing.T) {
|
||||
"bucket_id": 3,
|
||||
}, false)
|
||||
})
|
||||
t.Run("move task to another project", func(t *testing.T) {
|
||||
t.Run("move task to another project should use the default bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
@ -189,6 +189,7 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
||||
}
|
||||
|
||||
// Create all views, create default views if we don't have any
|
||||
var kanbanView *models.ProjectView
|
||||
viewsByOldIDs := make(map[int64]*models.ProjectView, len(oldViews))
|
||||
if len(oldViews) > 0 {
|
||||
for _, view := range oldViews {
|
||||
@ -216,12 +217,16 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
||||
return
|
||||
}
|
||||
viewsByOldIDs[oldID] = view
|
||||
if view.ViewKind == models.ProjectViewKindKanban {
|
||||
kanbanView = view
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Only using the default views
|
||||
// Add all buckets to the default kanban view
|
||||
for _, view := range project.Views {
|
||||
if view.ViewKind == models.ProjectViewKindKanban {
|
||||
kanbanView = view
|
||||
for _, b := range bucketsByOldID {
|
||||
b.ProjectViewID = view.ID
|
||||
err = b.Update(s, user)
|
||||
@ -236,25 +241,36 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
||||
|
||||
log.Debugf("[creating structure] Creating %d tasks", len(tasks))
|
||||
|
||||
setBucketOrDefault := func(task *models.Task) {
|
||||
bucket, exists := bucketsByOldID[task.BucketID]
|
||||
setBucketOrDefault := func(task *models.Task) (err error) {
|
||||
var bucketID = task.BucketID
|
||||
bucket, exists := bucketsByOldID[bucketID]
|
||||
if exists {
|
||||
task.BucketID = bucket.ID
|
||||
} else if task.BucketID > 0 {
|
||||
bucketID = bucket.ID
|
||||
tb := &models.TaskBucket{
|
||||
TaskID: task.ID,
|
||||
BucketID: bucketID,
|
||||
ProjectID: task.ProjectID,
|
||||
ProjectViewID: kanbanView.ID,
|
||||
}
|
||||
err = tb.Update(s, user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if bucketID > 0 {
|
||||
log.Debugf("[creating structure] No bucket created for original bucket id %d", task.BucketID)
|
||||
task.BucketID = 0
|
||||
bucketID = 0
|
||||
}
|
||||
if !exists || task.BucketID == 0 {
|
||||
if !exists || bucketID == 0 {
|
||||
needsDefaultBucket = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tasksByOldID := make(map[int64]*models.TaskWithComments, len(tasks))
|
||||
newTaskIDs := []int64{}
|
||||
// Create all tasks
|
||||
for i, t := range tasks {
|
||||
setBucketOrDefault(&tasks[i].Task)
|
||||
|
||||
oldid := t.ID
|
||||
t.ProjectID = project.ID
|
||||
err = t.Create(s, user)
|
||||
@ -262,6 +278,11 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
||||
continue
|
||||
}
|
||||
|
||||
err = setBucketOrDefault(&tasks[i].Task)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
newTaskIDs = append(newTaskIDs, t.ID)
|
||||
|
||||
if err != nil {
|
||||
@ -285,7 +306,10 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
||||
// First create the related tasks if they do not exist
|
||||
if _, exists := tasksByOldID[rt.ID]; !exists || rt.ID == 0 {
|
||||
oldid := rt.ID
|
||||
setBucketOrDefault(rt)
|
||||
err = setBucketOrDefault(rt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rt.ProjectID = t.ProjectID
|
||||
err = rt.Create(s, user)
|
||||
if err != nil {
|
||||
|
@ -622,12 +622,19 @@ func registerAPIRoutes(a *echo.Group) {
|
||||
return &models.ProjectView{}
|
||||
},
|
||||
}
|
||||
|
||||
a.GET("/projects/:project/views", projectViewProvider.ReadAllWeb)
|
||||
a.GET("/projects/:project/views/:view", projectViewProvider.ReadOneWeb)
|
||||
a.PUT("/projects/:project/views", projectViewProvider.CreateWeb)
|
||||
a.DELETE("/projects/:project/views/:view", projectViewProvider.DeleteWeb)
|
||||
a.POST("/projects/:project/views/:view", projectViewProvider.UpdateWeb)
|
||||
|
||||
// Kanban Task Bucket Relation
|
||||
taskBucketProvider := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.TaskBucket{}
|
||||
},
|
||||
}
|
||||
a.POST("/projects/:project/views/:view/buckets/:bucket/tasks", taskBucketProvider.UpdateWeb)
|
||||
}
|
||||
|
||||
func registerMigrations(m *echo.Group) {
|
||||
|
@ -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"
|
||||
@ -3736,6 +3737,78 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/projects/{project}/views/{view}/buckets/{bucket}/tasks": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Updates a task in a bucket",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
],
|
||||
"summary": "Update a task bucket",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project ID",
|
||||
"name": "project",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project View ID",
|
||||
"name": "view",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Bucket ID",
|
||||
"name": "bucket",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "The id of the task you want to move into the bucket.",
|
||||
"name": "taskBucket",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskBucket"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The updated task bucket.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskBucket"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid task bucket object provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/register": {
|
||||
"post": {
|
||||
"description": "Creates a new user account.",
|
||||
@ -8751,6 +8824,23 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.TaskBucket": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"project_view_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"task_done": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"task_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.TaskCollection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -9641,8 +9731,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() {
|
||||
|
@ -3728,6 +3728,78 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/projects/{project}/views/{view}/buckets/{bucket}/tasks": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Updates a task in a bucket",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
],
|
||||
"summary": "Update a task bucket",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project ID",
|
||||
"name": "project",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project View ID",
|
||||
"name": "view",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Bucket ID",
|
||||
"name": "bucket",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "The id of the task you want to move into the bucket.",
|
||||
"name": "taskBucket",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskBucket"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The updated task bucket.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskBucket"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid task bucket object provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/register": {
|
||||
"post": {
|
||||
"description": "Creates a new user account.",
|
||||
@ -8743,6 +8815,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.TaskBucket": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"project_view_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"task_done": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"task_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.TaskCollection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -870,6 +870,17 @@ definitions:
|
||||
task_id:
|
||||
type: integer
|
||||
type: object
|
||||
models.TaskBucket:
|
||||
properties:
|
||||
bucket_id:
|
||||
type: integer
|
||||
project_view_id:
|
||||
type: integer
|
||||
task_done:
|
||||
type: boolean
|
||||
task_id:
|
||||
type: integer
|
||||
type: object
|
||||
models.TaskCollection:
|
||||
properties:
|
||||
filter:
|
||||
@ -3855,6 +3866,53 @@ paths:
|
||||
summary: Updates a project view
|
||||
tags:
|
||||
- project
|
||||
/projects/{project}/views/{view}/buckets/{bucket}/tasks:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Updates a task in a bucket
|
||||
parameters:
|
||||
- description: Project ID
|
||||
in: path
|
||||
name: project
|
||||
required: true
|
||||
type: integer
|
||||
- description: Project View ID
|
||||
in: path
|
||||
name: view
|
||||
required: true
|
||||
type: integer
|
||||
- description: Bucket ID
|
||||
in: path
|
||||
name: bucket
|
||||
required: true
|
||||
type: integer
|
||||
- description: The id of the task you want to move into the bucket.
|
||||
in: body
|
||||
name: taskBucket
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.TaskBucket'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The updated task bucket.
|
||||
schema:
|
||||
$ref: '#/definitions/models.TaskBucket'
|
||||
"400":
|
||||
description: Invalid task bucket object provided.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Update a task bucket
|
||||
tags:
|
||||
- task
|
||||
/projects/{projectID}/duplicate:
|
||||
put:
|
||||
consumes:
|
||||
|
Loading…
x
Reference in New Issue
Block a user