mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-28 09:21:13 -05:00
Merge pull request '[UI] Convert milestone to HTMX' (#4542) from gusted/htmx-milestone into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4542 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
eb61437a52
8 changed files with 120 additions and 41 deletions
1
release-notes/4547.md
Normal file
1
release-notes/4547.md
Normal file
|
@ -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.
|
|
@ -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
|
// ViewIssue render issue view page
|
||||||
func ViewIssue(ctx *context.Context) {
|
func ViewIssue(ctx *context.Context) {
|
||||||
if ctx.Params(":type") == "issues" {
|
if ctx.Params(":type") == "issues" {
|
||||||
|
@ -2019,21 +2035,13 @@ func ViewIssue(ctx *context.Context) {
|
||||||
ctx.Data["NewPinAllowed"] = pinAllowed
|
ctx.Data["NewPinAllowed"] = pinAllowed
|
||||||
ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0
|
ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0
|
||||||
|
|
||||||
var hiddenCommentTypes *big.Int
|
prepareHiddenCommentType(ctx)
|
||||||
if ctx.IsSigned {
|
if ctx.Written() {
|
||||||
val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)
|
return
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For sidebar
|
// For sidebar
|
||||||
PrepareBranchList(ctx)
|
PrepareBranchList(ctx)
|
||||||
|
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
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
|
// UpdateIssueAssignee change issue's or pull's assignee
|
||||||
|
|
4
templates/htmx/milestone_sidebar.tmpl
Normal file
4
templates/htmx/milestone_sidebar.tmpl
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<div id="insert-timeline" hx-swap-oob="beforebegin">
|
||||||
|
{{template "repo/issue/view_content/comments" .}}
|
||||||
|
</div>
|
||||||
|
{{template "repo/issue/view_content/sidebar/milestones" .}}
|
|
@ -5,7 +5,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}</div>
|
<div class="no-select item" hx-post="{{$.RepoLink}}/issues/milestone?issue_ids={{$.Issue.ID}}&htmx=true">{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}</div>
|
||||||
{{if and (not .OpenMilestones) (not .ClosedMilestones)}}
|
{{if and (not .OpenMilestones) (not .ClosedMilestones)}}
|
||||||
<div class="disabled item">
|
<div class="disabled item">
|
||||||
{{ctx.Locale.Tr "repo.issues.new.no_items"}}
|
{{ctx.Locale.Tr "repo.issues.new.no_items"}}
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
{{ctx.Locale.Tr "repo.issues.new.open_milestone"}}
|
{{ctx.Locale.Tr "repo.issues.new.open_milestone"}}
|
||||||
</div>
|
</div>
|
||||||
{{range .OpenMilestones}}
|
{{range .OpenMilestones}}
|
||||||
<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
|
<a class="item" hx-post="{{$.RepoLink}}/issues/milestone?id={{.ID}}&issue_ids={{$.Issue.ID}}&htmx=true">
|
||||||
{{svg "octicon-milestone" 16 "tw-mr-1"}}
|
{{svg "octicon-milestone" 16 "tw-mr-1"}}
|
||||||
{{.Name}}
|
{{.Name}}
|
||||||
</a>
|
</a>
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
{{ctx.Locale.Tr "repo.issues.new.closed_milestone"}}
|
{{ctx.Locale.Tr "repo.issues.new.closed_milestone"}}
|
||||||
</div>
|
</div>
|
||||||
{{range .ClosedMilestones}}
|
{{range .ClosedMilestones}}
|
||||||
<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
|
<a class="item" hx-post="{{$.RepoLink}}/issues/milestone?id={{.ID}}&issue_ids={{$.Issue.ID}}&htmx=true">
|
||||||
{{svg "octicon-milestone" 16 "tw-mr-1"}}
|
{{svg "octicon-milestone" 16 "tw-mr-1"}}
|
||||||
{{.Name}}
|
{{.Name}}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -72,7 +72,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "repo/issue/view_content/comments" .}}
|
{{template "repo/issue/view_content/comments" .}}
|
||||||
|
<div id="insert-timeline"></div>
|
||||||
|
|
||||||
{{if and .Issue.IsPull (not $.Repository.IsArchived)}}
|
{{if and .Issue.IsPull (not $.Repository.IsArchived)}}
|
||||||
{{template "repo/issue/view_content/pull".}}
|
{{template "repo/issue/view_content/pull".}}
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-milestone dropdown">
|
<div id="milestone-section" hx-swap="morph" hx-target="this" hx-indicator="this">
|
||||||
<a class="text muted flex-text-block">
|
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-milestone dropdown">
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong>
|
<a class="text muted flex-text-block">
|
||||||
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
|
<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong>
|
||||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
|
||||||
{{end}}
|
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
||||||
</a>
|
{{end}}
|
||||||
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/milestone">
|
</a>
|
||||||
{{template "repo/issue/milestone/select_menu" .}}
|
<div class="menu">
|
||||||
</div>
|
{{template "repo/issue/milestone/select_menu" .}}
|
||||||
</div>
|
</div>
|
||||||
<div class="ui select-milestone list">
|
</div>
|
||||||
<span class="no-select item {{if .Issue.Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
|
<div class="ui select-milestone list">
|
||||||
<div class="selected">
|
<span class="no-select item {{if .Issue.Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
|
||||||
{{if .Issue.Milestone}}
|
<div class="selected">
|
||||||
<a class="item muted sidebar-item-link" href="{{.RepoLink}}/milestone/{{.Issue.Milestone.ID}}">
|
{{if .Issue.Milestone}}
|
||||||
{{svg "octicon-milestone" 18 "tw-mr-2"}}
|
<a class="item muted sidebar-item-link" href="{{.RepoLink}}/milestone/{{.Issue.Milestone.ID}}">
|
||||||
{{.Issue.Milestone.Name}}
|
{{svg "octicon-milestone" 18 "tw-mr-2"}}
|
||||||
</a>
|
{{.Issue.Milestone.Name}}
|
||||||
{{end}}
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -84,3 +84,27 @@ test('Issue: Labels', async ({browser}, workerInfo) => {
|
||||||
await expect(labelList.filter({hasText: 'label2'})).not.toBeVisible();
|
await expect(labelList.filter({hasText: 'label2'})).not.toBeVisible();
|
||||||
await expect(labelList.filter({hasText: 'label1'})).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');
|
||||||
|
});
|
||||||
|
|
|
@ -270,9 +270,7 @@ export function initRepoCommentForm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let icon = '';
|
let icon = '';
|
||||||
if (input_id === '#milestone_id') {
|
if (input_id === '#project_id') {
|
||||||
icon = svg('octicon-milestone', 18, 'tw-mr-2');
|
|
||||||
} else if (input_id === '#project_id') {
|
|
||||||
icon = svg('octicon-project', 18, 'tw-mr-2');
|
icon = svg('octicon-project', 18, 'tw-mr-2');
|
||||||
} else if (input_id === '#assignee_id') {
|
} else if (input_id === '#assignee_id') {
|
||||||
icon = `<img class="ui avatar image tw-mr-2" alt="avatar" src=${$(this).data('avatar')}>`;
|
icon = `<img class="ui avatar image tw-mr-2" alt="avatar" src=${$(this).data('avatar')}>`;
|
||||||
|
@ -313,7 +311,6 @@ export function initRepoCommentForm() {
|
||||||
|
|
||||||
// Milestone, Assignee, Project
|
// Milestone, Assignee, Project
|
||||||
selectItem('.select-project', '#project_id');
|
selectItem('.select-project', '#project_id');
|
||||||
selectItem('.select-milestone', '#milestone_id');
|
|
||||||
selectItem('.select-assignee', '#assignee_id');
|
selectItem('.select-assignee', '#assignee_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue