Compare commits

...

25 Commits

Author SHA1 Message Date
Lunny Xiao 5fa04476db
Merge 4afdf7c340 into 5c236bd4c0 2024-05-05 17:19:38 +03:00
wxiaoguang 5c236bd4c0
Fix issue/PR title edit (#30858)
1. "enter" doesn't work (I think it is the last enter support for #14843)
2. if a branch name contains something like `&`, then the branch selector doesn't update
2024-05-05 13:09:41 +00:00
yp05327 ecd1d96f49
Add result check in TestAPIEditUser (#29674)
Fix #29514
There are too many usage of `NewRequestWithValues`, so there's no need
to check all of them.
Just one is enough I think.
2024-05-05 02:10:20 +00:00
Lunny Xiao 4afdf7c340 Merge branch 'main' into lunny/add_comment_move_issue_column 2024-05-02 13:47:04 +08:00
Lunny Xiao b6faeefa97
Update models/migrations/v1_23/v297.go
Co-authored-by: yp05327 <576951401@qq.com>
2024-05-02 13:23:31 +08:00
Lunny Xiao 22a1e68ff2 Merge branch 'main' into lunny/add_comment_move_issue_column 2024-04-17 19:52:59 +08:00
Lunny Xiao 5b8fe99c20
Fix lint 2024-04-17 19:52:56 +08:00
Lunny Xiao 003d8330e6 Merge branch 'main' into lunny/add_comment_move_issue_column 2024-04-15 17:39:49 +08:00
Lunny Xiao 61087a2496
Fix metadata name 2024-04-08 14:48:11 +08:00
Lunny Xiao 1c478e6e8b Merge branch 'main' into lunny/add_comment_move_issue_column 2024-04-08 14:42:40 +08:00
Lunny Xiao 2370219c72 Merge branch 'lunny/add_comment_move_issue_column' of github.com:lunny/gitea into lunny/add_comment_move_issue_column 2024-03-31 12:38:06 +08:00
Lunny Xiao 351e58d6fc Merge branch 'main' into lunny/add_comment_move_issue_column 2024-03-31 12:30:24 +08:00
silverwind 6dbb557975
Merge branch 'main' into lunny/add_comment_move_issue_column 2024-03-16 19:51:47 +01:00
Lunny Xiao 385462e132 Merge branch 'main' into lunny/add_comment_move_issue_column 2024-03-16 21:42:21 +08:00
Lunny Xiao 9c13665f2a Merge branch 'main' into lunny/add_comment_move_issue_column 2024-03-04 16:56:15 +08:00
Lunny Xiao 327e1fda02 Merge branch 'lunny/add_comment_move_issue_column' of github.com:lunny/gitea into lunny/add_comment_move_issue_column 2024-02-29 10:35:03 +08:00
Lunny Xiao 323bf5ed86 Merge branch 'main' into lunny/add_comment_move_issue_column 2024-02-29 10:34:34 +08:00
Lunny Xiao 5e6ff769d5
Ignore the fields on database when it's empty 2024-02-29 10:34:23 +08:00
wxiaoguang 7bdc0e187d
Update templates/repo/issue/view_content/comments.tmpl 2024-02-25 23:37:21 +08:00
wxiaoguang b0a503043b
Apply suggestions from code review 2024-02-25 23:35:41 +08:00
Lunny Xiao 5881f9e357 Use new template HTML method 2024-02-25 19:19:21 +08:00
Lunny Xiao 41df70ddba Use Escape 2024-02-25 19:19:21 +08:00
Lunny Xiao 09be5ac173 Fix possible xss bug 2024-02-25 19:19:20 +08:00
Lunny Xiao 0be30d9e3a Fix bug 2024-02-25 19:19:20 +08:00
Lunny Xiao b91eefaa64 Add issue comment when moving issues from one column to another of the project 2024-02-25 19:19:20 +08:00
19 changed files with 327 additions and 199 deletions

View File

@ -220,6 +220,13 @@ func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
return lang.TrString("repo.issues.role." + string(r) + "_helper")
}
// CommentMetaData stores metadata for a comment, these data will not be changed once inserted into database
type CommentMetaData struct {
ProjectColumnID int64 `json:"project_column_id,omitempty"`
ProjectColumnTitle string `json:"project_column_title,omitempty"`
ProjectTitle string `json:"project_title,omitempty"`
}
// Comment represents a comment in commit and issue page.
type Comment struct {
ID int64 `xorm:"pk autoincr"`
@ -292,6 +299,8 @@ type Comment struct {
RefAction references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves
RefIsPull bool
CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
RefRepo *repo_model.Repository `xorm:"-"`
RefIssue *Issue `xorm:"-"`
RefComment *Comment `xorm:"-"`
@ -794,6 +803,15 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
LabelID = opts.Label.ID
}
var commentMetaData *CommentMetaData
if opts.ProjectColumnTitle != "" {
commentMetaData = &CommentMetaData{
ProjectColumnID: opts.ProjectColumnID,
ProjectColumnTitle: opts.ProjectColumnTitle,
ProjectTitle: opts.ProjectTitle,
}
}
comment := &Comment{
Type: opts.Type,
PosterID: opts.Doer.ID,
@ -827,6 +845,7 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
RefIsPull: opts.RefIsPull,
IsForcePush: opts.IsForcePush,
Invalidated: opts.Invalidated,
CommentMetaData: commentMetaData,
}
if _, err = e.Insert(comment); err != nil {
return nil, err
@ -979,34 +998,37 @@ type CreateCommentOptions struct {
Issue *Issue
Label *Label
DependentIssueID int64
OldMilestoneID int64
MilestoneID int64
OldProjectID int64
ProjectID int64
TimeID int64
AssigneeID int64
AssigneeTeamID int64
RemovedAssignee bool
OldTitle string
NewTitle string
OldRef string
NewRef string
CommitID int64
CommitSHA string
Patch string
LineNum int64
TreePath string
ReviewID int64
Content string
Attachments []string // UUIDs of attachments
RefRepoID int64
RefIssueID int64
RefCommentID int64
RefAction references.XRefAction
RefIsPull bool
IsForcePush bool
Invalidated bool
DependentIssueID int64
OldMilestoneID int64
MilestoneID int64
OldProjectID int64
ProjectID int64
ProjectTitle string
ProjectColumnID int64
ProjectColumnTitle string
TimeID int64
AssigneeID int64
AssigneeTeamID int64
RemovedAssignee bool
OldTitle string
NewTitle string
OldRef string
NewRef string
CommitID int64
CommitSHA string
Patch string
LineNum int64
TreePath string
ReviewID int64
Content string
Attachments []string // UUIDs of attachments
RefRepoID int64
RefIssueID int64
RefCommentID int64
RefAction references.XRefAction
RefIsPull bool
IsForcePush bool
Invalidated bool
}
// GetCommentByID returns the comment by given ID.

View File

@ -434,6 +434,7 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er
Join("INNER", "issue", "issue.id = comment.issue_id").
In("issue.id", issuesIDs[:limit]).
Where(cond).
NoAutoCondition().
Rows(new(Comment))
if err != nil {
return err

View File

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/models/migrations/v1_20"
"code.gitea.io/gitea/models/migrations/v1_21"
"code.gitea.io/gitea/models/migrations/v1_22"
"code.gitea.io/gitea/models/migrations/v1_23"
"code.gitea.io/gitea/models/migrations/v1_6"
"code.gitea.io/gitea/models/migrations/v1_7"
"code.gitea.io/gitea/models/migrations/v1_8"
@ -587,6 +588,9 @@ var migrations = []Migration{
NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable),
// Gitea 1.22.0-rc1 ends at 299
// v299 -> v300
NewMigration("Add metadata column for comment table", v1_23.AddCommentMetaDataColumn),
}
// GetCurrentDBVersion returns the current db version

View File

@ -0,0 +1,22 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
import (
"xorm.io/xorm"
)
// CommentMetaData stores metadata for a comment, these data will not be changed once inserted into database
type CommentMetaData struct {
ProjectColumnID int64 `json:"project_column_id"`
ProjectColumnTitle string `json:"project_column_title"`
ProjectTitle string `json:"project_title"`
}
func AddCommentMetaDataColumn(x *xorm.Engine) error {
type Comment struct {
CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
}
return x.Sync(new(Comment))
}

View File

@ -5,7 +5,6 @@ package project
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
@ -75,33 +74,6 @@ func (p *Project) NumOpenIssues(ctx context.Context) int {
return int(c)
}
// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column
func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
sess := db.GetEngine(ctx)
issueIDs := make([]int64, 0, len(sortedIssueIDs))
for _, issueID := range sortedIssueIDs {
issueIDs = append(issueIDs, issueID)
}
count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count()
if err != nil {
return err
}
if int(count) != len(sortedIssueIDs) {
return fmt.Errorf("all issues have to be added to a project first")
}
for sorting, issueID := range sortedIssueIDs {
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID)
if err != nil {
return err
}
}
return nil
})
}
func (b *Board) removeIssues(ctx context.Context) error {
_, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", b.ID)
return err

View File

@ -1469,6 +1469,7 @@ issues.remove_labels = removed the %s labels %s
issues.add_remove_labels = added %s and removed %s labels %s
issues.add_milestone_at = `added this to the <b>%s</b> milestone %s`
issues.add_project_at = `added this to the <b>%s</b> project %s`
issues.move_to_column_of_project = `moved this to %s in %s on %s`
issues.change_milestone_at = `modified the milestone from <b>%s</b> to <b>%s</b> %s`
issues.change_project_at = `modified the project from <b>%s</b> to <b>%s</b> %s`
issues.remove_milestone_at = `removed this from the <b>%s</b> milestone %s`

View File

@ -24,6 +24,7 @@ import (
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
project_service "code.gitea.io/gitea/services/projects"
)
const (
@ -640,6 +641,7 @@ func MoveIssues(ctx *context.Context) {
form := &movedIssuesForm{}
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
ctx.ServerError("DecodeMovedIssuesForm", err)
return
}
issueIDs := make([]int64, 0, len(form.Issues))
@ -671,7 +673,7 @@ func MoveIssues(ctx *context.Context) {
}
}
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil {
if err = project_service.MoveIssuesOnProjectBoard(ctx, ctx.Doer, board, sortedIssueIDs); err != nil {
ctx.ServerError("MoveIssuesOnProjectBoard", err)
return
}

View File

@ -1680,6 +1680,11 @@ func ViewIssue(ctx *context.Context) {
if comment.ProjectID > 0 && comment.Project == nil {
comment.Project = ghostProject
}
} else if comment.Type == issues_model.CommentTypeProjectBoard {
if err = comment.LoadProject(ctx); err != nil {
ctx.ServerError("LoadProject", err)
return
}
} else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest {
if err = comment.LoadAssigneeUserAndTeam(ctx); err != nil {
ctx.ServerError("LoadAssigneeUserAndTeam", err)

View File

@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
project_service "code.gitea.io/gitea/services/projects"
)
const (
@ -659,7 +660,7 @@ func MoveIssues(ctx *context.Context) {
}
}
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil {
if err = project_service.MoveIssuesOnProjectBoard(ctx, ctx.Doer, board, sortedIssueIDs); err != nil {
ctx.ServerError("MoveIssuesOnProjectBoard", err)
return
}

View File

@ -0,0 +1,77 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project"
user_model "code.gitea.io/gitea/models/user"
)
// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column
func MoveIssuesOnProjectBoard(ctx context.Context, doer *user_model.User, column *project_model.Board, sortedIssueIDs map[int64]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
issueIDs := make([]int64, 0, len(sortedIssueIDs))
for _, issueID := range sortedIssueIDs {
issueIDs = append(issueIDs, issueID)
}
count, err := db.GetEngine(ctx).
Where("project_id=?", column.ProjectID).
In("issue_id", issueIDs).
Count(new(project_model.ProjectIssue))
if err != nil {
return err
}
if int(count) != len(sortedIssueIDs) {
return fmt.Errorf("all issues have to be added to a project first")
}
issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
if err != nil {
return err
}
if _, err := issues.LoadRepositories(ctx); err != nil {
return err
}
project, err := project_model.GetProjectByID(ctx, column.ProjectID)
if err != nil {
return err
}
for sorting, issueID := range sortedIssueIDs {
_, err = db.Exec(ctx, "UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
if err != nil {
return err
}
var curIssue *issues_model.Issue
for _, issue := range issues {
if issue.ID == issueID {
curIssue = issue
break
}
}
// add timeline to issue
if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypeProjectBoard,
Doer: doer,
Repo: curIssue.Repo,
Issue: curIssue,
ProjectID: column.ProjectID,
ProjectTitle: project.Title,
ProjectColumnID: column.ID,
ProjectColumnTitle: column.Title,
}); err != nil {
return err
}
}
return nil
})
}

View File

@ -600,6 +600,22 @@
</span>
</div>
{{end}}
{{else if eq .Type 31}}
{{if not $.UnitProjectsGlobalDisabled}}
<div class="timeline-item event" id="{{.HashTag}}">
<span class="badge">{{svg "octicon-project"}}</span>
{{template "shared/user/avatarlink" dict "user" .Poster}}
<span class="text grey muted-links">
{{template "shared/user/authorlink" .Poster}}
{{$newProjectDisplay := .CommentMetaData.ProjectTitle}}
{{if .Project}}
{{$trKey := printf "projects.type-%d.display_name" .Project.Type}}
{{$newProjectDisplay = HTMLFormat `%s <a href="%s"><span data-tooltip-content="%s">%s</span></a>` (svg .Project.IconName) (.Project.Link ctx) (ctx.Locale.Tr $trKey) .Project.Title}}
{{end}}
{{ctx.Locale.Tr "repo.issues.move_to_column_of_project" .CommentMetaData.ProjectColumnTitle $newProjectDisplay $createdStr}}
</span>
</div>
{{end}}
{{else if eq .Type 32}}
<div class="timeline-item-group">
<div class="timeline-item event" id="{{.HashTag}}">

View File

@ -4,29 +4,36 @@
</div>
{{end}}
<div class="issue-title-header">
<div class="issue-title" id="issue-title-wrapper">
{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<div class="issue-title" id="issue-title-display">
<h1 class="gt-word-break">
<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
</span>
<div id="edit-title-input" class="ui input tw-flex-1 tw-hidden">
<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">
</div>
{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}}
<span class="index">#{{.Issue.Index}}</span>
</h1>
<div class="issue-title-buttons">
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} tw-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
{{if $canEditIssueTitle}}
<button id="issue-title-edit-show" class="ui small basic button">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
{{end}}
{{if not .Issue.IsPull}}
<a role="button" class="ui small primary button new-issue-button tw-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
<a role="button" class="ui small primary button" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
{{end}}
</div>
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<div class="edit-buttons">
<button id="cancel-edit-title" class="ui small basic button in-edit tw-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
<button id="save-edit-title" class="ui small primary button in-edit tw-hidden tw-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button>
</div>
{{end}}
</div>
{{if $canEditIssueTitle}}
<div class="ui form issue-title tw-hidden" id="issue-title-editor">
<div class="ui input tw-flex-1">
<input value="{{.Issue.Title}}" data-old-title="{{.Issue.Title}}" maxlength="255" autocomplete="off">
</div>
<div class="issue-title-buttons">
<button class="ui small basic cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
<button class="ui small primary button"
data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title"
{{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>
{{ctx.Locale.Tr "repo.issues.save"}}
</button>
</div>
</div>
{{end}}
<div class="issue-title-meta">
{{if .HasMerged}}
<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
@ -63,14 +70,14 @@
{{end}}
{{else}}
{{if .Issue.OriginalAuthor}}
<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
<span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
{{else}}
<span id="pull-desc" class="pull-desc">
<span id="pull-desc-display" class="pull-desc">
<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}
</span>
{{end}}
<span id="pull-desc-edit" class="tw-hidden flex-text-block">
<span id="pull-desc-editor" class="tw-hidden flex-text-block">
<div class="ui floating filter dropdown">
<div class="ui basic small button tw-mr-0">
<span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span>

View File

@ -195,14 +195,17 @@ func TestAPIEditUser(t *testing.T) {
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
urlStr := fmt.Sprintf("/api/v1/admin/users/%s", "user2")
fullNameToChange := "Full Name User 2"
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
// required
"login_name": "user2",
"source_id": "0",
// to change
"full_name": "Full Name User 2",
"full_name": fullNameToChange,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
assert.Equal(t, fullNameToChange, user2.FullName)
empty := ""
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
@ -216,7 +219,7 @@ func TestAPIEditUser(t *testing.T) {
json.Unmarshal(resp.Body.Bytes(), &errMap)
assert.EqualValues(t, "e-mail invalid [email: ]", errMap["message"].(string))
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
assert.False(t, user2.IsRestricted)
bTrue := true
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{

View File

@ -144,7 +144,7 @@ func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
val := htmlDoc.doc.Find("#issue-title").Text()
val := htmlDoc.doc.Find("#issue-title-display").Text()
assert.Contains(t, val, title)
val = htmlDoc.doc.Find(".comment .render-content p").First().Text()
assert.Equal(t, content, val)

View File

@ -125,7 +125,7 @@ func TestPullCreate_TitleEscape(t *testing.T) {
req := NewRequest(t, "GET", url)
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
editTestTitleURL, exists := htmlDoc.doc.Find("#save-edit-title").First().Attr("data-update-url")
editTestTitleURL, exists := htmlDoc.doc.Find(".issue-title-buttons button[data-update-url]").First().Attr("data-update-url")
assert.True(t, exists, "The template has changed")
req = NewRequestWithValues(t, "POST", editTestTitleURL, map[string]string{

View File

@ -575,34 +575,7 @@ td .commit-summary {
display: inline-block;
}
.issue-title-header {
width: 100%;
padding-bottom: 4px;
margin-bottom: 1rem;
}
.issue-title-meta {
display: flex;
align-items: center;
}
.repository.view.issue .issue-title-buttons,
.repository.view.issue .edit-buttons {
display: flex;
}
@media (max-width: 767.98px) {
.repository.view.issue .issue-title {
flex-direction: column;
}
.repository.view.issue .issue-title-buttons,
.repository.view.issue .edit-buttons {
width: 100%;
justify-content: space-between;
}
.repository.view.issue .edit-buttons {
margin-top: .5rem;
}
.comment.form .issue-content-left .avatar {
display: none;
}
@ -617,15 +590,37 @@ td .commit-summary {
}
}
/* issue title & meta & edit */
.issue-title-header {
width: 100%;
padding-bottom: 4px;
margin-bottom: 1rem;
}
.issue-title-meta {
display: flex;
align-items: center;
}
.repository.view.issue .issue-title-buttons {
display: flex;
gap: 0.5em;
}
.repository.view.issue .issue-title-buttons > .ui.button {
margin: 0;
height: 35px;
}
.repository.view.issue .issue-title {
display: flex;
align-items: center;
gap: 0.5em;
margin-bottom: 8px;
min-height: 40px; /* avoid layout shift on edit */
}
.repository.view.issue .issue-title h1 {
display: flex;
align-items: center;
flex: 1;
width: 100%;
font-weight: var(--font-weight-normal);
@ -633,14 +628,24 @@ td .commit-summary {
line-height: 40px;
margin: 0;
padding-right: 0.25rem;
min-height: 41px; /* avoid layout shift on edit */
}
.repository.view.issue .issue-title h1 .ui.input {
font-size: 0.5em;
@media (max-width: 767.98px) {
.repository.view.issue .issue-title {
flex-direction: column;
}
.repository.view.issue .issue-title-buttons {
width: 100%;
justify-content: space-between;
}
}
.repository.view.issue .issue-title h1 .ui.input input {
.repository.view.issue .issue-title .ui.input {
width: 100%;
height: 35px;
}
.repository.view.issue .issue-title .ui.input input {
font-size: 1.5em;
padding: 2px .5rem;
}
@ -653,10 +658,6 @@ td .commit-summary {
margin-right: 10px;
}
.issue-title .edit-zone {
margin-top: 10px;
}
.issue-state-label {
display: flex !important;
align-items: center !important;

View File

@ -47,10 +47,18 @@ export function initFootLanguageMenu() {
export function initGlobalEnterQuickSubmit() {
document.addEventListener('keydown', (e) => {
const isQuickSubmitEnter = ((e.ctrlKey && !e.altKey) || e.metaKey) && (e.key === 'Enter');
if (isQuickSubmitEnter && e.target.matches('textarea')) {
e.preventDefault();
handleGlobalEnterQuickSubmit(e.target);
if (e.key !== 'Enter') return;
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
if (hasCtrlOrMeta && e.target.matches('textarea')) {
if (handleGlobalEnterQuickSubmit(e.target)) {
e.preventDefault();
}
} else if (e.target.matches('input') && !e.target.closest('form')) {
// input in a normal form could handle Enter key by default, so we only handle the input outside a form
// eslint-disable-next-line unicorn/no-lonely-if
if (handleGlobalEnterQuickSubmit(e.target)) {
e.preventDefault();
}
}
});
}

View File

@ -3,16 +3,17 @@ export function handleGlobalEnterQuickSubmit(target) {
if (form) {
if (!form.checkValidity()) {
form.reportValidity();
return;
} else {
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
}
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
return;
return true;
}
form = target.closest('.ui.form');
if (form) {
form.querySelector('.ui.primary.button')?.click();
return true;
}
return false;
}

View File

@ -7,6 +7,7 @@ import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkd
import {toAbsoluteUrl} from '../utils.js';
import {initDropzone} from './common-global.js';
import {POST, GET} from '../modules/fetch.js';
import {showErrorToast} from '../modules/toast.js';
const {appSubUrl} = window.config;
@ -602,85 +603,69 @@ export function initRepoIssueWipToggle() {
});
}
async function pullrequest_targetbranch_change(update_url) {
const targetBranch = $('#pull-target-branch').data('branch');
const $branchTarget = $('#branch_target');
if (targetBranch === $branchTarget.text()) {
window.location.reload();
return false;
}
try {
await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})});
} catch (error) {
console.error(error);
} finally {
window.location.reload();
}
}
export function initRepoIssueTitleEdit() {
// Edit issue title
const $issueTitle = $('#issue-title');
const $editInput = $('#edit-title-input input');
const issueTitleDisplay = document.querySelector('#issue-title-display');
const issueTitleEditor = document.querySelector('#issue-title-editor');
if (!issueTitleEditor) return;
const editTitleToggle = function () {
toggleElem($issueTitle);
toggleElem('.not-in-edit');
toggleElem('#edit-title-input');
toggleElem('#pull-desc');
toggleElem('#pull-desc-edit');
toggleElem('.in-edit');
toggleElem('.new-issue-button');
document.getElementById('issue-title-wrapper')?.classList.toggle('edit-active');
$editInput[0].focus();
$editInput[0].select();
return false;
};
$('#edit-title').on('click', editTitleToggle);
$('#cancel-edit-title').on('click', editTitleToggle);
$('#save-edit-title').on('click', editTitleToggle).on('click', async function () {
const pullrequest_target_update_url = this.getAttribute('data-target-update-url');
if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) {
$editInput.val($issueTitle.text());
await pullrequest_targetbranch_change(pullrequest_target_update_url);
} else {
try {
const params = new URLSearchParams();
params.append('title', $editInput.val());
const response = await POST(this.getAttribute('data-update-url'), {data: params});
const data = await response.json();
$editInput.val(data.title);
$issueTitle.text(data.title);
if (pullrequest_target_update_url) {
await pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window
} else {
window.location.reload();
}
} catch (error) {
console.error(error);
}
const issueTitleInput = issueTitleEditor.querySelector('input');
const oldTitle = issueTitleInput.getAttribute('data-old-title');
issueTitleDisplay.querySelector('#issue-title-edit-show').addEventListener('click', () => {
hideElem(issueTitleDisplay);
hideElem('#pull-desc-display');
showElem(issueTitleEditor);
showElem('#pull-desc-editor');
if (!issueTitleInput.value.trim()) {
issueTitleInput.value = oldTitle;
}
issueTitleInput.focus();
});
issueTitleEditor.querySelector('.ui.cancel.button').addEventListener('click', () => {
hideElem(issueTitleEditor);
hideElem('#pull-desc-editor');
showElem(issueTitleDisplay);
showElem('#pull-desc-display');
});
const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button');
editSaveButton.addEventListener('click', async () => {
const prTargetUpdateUrl = editSaveButton.getAttribute('data-target-update-url');
const newTitle = issueTitleInput.value.trim();
try {
if (newTitle && newTitle !== oldTitle) {
const resp = await POST(editSaveButton.getAttribute('data-update-url'), {data: new URLSearchParams({title: newTitle})});
if (!resp.ok) {
throw new Error(`Failed to update issue title: ${resp.statusText}`);
}
}
if (prTargetUpdateUrl) {
const newTargetBranch = document.querySelector('#pull-target-branch').getAttribute('data-branch');
const oldTargetBranch = document.querySelector('#branch_target').textContent;
if (newTargetBranch !== oldTargetBranch) {
const resp = await POST(prTargetUpdateUrl, {data: new URLSearchParams({target_branch: newTargetBranch})});
if (!resp.ok) {
throw new Error(`Failed to update PR target branch: ${resp.statusText}`);
}
}
}
window.location.reload();
} catch (error) {
console.error(error);
showErrorToast(error.message);
}
return false;
});
}
export function initRepoIssueBranchSelect() {
const changeBranchSelect = function () {
const $selectionTextField = $('#pull-target-branch');
const baseName = $selectionTextField.data('basename');
const branchNameNew = $(this).data('branch');
const branchNameOld = $selectionTextField.data('branch');
// Replace branch name to keep translation from HTML template
$selectionTextField.html($selectionTextField.html().replace(
`${baseName}:${branchNameOld}`,
`${baseName}:${branchNameNew}`,
));
$selectionTextField.data('branch', branchNameNew); // update branch name in setting
};
$('#branch-select > .item').on('click', changeBranchSelect);
document.querySelector('#branch-select')?.addEventListener('click', (e) => {
const el = e.target.closest('.item[data-branch]');
if (!el) return;
const pullTargetBranch = document.querySelector('#pull-target-branch');
const baseName = pullTargetBranch.getAttribute('data-basename');
const branchNameNew = el.getAttribute('data-branch');
const branchNameOld = pullTargetBranch.getAttribute('data-branch');
pullTargetBranch.textContent = pullTargetBranch.textContent.replace(`${baseName}:${branchNameOld}`, `${baseName}:${branchNameNew}`);
pullTargetBranch.setAttribute('data-branch', branchNameNew);
});
}
export function initSingleCommentEditor($commentForm) {