Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 67 additions & 4 deletions routers/web/org/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,12 +341,22 @@ 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 == -1 {
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,
Doer: ctx.Doer,
}

issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts)
Expand Down Expand Up @@ -420,6 +430,59 @@ 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.Milestone
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
// Get repos owned by the organization
repos, _, err := attachment_model.GetUserRepositories(ctx, attachment_model.SearchRepoOptions{
Actor: project.Owner,
Private: true,
OwnerID: project.OwnerID,
})
if err != nil {
ctx.ServerError("GetUserRepositories", err)
return
}

repoIDs := make([]int64, 0, len(repos))
for _, repo := range repos {
repoIDs = append(repoIDs, repo.ID)
}

if len(repoIDs) > 0 {
milestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
RepoIDs: repoIDs,
})
if err != nil {
ctx.ServerError("GetOrgMilestones", err)
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)
}
}
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 {
Expand Down
36 changes: 33 additions & 3 deletions routers/web/repo/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,20 @@ func ViewProject(ctx *context.Context) {
preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)

assigneeID := ctx.FormString("assignee")
milestoneID := ctx.FormInt64("milestone")

var milestoneIDs []int64
if milestoneID > 0 {
milestoneIDs = []int64{milestoneID}
} else if milestoneID == -1 {
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)
Expand Down Expand Up @@ -408,6 +417,27 @@ func ViewProject(ctx *context.Context) {
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
ctx.Data["AssigneeID"] = assigneeID

// Get milestones
milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
ctx.ServerError("GetMilestones", err)
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)
}
}
ctx.Data["OpenMilestones"] = openMilestones
ctx.Data["ClosedMilestones"] = closedMilestones
ctx.Data["MilestoneID"] = milestoneID

rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
project.RenderedContent, err = markdown.RenderString(rctx, project.Description)
if err != nil {
Expand Down
38 changes: 37 additions & 1 deletion templates/projects/view.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<h2>{{.Project.Title}}</h2>
<div class="tw-flex-1"></div>
<div class="ui secondary menu tw-m-0">
{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "milestone" $.MilestoneID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
{{template "repo/issue/filter_item_user_assign" dict
"QueryParamKey" "assignee"
Expand All @@ -16,6 +16,42 @@
"TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assignee_no_assignee")
"TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee")
}}
<!-- Milestone -->
<div class="item ui dropdown jump {{if not (or .OpenMilestones .ClosedMilestones)}}disabled{{end}}">
<span class="text">
{{ctx.Locale.Tr "repo.issues.filter_milestone"}}
</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="ui icon search input">
<i class="icon">{{svg "octicon-search" 16}}</i>
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}">
</div>
<div class="divider"></div>
<a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a>
<a class="{{if eq $.MilestoneID -1}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" -1}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a>
{{if .OpenMilestones}}
<div class="divider"></div>
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div>
{{range .OpenMilestones}}
<a class="{{if eq $.MilestoneID .ID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" .ID}}">
{{svg "octicon-milestone" 16 "mr-2"}}
{{.Name}}
</a>
{{end}}
{{end}}
{{if .ClosedMilestones}}
<div class="divider"></div>
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div>
{{range .ClosedMilestones}}
<a class="{{if eq $.MilestoneID .ID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" .ID}}">
{{svg "octicon-milestone" 16 "mr-2"}}
{{.Name}}
</a>
{{end}}
{{end}}
</div>
</div>
</div>
{{if $canWriteProject}}
<div class="ui compact mini menu">
Expand Down