fix(tasks): include favorites scope when scoping subtask roots to parent

The subtask root condition only checked whether the parent was within the
project scope, falling back to 1 = 1 when projectIDs was empty. For favorites
pseudo-project searches (hasFavoritesProject true, projectIDs empty) this
dropped the scope check entirely, so a subtask whose parent matched the
filter but was not itself favorited could be hidden from the roots - the same
class of bug as #2646. Mirror the base (projectIDCond OR favoritesCond) result
set so the parent is in scope exactly when it could appear as a result row.
This commit is contained in:
kolaente 2026-06-19 22:54:59 +02:00
parent d52f6d547f
commit 4c5a40e6f4
1 changed files with 23 additions and 6 deletions

View File

@ -369,16 +369,33 @@ func stripBucketIDFilters(filters []*taskFilter) []*taskFilter {
// A task is excluded from roots only when ALL of the following hold:
// - it has a parenttask relation, AND
// - the parent task exists, AND
// - the parent is within the queried project scope, AND
// - the parent is within the queried result scope, AND
// - the parent satisfies the active filter.
//
// Note the filter (and project scope) is applied here, but not the text-search
// Note the filter (and result scope) is applied here, but not the text-search
// predicate: search uses ParadeDB operators that don't compose against the
// parent_tasks alias, and #2646 is purely about filters.
func buildSubtaskRootCondition(opts *taskSearchOptions) (builder.Cond, error) {
parentInScope := builder.Cond(builder.Expr("1 = 1"))
func (d *dbTaskSearcher) buildSubtaskRootCondition(opts *taskSearchOptions) (builder.Cond, error) {
// The base result set is (projectIDCond OR favoritesCond); mirror both so the
// parent is considered "in scope" exactly when it could appear as a result row.
scopes := make([]builder.Cond, 0, 2)
if len(opts.projectIDs) > 0 {
parentInScope = builder.In("parent_tasks.project_id", opts.projectIDs)
scopes = append(scopes, builder.In("parent_tasks.project_id", opts.projectIDs))
}
if d.hasFavoritesProject {
favCond := builder.
Select("entity_id").
From("favorites").
Where(builder.And(
builder.Eq{"user_id": d.a.GetID()},
builder.Eq{"kind": FavoriteKindTask},
))
scopes = append(scopes, builder.In("parent_tasks.id", favCond))
}
parentInScope := builder.Cond(builder.Expr("1 = 1"))
if len(scopes) > 0 {
parentInScope = builder.Or(scopes...)
}
parentMatchesFilter := builder.Cond(builder.Expr("1 = 1"))
@ -426,7 +443,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
// before convertFiltersToDBFilterCond mutates the shared filter field names.
var subtaskRootCond builder.Cond
if expandSubtasks {
subtaskRootCond, err = buildSubtaskRootCondition(opts)
subtaskRootCond, err = d.buildSubtaskRootCondition(opts)
if err != nil {
return nil, 0, err
}