mirror of https://github.com/go-gitea/gitea.git
Compare commits
25 Commits
4c69b3de6b
...
5fa04476db
Author | SHA1 | Date |
---|---|---|
Lunny Xiao | 5fa04476db | |
wxiaoguang | 5c236bd4c0 | |
yp05327 | ecd1d96f49 | |
Lunny Xiao | 4afdf7c340 | |
Lunny Xiao | b6faeefa97 | |
Lunny Xiao | 22a1e68ff2 | |
Lunny Xiao | 5b8fe99c20 | |
Lunny Xiao | 003d8330e6 | |
Lunny Xiao | 61087a2496 | |
Lunny Xiao | 1c478e6e8b | |
Lunny Xiao | 2370219c72 | |
Lunny Xiao | 351e58d6fc | |
silverwind | 6dbb557975 | |
Lunny Xiao | 385462e132 | |
Lunny Xiao | 9c13665f2a | |
Lunny Xiao | 327e1fda02 | |
Lunny Xiao | 323bf5ed86 | |
Lunny Xiao | 5e6ff769d5 | |
wxiaoguang | 7bdc0e187d | |
wxiaoguang | b0a503043b | |
Lunny Xiao | 5881f9e357 | |
Lunny Xiao | 41df70ddba | |
Lunny Xiao | 09be5ac173 | |
Lunny Xiao | 0be30d9e3a | |
Lunny Xiao | b91eefaa64 |
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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}}">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue