diff --git a/release-notes/4547.md b/release-notes/4547.md
new file mode 100644
index 0000000000..08f131fccd
--- /dev/null
+++ b/release-notes/4547.md
@@ -0,0 +1 @@
+The milestone section in the sidebar on the issue and pull request page now uses HTMX. If you update the milestone of a issue or pull request it will no longer reload the whole page and instead update the current page with the new information about the milestone update. This should provide a smoother user experience.
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index e34f90c73b..dcc1cdd467 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1370,6 +1370,22 @@ func getBranchData(ctx *context.Context, issue *issues_model.Issue) {
}
}
+func prepareHiddenCommentType(ctx *context.Context) {
+ var hiddenCommentTypes *big.Int
+ if ctx.IsSigned {
+ val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)
+ if err != nil {
+ ctx.ServerError("GetUserSetting", err)
+ return
+ }
+ hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here
+ }
+
+ ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool {
+ return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0
+ }
+}
+
// ViewIssue render issue view page
func ViewIssue(ctx *context.Context) {
if ctx.Params(":type") == "issues" {
@@ -2019,21 +2035,13 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["NewPinAllowed"] = pinAllowed
ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0
- var hiddenCommentTypes *big.Int
- if ctx.IsSigned {
- val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)
- if err != nil {
- ctx.ServerError("GetUserSetting", err)
- return
- }
- hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here
- }
- ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool {
- return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0
+ prepareHiddenCommentType(ctx)
+ if ctx.Written() {
+ return
}
+
// For sidebar
PrepareBranchList(ctx)
-
if ctx.Written() {
return
}
@@ -2342,7 +2350,49 @@ func UpdateIssueMilestone(ctx *context.Context) {
}
}
- ctx.JSONOK()
+ if ctx.FormBool("htmx") {
+ renderMilestones(ctx)
+ if ctx.Written() {
+ return
+ }
+ prepareHiddenCommentType(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ issue := issues[0]
+ var err error
+ if issue.MilestoneID > 0 {
+ issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, issue.MilestoneID)
+ if err != nil {
+ ctx.ServerError("GetMilestoneByRepoID", err)
+ return
+ }
+ } else {
+ issue.Milestone = nil
+ }
+
+ comment := &issues_model.Comment{}
+ has, err := db.GetEngine(ctx).Where("issue_id = ? AND type = ?", issue.ID, issues_model.CommentTypeMilestone).OrderBy("id DESC").Limit(1).Get(comment)
+ if !has || err != nil {
+ ctx.ServerError("GetLatestMilestoneComment", err)
+ }
+ if err := comment.LoadMilestone(ctx); err != nil {
+ ctx.ServerError("LoadMilestone", err)
+ return
+ }
+ if err := comment.LoadPoster(ctx); err != nil {
+ ctx.ServerError("LoadPoster", err)
+ return
+ }
+ issue.Comments = issues_model.CommentList{comment}
+
+ ctx.Data["Issue"] = issue
+ ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
+ ctx.HTML(http.StatusOK, "htmx/milestone_sidebar")
+ } else {
+ ctx.JSONOK()
+ }
}
// UpdateIssueAssignee change issue's or pull's assignee
diff --git a/templates/htmx/milestone_sidebar.tmpl b/templates/htmx/milestone_sidebar.tmpl
new file mode 100644
index 0000000000..458dabc5b1
--- /dev/null
+++ b/templates/htmx/milestone_sidebar.tmpl
@@ -0,0 +1,4 @@
+
+ {{template "repo/issue/view_content/comments" .}}
+
+{{template "repo/issue/view_content/sidebar/milestones" .}}
diff --git a/templates/repo/issue/milestone/select_menu.tmpl b/templates/repo/issue/milestone/select_menu.tmpl
index 9b0492ce52..eae2f3baa9 100644
--- a/templates/repo/issue/milestone/select_menu.tmpl
+++ b/templates/repo/issue/milestone/select_menu.tmpl
@@ -5,7 +5,7 @@
{{end}}
-{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}
+{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}
{{if and (not .OpenMilestones) (not .ClosedMilestones)}}
{{ctx.Locale.Tr "repo.issues.new.no_items"}}
@@ -17,7 +17,7 @@
{{ctx.Locale.Tr "repo.issues.new.open_milestone"}}
{{range .OpenMilestones}}
-
+
{{svg "octicon-milestone" 16 "tw-mr-1"}}
{{.Name}}
@@ -29,7 +29,7 @@
{{ctx.Locale.Tr "repo.issues.new.closed_milestone"}}
{{range .ClosedMilestones}}
-
+
{{svg "octicon-milestone" 16 "tw-mr-1"}}
{{.Name}}
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 543191e02d..b97ce8266f 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -72,7 +72,8 @@
- {{template "repo/issue/view_content/comments" .}}
+ {{template "repo/issue/view_content/comments" .}}
+
{{if and .Issue.IsPull (not $.Repository.IsArchived)}}
{{template "repo/issue/view_content/pull".}}
diff --git a/templates/repo/issue/view_content/sidebar/milestones.tmpl b/templates/repo/issue/view_content/sidebar/milestones.tmpl
index 661ca80743..44d9419f9b 100644
--- a/templates/repo/issue/view_content/sidebar/milestones.tmpl
+++ b/templates/repo/issue/view_content/sidebar/milestones.tmpl
@@ -1,22 +1,24 @@
-
-
-
{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}
-
- {{if .Issue.Milestone}}
-
- {{end}}
+
+
+
+
{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}
+
+ {{if .Issue.Milestone}}
+
+ {{end}}
+
diff --git a/tests/e2e/issue-sidebar.test.e2e.js b/tests/e2e/issue-sidebar.test.e2e.js
index 41b1b2064a..4bd211abe5 100644
--- a/tests/e2e/issue-sidebar.test.e2e.js
+++ b/tests/e2e/issue-sidebar.test.e2e.js
@@ -84,3 +84,27 @@ test('Issue: Labels', async ({browser}, workerInfo) => {
await expect(labelList.filter({hasText: 'label2'})).not.toBeVisible();
await expect(labelList.filter({hasText: 'label1'})).toBeVisible();
});
+
+test('Issue: Milestone', async ({browser}, workerInfo) => {
+ test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
+ const page = await login({browser}, workerInfo);
+
+ const response = await page.goto('/user2/repo1/issues/1');
+ await expect(response?.status()).toBe(200);
+
+ const selectedMilestone = page.locator('.issue-content-right .select-milestone.list');
+ const milestoneDropdown = page.locator('.issue-content-right .select-milestone.dropdown');
+ await expect(selectedMilestone).toContainText('No milestone');
+
+ // Add milestone.
+ await milestoneDropdown.click();
+ await page.getByRole('option', {name: 'milestone1'}).click();
+ await expect(selectedMilestone).toContainText('milestone1');
+ await expect(page.locator('.timeline-item.event').last()).toContainText('user2 added this to the milestone1 milestone');
+
+ // Clear milestone.
+ await milestoneDropdown.click();
+ await page.getByText('Clear milestone', {exact: true}).click();
+ await expect(selectedMilestone).toContainText('No milestone');
+ await expect(page.locator('.timeline-item.event').last()).toContainText('user2 removed this from the milestone1 milestone');
+});
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 76de8daf56..d3566fb121 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -270,9 +270,7 @@ export function initRepoCommentForm() {
}
let icon = '';
- if (input_id === '#milestone_id') {
- icon = svg('octicon-milestone', 18, 'tw-mr-2');
- } else if (input_id === '#project_id') {
+ if (input_id === '#project_id') {
icon = svg('octicon-project', 18, 'tw-mr-2');
} else if (input_id === '#assignee_id') {
icon = `
`;
@@ -313,7 +311,6 @@ export function initRepoCommentForm() {
// Milestone, Assignee, Project
selectItem('.select-project', '#project_id');
- selectItem('.select-milestone', '#milestone_id');
selectItem('.select-assignee', '#assignee_id');
}