diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go index 955ab2356d..021b8beb9e 100644 --- a/models/issues/milestone_list.go +++ b/models/issues/milestone_list.go @@ -24,6 +24,18 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 { return ids } +// SplitByOpenClosed splits the milestone list into open and closed milestones +func (milestones MilestoneList) SplitByOpenClosed() (open, closed MilestoneList) { + for _, m := range milestones { + if m.IsClosed { + closed = append(closed, m) + } else { + open = append(open, m) + } + } + return open, closed +} + // FindMilestoneOptions contain options to get milestones type FindMilestoneOptions struct { db.ListOptions diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index f4a54db006..e01e615de6 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -13,7 +13,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" org_model "code.gitea.io/gitea/models/organization" project_model "code.gitea.io/gitea/models/project" - attachment_model "code.gitea.io/gitea/models/repo" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/optional" @@ -25,6 +25,8 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" project_service "code.gitea.io/gitea/services/projects" + + "xorm.io/builder" ) const ( @@ -332,12 +334,26 @@ func ViewProject(ctx *context.Context) { return } assigneeID := ctx.FormString("assignee") + milestoneID := ctx.FormInt64("milestone") + + // Prepare milestone IDs for filtering + var milestoneIDs []int64 + if milestoneID > 0 { + milestoneIDs = []int64{milestoneID} + } else if milestoneID == db.NoConditionID { + milestoneIDs = []int64{db.NoConditionID} + } opts := issues_model.IssuesOptions{ - LabelIDs: preparedLabelFilter.SelectedLabelIDs, - AssigneeID: assigneeID, - Owner: project.Owner, - Doer: ctx.Doer, + LabelIDs: preparedLabelFilter.SelectedLabelIDs, + AssigneeID: assigneeID, + MilestoneIDs: milestoneIDs, + Owner: project.Owner, + } + if ctx.Doer != nil { + opts.Doer = ctx.Doer + } else { + opts.AllPublic = true } issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts) @@ -350,10 +366,10 @@ func ViewProject(ctx *context.Context) { } if project.CardType != project_model.CardTypeTextOnly { - issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment) + issuesAttachmentMap := make(map[int64][]*repo_model.Attachment) for _, issuesList := range issuesMap { for _, issue := range issuesList { - if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil { + if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil { issuesAttachmentMap[issue.ID] = issueAttachment } } @@ -411,6 +427,42 @@ func ViewProject(ctx *context.Context) { ctx.Data["Labels"] = labels ctx.Data["NumLabels"] = len(labels) + // Get milestones for filtering + // For organization projects, we need to get milestones from all repos the user has access to + var milestones issues_model.MilestoneList + if project.RepoID > 0 { + // Repo-specific project + milestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{ + RepoID: project.RepoID, + }) + if err != nil { + ctx.ServerError("GetRepoMilestones", err) + return + } + } else { + // Organization-wide project - get milestones from all organization repos + // but only from repositories the current user can access. + // Use RepoCond with a subquery to avoid materializing all repo IDs in memory + // which can hit SQL parameter limits for orgs with many repos. + accessCond := repo_model.AccessibleRepositoryCondition(ctx.Doer, unit.TypeIssues) + repoCond := builder.And( + builder.Eq{"owner_id": project.OwnerID}, + accessCond, + ) + milestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{ + RepoCond: repoCond, + }) + if err != nil { + ctx.ServerError("GetOrgMilestones", err) + return + } + } + + openMilestones, closedMilestones := milestones.SplitByOpenClosed() + ctx.Data["OpenMilestones"] = openMilestones + ctx.Data["ClosedMilestones"] = closedMilestones + ctx.Data["MilestoneID"] = milestoneID + // Get assignees. assigneeUsers, err := org_model.GetOrgAssignees(ctx, project.OwnerID) if err != nil { diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index da0ba6c407..ff4ff26685 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -462,14 +462,7 @@ func renderMilestones(ctx *context.Context) { return } - openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{} - for _, milestone := range milestones { - if milestone.IsClosed { - closedMilestones = append(closedMilestones, milestone) - } else { - openMilestones = append(openMilestones, milestone) - } - } + openMilestones, closedMilestones := issues_model.MilestoneList(milestones).SplitByOpenClosed() ctx.Data["OpenMilestones"] = openMilestones ctx.Data["ClosedMilestones"] = closedMilestones } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index a57976b4ca..e7a9e6ba12 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -311,13 +311,25 @@ func ViewProject(ctx *context.Context) { } preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner) + if ctx.Written() { + return + } assigneeID := ctx.FormString("assignee") + milestoneID := ctx.FormInt64("milestone") + + var milestoneIDs []int64 + if milestoneID > 0 { + milestoneIDs = []int64{milestoneID} + } else if milestoneID == db.NoConditionID { + milestoneIDs = []int64{db.NoConditionID} + } issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{ - RepoIDs: []int64{ctx.Repo.Repository.ID}, - LabelIDs: preparedLabelFilter.SelectedLabelIDs, - AssigneeID: assigneeID, + RepoIDs: []int64{ctx.Repo.Repository.ID}, + LabelIDs: preparedLabelFilter.SelectedLabelIDs, + AssigneeID: assigneeID, + MilestoneIDs: milestoneIDs, }) if err != nil { ctx.ServerError("LoadIssuesOfColumns", err) @@ -408,6 +420,12 @@ func ViewProject(ctx *context.Context) { ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) ctx.Data["AssigneeID"] = assigneeID + renderMilestones(ctx) + if ctx.Written() { + return + } + ctx.Data["MilestoneID"] = milestoneID + rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository) project.RenderedContent, err = markdown.RenderString(rctx, project.Description) if err != nil { diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 5801396e3c..09edcb1185 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -5,7 +5,7 @@

{{.Project.Title}}

{{if $canWriteProject}}