diff --git a/frontend/src/views/project/settings/ProjectSettingsArchive.vue b/frontend/src/views/project/settings/ProjectSettingsArchive.vue index abf928187..e4f52f6d1 100644 --- a/frontend/src/views/project/settings/ProjectSettingsArchive.vue +++ b/frontend/src/views/project/settings/ProjectSettingsArchive.vue @@ -44,6 +44,7 @@ async function archiveProject() { }) useBaseStore().setCurrentProject(newProject) success({message: t('project.archive.success')}) + await projectStore.loadAllProjects() } finally { router.back() } diff --git a/pkg/models/project.go b/pkg/models/project.go index ea89a3ccb..241d376c3 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -944,6 +944,11 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje } } + err = setArchiveStateForProjectDescendants(s, project.ID, project.IsArchived) + if err != nil { + return err + } + // We need to specify the cols we want to update here to be able to un-archive projects colsToUpdate := []string{ "title", @@ -1286,3 +1291,40 @@ func SetProjectBackground(s *xorm.Session, projectID int64, background *files.Fi Update(l) return } + +// setArchiveStateForProjectDescendants uses a recursive CTE to find and set the archived status of all descendant projects. +func setArchiveStateForProjectDescendants(s *xorm.Session, parentProjectID int64, shouldBeArchived bool) error { + var descendantIDs []int64 + err := s.SQL( + ` +WITH RECURSIVE descendant_ids (id) AS ( + SELECT id + FROM projects + WHERE parent_project_id = ? + UNION ALL + SELECT p.id + FROM projects p + INNER JOIN descendant_ids di ON p.parent_project_id = di.id +) +SELECT id FROM descendant_ids`, + parentProjectID, + ).Find(&descendantIDs) + if err != nil { + log.Errorf("Error finding descendant projects for parent ID %d: %v", parentProjectID, err) + return fmt.Errorf("failed to find descendant projects for parent ID %d: %w", parentProjectID, err) + } + + if len(descendantIDs) == 0 { + return nil + } + + _, err = s.In("id", descendantIDs). + And("is_archived != ?", shouldBeArchived). + Cols("is_archived"). + Update(&Project{IsArchived: shouldBeArchived}) + if err != nil { + log.Errorf("Error updating is_archived for descendant projects for parent ID %d to %t: %v", parentProjectID, shouldBeArchived, err) + return fmt.Errorf("failed to update is_archived for descendant projects for parent ID %d to %t: %w", parentProjectID, shouldBeArchived, err) + } + return nil +} diff --git a/pkg/models/project_test.go b/pkg/models/project_test.go index cb11f683d..88b7ca548 100644 --- a/pkg/models/project_test.go +++ b/pkg/models/project_test.go @@ -305,6 +305,38 @@ func TestProject_CreateOrUpdate(t *testing.T) { require.Error(t, err) assert.True(t, IsErrCannotArchiveDefaultProject(err)) }) + t.Run("archive parent archives child", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + actingUser := &user.User{ID: 6} + + projectToArchive := Project{ + ID: 27, + } + + // We need to load the project first to have its fields populated for the update + can, err := projectToArchive.CanUpdate(s, actingUser) + require.NoError(t, err, "Failed to read project 27 before archiving") + assert.True(t, can) + projectToArchive.IsArchived = true // Ensure IsArchived is set after reading + + err = projectToArchive.Update(s, actingUser) + require.NoError(t, err, "Failed to archive project") + err = s.Commit() + require.NoError(t, err, "Failed to commit session after archiving project") + + db.AssertExists(t, "projects", map[string]interface{}{ + "id": 27, + "is_archived": true, + }, false) + // Assert child project (ID 12) is also archived + db.AssertExists(t, "projects", map[string]interface{}{ + "id": 12, + "is_archived": true, + }, false) + }) }) }