diff --git a/frontend/cypress/e2e/task/task.spec.ts b/frontend/cypress/e2e/task/task.spec.ts index 9a90471dd..19cd60ac7 100644 --- a/frontend/cypress/e2e/task/task.spec.ts +++ b/frontend/cypress/e2e/task/task.spec.ts @@ -509,6 +509,7 @@ describe('Task', () => { TaskBucketFactory.create(1, { task_id: tasks[0].id, bucket_id: buckets[0].id, + project_view_id: buckets[0].project_view_id, }) cy.visit(`/projects/${projects[0].id}/4`) @@ -901,6 +902,11 @@ describe('Task', () => { }) const labels = LabelFactory.create(1) LabelTaskFactory.truncate() + TaskBucketFactory.create(1, { + task_id: tasks[0].id, + bucket_id: buckets[0].id, + project_view_id: buckets[0].project_view_id, + }) cy.visit(`/projects/${projects[0].id}/4`) diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 3122d5321..1a5d091fd 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -201,6 +201,7 @@ func GetTasksInBucketsForView(s *xorm.Session, view *ProjectView, projects []*Pr tasks := []*Task{} + opts.projectViewID = view.ID opts.sortby = []*sortParam{ { projectViewID: view.ID, diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index efa19f47d..083131278 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -142,6 +142,10 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection, projectView *ProjectVie filterTimezone: tf.FilterTimezone, } + if projectView != nil { + opts.projectViewID = projectView.ID + } + opts.parsedFilters, err = getTaskFiltersFromFilterString(tf.Filter, tf.FilterTimezone) return opts, err } diff --git a/pkg/models/task_position.go b/pkg/models/task_position.go index 96a10ff68..ea01711d8 100644 --- a/pkg/models/task_position.go +++ b/pkg/models/task_position.go @@ -118,6 +118,7 @@ func RecalculateTaskPositions(s *xorm.Session, view *ProjectView, a web.Auth) (e log.Debugf("Recalculating task positions for view %d", view.ID) opts := &taskSearchOptions{ + projectViewID: view.ID, sortby: []*sortParam{ { projectViewID: view.ID, diff --git a/pkg/models/task_search.go b/pkg/models/task_search.go index e9fdb8acb..635a21a3d 100644 --- a/pkg/models/task_search.go +++ b/pkg/models/task_search.go @@ -350,7 +350,13 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo } if joinTaskBuckets { - query = query.Join("LEFT", "task_buckets", "task_buckets.task_id = tasks.id") + joinCond := "task_buckets.task_id = tasks.id" + if opts.projectViewID > 0 { + joinCond += " AND task_buckets.project_view_id = ?" + query = query.Join("LEFT", "task_buckets", joinCond, opts.projectViewID) + } else { + query = query.Join("LEFT", "task_buckets", joinCond) + } } if expandSubtasks { query = query. @@ -419,7 +425,13 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo queryCount := d.s.Where(cond) if joinTaskBuckets { - queryCount = queryCount.Join("LEFT", "task_buckets", "task_buckets.task_id = tasks.id") + joinCond := "task_buckets.task_id = tasks.id" + if opts.projectViewID > 0 { + joinCond += " AND task_buckets.project_view_id = ?" + queryCount = queryCount.Join("LEFT", "task_buckets", joinCond, opts.projectViewID) + } else { + queryCount = queryCount.Join("LEFT", "task_buckets", joinCond) + } } if expandSubtasks { queryCount = queryCount. diff --git a/pkg/models/task_search_test.go b/pkg/models/task_search_test.go new file mode 100644 index 000000000..83e52700a --- /dev/null +++ b/pkg/models/task_search_test.go @@ -0,0 +1,56 @@ +// 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 License 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 License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +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 TestKanbanViewBucketFiltering(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + view, err := GetProjectViewByID(s, 4) + require.NoError(t, err) + + project, err := GetProjectSimpleByID(s, view.ProjectID) + require.NoError(t, err) + + buckets, err := GetTasksInBucketsForView(s, view, []*Project{project}, &taskSearchOptions{}, &user.User{ID: 1}) + require.NoError(t, err) + + taskBuckets := map[int64][]int64{} + for _, b := range buckets { + for _, tsk := range b.Tasks { + taskBuckets[tsk.ID] = append(taskBuckets[tsk.ID], b.ID) + } + } + + for tid, bs := range taskBuckets { + assert.Lenf(t, bs, 1, "task %d appears in multiple buckets: %v", tid, bs) + } + + for _, id := range []int64{40, 41, 42, 43, 44, 45, 46} { + assert.NotContains(t, taskBuckets, id) + } +} diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 0c49de38d..ef7222d3c 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -195,6 +195,7 @@ type taskSearchOptions struct { isSavedFilter bool projectIDs []int64 expand []TaskCollectionExpandable + projectViewID int64 } // ReadAll is a dummy function to still have that endpoint documented