From e3045dfd00059145bede25274c1a9f42ba4f8f02 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 25 Mar 2026 09:57:30 +0100 Subject: [PATCH] fix: propagate is_archived from parent to child projects in ReadAll CTE Replace the Go-side propagateArchivedState function with in-CTE propagation. The recursive SELECT uses (ap.is_archived OR p.is_archived) to inherit archived state from parent projects. The outer query uses GROUP BY with MAX(CAST(is_archived AS int)) to handle projects accessible via both direct permissions and parent traversal. When getArchived=false, a HAVING clause filters out archived projects. The is_archived filter is removed from getUserProjectsStatement so archived parents enter the CTE and propagation works correctly. --- pkg/models/label_permissions.go | 2 +- pkg/models/project.go | 47 +++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/pkg/models/label_permissions.go b/pkg/models/label_permissions.go index 4c0badd11..013b3586a 100644 --- a/pkg/models/label_permissions.go +++ b/pkg/models/label_permissions.go @@ -70,7 +70,7 @@ func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxPerm if isLinkShare { where = builder.Eq{"project_id": linkShare.ProjectID} } else { - where = builder.In("project_id", getUserProjectsStatement(a.GetID(), "", false).Select("l.id")) + where = builder.In("project_id", getUserProjectsStatement(a.GetID(), "").Select("l.id")) createdByID = a.GetID() } diff --git a/pkg/models/project.go b/pkg/models/project.go index 26426a62f..bcb3b9d17 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -454,7 +454,7 @@ type projectOptions struct { getArchived bool } -func getUserProjectsStatement(userID int64, search string, getArchived bool) *builder.Builder { +func getUserProjectsStatement(userID int64, search string) *builder.Builder { dialect := db.GetDialect() conds := []builder.Cond{ @@ -507,16 +507,8 @@ func getUserProjectsStatement(userID int64, search string, getArchived bool) *bu conds = append(conds, filterCond, parentCondition) } - if !getArchived { - conds = append(conds, - builder.And( - builder.Eq{"l.is_archived": false}, - ), - ) - } - return builder.Dialect(dialect). - Select("l.*"). + Select("l.id, l.title, l.description, l.identifier, l.hex_color, l.owner_id, l.parent_project_id, l.is_archived, l.background_file_id, l.background_blur_hash, l.position, l.created, l.updated"). From("projects", "l"). Join("LEFT", "team_projects tl", "tl.project_id = l.id"). Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id"). @@ -541,14 +533,14 @@ func accessibleProjectIDsSubquery(a web.Auth, column string) builder.Cond { } return builder.In(column, - getUserProjectsStatement(u.ID, "", false).Select("l.id"), + getUserProjectsStatement(u.ID, "").Select("l.id"), ) } func getAllProjectsForUser(s *xorm.Session, userID int64, opts *projectOptions) (projects []*Project, totalCount int64, err error) { limit, start := getLimitFromPageIndex(opts.page, opts.perPage) - query := getUserProjectsStatement(userID, opts.search, opts.getArchived) + query := getUserProjectsStatement(userID, opts.search) querySQLString, args, err := query.ToSQL() if err != nil { @@ -562,7 +554,7 @@ func getAllProjectsForUser(s *xorm.Session, userID int64, opts *projectOptions) baseQuery := querySQLString + ` UNION ALL -SELECT p.* FROM projects p +SELECT p.id, p.title, p.description, p.identifier, p.hex_color, p.owner_id, p.parent_project_id, (ap.is_archived OR p.is_archived) AS is_archived, p.background_file_id, p.background_blur_hash, p.position, p.created, p.updated FROM projects p INNER JOIN all_projects ap ON p.parent_project_id = ap.id` columnStr := strings.Join([]string{ @@ -573,17 +565,38 @@ INNER JOIN all_projects ap ON p.parent_project_id = ap.id` "all_projects.hex_color", "all_projects.owner_id", "CASE WHEN all_projects.parent_project_id IS NULL THEN 0 ELSE all_projects.parent_project_id END AS parent_project_id", - "all_projects.is_archived", + "MAX(CAST(all_projects.is_archived AS int)) AS is_archived", "all_projects.background_file_id", "all_projects.background_blur_hash", "all_projects.position", "all_projects.created", "all_projects.updated", }, ", ") + + groupByStr := strings.Join([]string{ + "all_projects.id", + "all_projects.title", + "all_projects.description", + "all_projects.identifier", + "all_projects.hex_color", + "all_projects.owner_id", + "all_projects.parent_project_id", + "all_projects.background_file_id", + "all_projects.background_blur_hash", + "all_projects.position", + "all_projects.created", + "all_projects.updated", + }, ", ") + + var archivedFilter string + if !opts.getArchived { + archivedFilter = "HAVING MAX(CAST(all_projects.is_archived AS int)) = 0 " + } + currentProjects := []*Project{} err = s.SQL(`WITH RECURSIVE all_projects as (`+baseQuery+`) -SELECT DISTINCT `+columnStr+` FROM all_projects -ORDER BY all_projects.position `+limitSQL, args...).Find(¤tProjects) +SELECT `+columnStr+` FROM all_projects +GROUP BY `+groupByStr+` `+archivedFilter+`ORDER BY all_projects.position `+limitSQL, args...).Find(¤tProjects) if err != nil { return } @@ -594,7 +607,7 @@ ORDER BY all_projects.position `+limitSQL, args...).Find(¤tProjects) totalCount, err = s. SQL(`WITH RECURSIVE all_projects as (`+baseQuery+`) -SELECT COUNT(DISTINCT all_projects.id) FROM all_projects`, args...). +SELECT COUNT(*) FROM (SELECT all_projects.id FROM all_projects GROUP BY all_projects.id `+archivedFilter+`) sub`, args...). Count(&Project{}) if err != nil { return nil, 0, err