diff --git a/go.sum b/go.sum index 7710cbc63f5a2..eb00e7745cfb1 100644 --- a/go.sum +++ b/go.sum @@ -288,6 +288,7 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethantkoenig/rupture v0.0.0-20181029165146-c3b3b810dc77 h1:ZLWiTTzTUBb0WEXUxobYI/RxULIzOoIP7pgfDd4p1cw= github.com/ethantkoenig/rupture v0.0.0-20181029165146-c3b3b810dc77/go.mod h1:MkKY/CB98aVE4VxO63X5vTQKUgcn+3XP15LMASe3lYs= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= @@ -655,6 +656,7 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= +github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= @@ -684,6 +686,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= @@ -1025,6 +1028,7 @@ github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/steveyen/gtreap v0.1.0 h1:CjhzTa274PyJLJuMZwIzCO1PfC00oRa8d1Kc78bFXJM= github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7Z4dM9/Y= @@ -1069,6 +1073,7 @@ github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnl github.com/unknwon/i18n v0.0.0-20190805065654-5c6446a380b6/go.mod h1:+5rDk6sDGpl3azws3O+f+GpFSyN9GVr0K8cvQLQM2ZQ= github.com/unknwon/i18n v0.0.0-20200823051745-09abd91c7f2c h1:679/gJXwrsHC3RATr0YYjZvDMJPYN7W9FGSGNoLmKxM= github.com/unknwon/i18n v0.0.0-20200823051745-09abd91c7f2c/go.mod h1:+5rDk6sDGpl3azws3O+f+GpFSyN9GVr0K8cvQLQM2ZQ= +github.com/unknwon/paginater v0.0.0-20200328080006-042474bd0eae h1:ihaXiJkaca54IaCSnEXtE/uSZOmPxKZhDfVLrzZLFDs= github.com/unknwon/paginater v0.0.0-20200328080006-042474bd0eae/go.mod h1:1fdkY6xxl6ExVs2QFv7R0F5IRZHKA8RahhB9fMC9RvM= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= diff --git a/models/issue.go b/models/issue.go index ee75623f53025..81fdee2becc43 100644 --- a/models/issue.go +++ b/models/issue.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" @@ -63,6 +64,7 @@ type Issue struct { Reactions ReactionList `xorm:"-"` TotalTrackedTime int64 `xorm:"-"` Assignees []*User `xorm:"-"` + ProjectIssueID ProjectIssue // IsLocked limits commenting abilities to users on an issue // with write access @@ -1100,8 +1102,10 @@ type IssuesOptions struct { ExcludedLabelNames []string SortType string IssueIDs []int64 + ExcludeProjectID int64 // prioritize issues from this repo - PriorityRepoID int64 + PriorityRepoID int64 + RenderEmojiTitle util.OptionalBool } // sortIssuesSession sort an issues-related session based on the provided @@ -1180,17 +1184,21 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) { if opts.ProjectID > 0 { sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). - And("project_issue.project_id=?", opts.ProjectID) + And("project_issue.project_id=?", opts.ProjectID).OrderBy("`project_issue`.priority") } if opts.ProjectBoardID != 0 { if opts.ProjectBoardID > 0 { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID})) + sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID}).OrderBy("`project_issue`.priority")) } else { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) + sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}).OrderBy("`project_issue`.priority")) } } + if opts.ExcludeProjectID != 0 { + sess.NotIn("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_id": opts.ExcludeProjectID}).OrderBy("`project_issue`.priority")) + } + switch opts.IsPull { case util.OptionalBoolTrue: sess.And("issue.is_pull=?", true) @@ -1280,7 +1288,15 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) { if err := IssueList(issues).LoadAttributes(); err != nil { return nil, fmt.Errorf("LoadAttributes: %v", err) } - + if opts.RenderEmojiTitle == util.OptionalBoolTrue { + var issuesWithEmojis []*Issue + for _, issue := range issues { + title := string(markup.RenderEmoji(issue.Title)) + issue.Title = title + issuesWithEmojis = append(issuesWithEmojis, issue) + } + return issuesWithEmojis, nil + } return issues, nil } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 4715f192c157e..26c09e5541af5 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -252,6 +252,8 @@ var migrations = []Migration{ NewMigration("ensure repo topics are up-to-date", fixRepoTopics), // v158 -> v159 NewMigration("code comment replies should have the commitID of the review they are replying to", updateCodeCommentReplies), + // v159 -> v160 + NewMigration("Add projects boards and issues priorities", addProjectsIssuesBoardsPriority), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v159.go b/models/migrations/v159.go new file mode 100644 index 0000000000000..7e83abb069ffb --- /dev/null +++ b/models/migrations/v159.go @@ -0,0 +1,26 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func addProjectsIssuesBoardsPriority(x *xorm.Engine) error { + // ProjectIssue saves relation from issue to a project + type ProjectIssue struct { + Priority int `xorm:"NOT NULL DEFAULT 0"` + } + + if err := x.Sync2(new(ProjectIssue)); err != nil { + return err + } + + type ProjectBoard struct { + Priority int `xorm:"NOT NULL DEFAULT 0"` + } + + return x.Sync2(new(ProjectBoard)) +} diff --git a/models/project.go b/models/project.go index e032da351dde4..0afa72753f2db 100644 --- a/models/project.go +++ b/models/project.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -47,6 +48,7 @@ type Project struct { IsClosed bool `xorm:"INDEX"` BoardType ProjectBoardType Type ProjectType + Repo *Repository `xorm:"-"` RenderedContent string `xorm:"-"` @@ -305,3 +307,24 @@ func deleteProjectByID(e Engine, id int64) error { return updateRepositoryProjectCount(e, p.RepoID) } + +// LoadRepository loads repository of a given project +func (p *Project) LoadRepository() error { + return p.loadRepository(x) +} + +// loadRepository loads repository of a given project +func (p *Project) loadRepository(e Engine) error { + if p.Repo != nil { + return nil + } + if p.Type == ProjectTypeRepository { + repo := &Repository{} + if _, err := e.ID(p.RepoID).Get(repo); err != nil { + log.Info("failed getting repo %v", err) + return err + } + p.Repo = repo + } + return nil +} diff --git a/models/project_board.go b/models/project_board.go index 260fc8304b22e..914d0d0857851 100644 --- a/models/project_board.go +++ b/models/project_board.go @@ -5,6 +5,7 @@ package models import ( + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -32,9 +33,10 @@ const ( // ProjectBoard is used to represent boards on a project type ProjectBoard struct { - ID int64 `xorm:"pk autoincr"` - Title string - Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board + ID int64 `xorm:"pk autoincr"` + Title string + Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board + Priority int `xorm:"NOT NULL DEFAULT 0"` ProjectID int64 `xorm:"INDEX NOT NULL"` CreatorID int64 `xorm:"NOT NULL"` @@ -42,7 +44,8 @@ type ProjectBoard struct { CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` - Issues []*Issue `xorm:"-"` + Issues []*Issue `xorm:"-"` + ProjectIssues []*ProjectIssue `xorm:"-"` } // IsProjectBoardTypeValid checks if the project board type is valid @@ -174,7 +177,7 @@ func GetProjectBoards(projectID int64) ([]*ProjectBoard, error) { var boards = make([]*ProjectBoard, 0, 5) - sess := x.Where("project_id=?", projectID) + sess := x.Where("project_id=?", projectID).OrderBy("priority") return boards, sess.Find(&boards) } @@ -187,34 +190,63 @@ func GetUncategorizedBoard(projectID int64) (*ProjectBoard, error) { }, nil } -// LoadIssues load issues assigned to this board -func (b *ProjectBoard) LoadIssues() (IssueList, error) { +// LoadProjectIssues load project issues assigned to this board +func (b *ProjectBoard) LoadProjectIssues() ([]*ProjectIssue, error) { var boardID int64 if !b.Default { boardID = b.ID } else { // Issues without ProjectBoardID - boardID = -1 + boardID = 0 } - issues, err := Issues(&IssuesOptions{ - ProjectBoardID: boardID, - ProjectID: b.ProjectID, - }) - b.Issues = issues - return issues, err + var projectIssues []*ProjectIssue + var issues []*Issue + if err := x.Table("issue"). + Cols("issue.id, issue.repo_id, issue.index, issue.poster_id,issue.name,issue.milestone_id,issue.is_closed,issue.is_pull,issue.created_unix,issue.updated_unix, project_issue.id as project_issue_id, project_issue.project_id as project_issue_project_id, project_issue.project_board_id as project_issue_project_board_id"). + Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). + Where("project_board_id = ? and project_id =?", + boardID, b.ProjectID). + OrderBy("`project_issue`.priority").Find(&issues); err != nil { + log.Error("LoadAttributes: %v", err) + } + + if err := IssueList(issues).LoadAttributes(); err != nil { + log.Error("LoadAttributes: %v", err) + } + + for _, issue := range issues { + projectIssue := issue.ProjectIssueID + projectIssue.Issue = issue + projectIssues = append(projectIssues, &projectIssue) + } + return projectIssues, nil } // LoadIssues load issues assigned to the boards -func (bs ProjectBoardList) LoadIssues() (IssueList, error) { - issues := make(IssueList, 0, len(bs)*10) +func (bs ProjectBoardList) LoadIssues() error { for i := range bs { - il, err := bs[i].LoadIssues() + il, err := bs[i].LoadProjectIssues() if err != nil { - return nil, err + return err + } + bs[i].ProjectIssues = il + } + return nil +} + +// UpdateBoardsPriority updates boards priority for a project +func UpdateBoardsPriority(boards []ProjectBoard) error { + sess := x.NewSession() + if err := sess.Begin(); err != nil { + return err + } + defer sess.Close() + for _, board := range boards { + if _, err := sess.ID(board.ID).Cols("priority").Update(&board); err != nil { + log.Info("failed updating board priorities %s", err) + return err } - bs[i].Issues = il - issues = append(issues, il...) } - return issues, nil + return sess.Commit() } diff --git a/models/project_issue.go b/models/project_issue.go index c41bfe5158679..a744bd5cfe444 100644 --- a/models/project_issue.go +++ b/models/project_issue.go @@ -7,14 +7,18 @@ package models import ( "fmt" + "code.gitea.io/gitea/modules/log" + "xorm.io/xorm" ) // ProjectIssue saves relation from issue to a project type ProjectIssue struct { - ID int64 `xorm:"pk autoincr"` - IssueID int64 `xorm:"INDEX"` - ProjectID int64 `xorm:"INDEX"` + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"INDEX"` + ProjectID int64 `xorm:"INDEX"` + Priority int `xorm:"NOT NULL DEFAULT 0"` + Issue *Issue `xorm:"-"` // If 0, then it has not been added to a specific board in the project ProjectBoardID int64 `xorm:"INDEX"` @@ -64,12 +68,12 @@ func (i *Issue) projectID(e Engine) int64 { return ip.ProjectID } -// ProjectBoardID return project board id if issue was assigned to one -func (i *Issue) ProjectBoardID() int64 { - return i.projectBoardID(x) +// LoadProjectBoardID return project board id if issue was assigned to one +func (i *Issue) LoadProjectBoardID() int64 { + return i.loadProjectBoardID(x) } -func (i *Issue) projectBoardID(e Engine) int64 { +func (i *Issue) loadProjectBoardID(e Engine) int64 { var ip ProjectIssue has, err := e.Where("issue_id=?", i.ID).Get(&ip) if err != nil || !has { @@ -139,9 +143,7 @@ func ChangeProjectAssign(issue *Issue, doer *User, newProjectID int64) error { } func addUpdateIssueProject(e *xorm.Session, issue *Issue, doer *User, newProjectID int64) error { - oldProjectID := issue.projectID(e) - if _, err := e.Where("project_issue.issue_id=?", issue.ID).Delete(&ProjectIssue{}); err != nil { return err } @@ -163,11 +165,21 @@ func addUpdateIssueProject(e *xorm.Session, issue *Issue, doer *User, newProject } } - _, err := e.Insert(&ProjectIssue{ - IssueID: issue.ID, - ProjectID: newProjectID, - }) - return err + var projectIssues []ProjectIssue + if newProjectID != 0 { + err := e.Where("issue_id = ? and project_id = ?", issue.ID, newProjectID).Find(&projectIssues) + if err != nil { + return err + } + if len(projectIssues) == 0 { + _, err := e.Insert(&ProjectIssue{ + IssueID: issue.ID, + ProjectID: newProjectID, + }) + return err + } + } + return nil } // ____ _ _ ____ _ @@ -208,3 +220,36 @@ func (pb *ProjectBoard) removeIssues(e Engine) error { _, err := e.Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", pb.ID) return err } + +// UpdateBoardIssuesPriority update given board issue priority +func UpdateBoardIssuesPriority(issues []ProjectIssue) ([]ProjectIssue, error) { + sess := x.NewSession() + if err := sess.Begin(); err != nil { + var updatedIssues []ProjectIssue + return updatedIssues, err + } + defer sess.Close() + var updatedIssues []ProjectIssue + for _, pissue := range issues { + if pissue.ID != 0 { + if _, err := sess.ID(pissue.ID).Cols("priority", "project_board_id").Update(&pissue); err != nil { + log.Info("failed updating cards priorities %s", err) + return updatedIssues, err + } + updatedIssues = append(updatedIssues, pissue) + } else { + var existingIssue ProjectIssue + if found, err := sess.Where("issue_id = ? and project_id = ?", pissue.IssueID, pissue.ProjectID). + Get(&existingIssue); err != nil { + log.Error("failed finding issue %s", err) + } else if !found { + if _, err := sess.Insert(&pissue); err != nil { + log.Info("failed inserting cards priorities %s", err) + return updatedIssues, err + } + updatedIssues = append(updatedIssues, pissue) + } + } + } + return updatedIssues, sess.Commit() +} diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 039b0cb583a09..8da91e616fb53 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -759,3 +759,23 @@ type DeadlineForm struct { func (f *DeadlineForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { return validate(errs, ctx.Data, f, ctx.Locale) } + +// UpdateBoardPriorityForm form for updating cards on drag and drop +type UpdateBoardPriorityForm struct { + Boards []models.ProjectBoard +} + +// Validate validates the fields +func (f *UpdateBoardPriorityForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { + return validate(errs, ctx.Data, f, ctx.Locale) +} + +// UpdateIssuePriorityBoardForm form for updating issue on drag and drop +type UpdateIssuePriorityBoardForm struct { + Issues []models.ProjectIssue `form:"issues" json:"issues"` +} + +// Validate validates the fields +func (f *UpdateIssuePriorityBoardForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { + return validate(errs, ctx.Data, f, ctx.Locale) +} diff --git a/modules/markup/html.go b/modules/markup/html.go index f5f811b59b6fc..05a5c5c760b0e 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -7,6 +7,7 @@ package markup import ( "bytes" "fmt" + "html/template" "net/url" "path" "path/filepath" @@ -287,9 +288,19 @@ func RenderDescriptionHTML( return ctx.postProcess(rawHTML) } +// RenderEmoji renders html text with emoji post processors +func RenderEmoji(text string) template.HTML { + renderedText, err := renderEmoji([]byte(template.HTMLEscapeString(text))) + if err != nil { + log.Error("RenderEmoji: %v", err) + return template.HTML("") + } + return template.HTML(renderedText) +} + // RenderEmoji for when we want to just process emoji and shortcodes // in various places it isn't already run through the normal markdown procesor -func RenderEmoji( +func renderEmoji( rawHTML []byte, ) ([]byte, error) { ctx := &postProcessCtx{ diff --git a/modules/templates/helper.go b/modules/templates/helper.go index e4107dfa9a10d..378154e4857f5 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -154,7 +154,7 @@ func NewFuncMap() []template.FuncMap { "RenderCommitMessageLink": RenderCommitMessageLink, "RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject, "RenderCommitBody": RenderCommitBody, - "RenderEmoji": RenderEmoji, + "RenderEmoji": markup.RenderEmoji, "RenderEmojiPlain": emoji.ReplaceAliases, "ReactionToEmoji": ReactionToEmoji, "RenderNote": RenderNote, @@ -369,7 +369,7 @@ func NewFuncMap() []template.FuncMap { for _, label := range labels { html = fmt.Sprintf("%s
%s
", - html, label.ForegroundColor(), label.Color, RenderEmoji(label.Name)) + html, label.ForegroundColor(), label.Color, markup.RenderEmoji(label.Name)) } return template.HTML(html) @@ -632,16 +632,6 @@ func RenderCommitBody(msg, urlPrefix string, metas map[string]string) template.H return template.HTML(renderedMessage) } -// RenderEmoji renders html text with emoji post processors -func RenderEmoji(text string) template.HTML { - renderedText, err := markup.RenderEmoji([]byte(template.HTMLEscapeString(text))) - if err != nil { - log.Error("RenderEmoji: %v", err) - return template.HTML("") - } - return template.HTML(renderedText) -} - //ReactionToEmoji renders emoji for use in reactions func ReactionToEmoji(reaction string) template.HTML { val := emoji.FromCode(reaction) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b1447f310867f..300a4ab1a3c53 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -56,6 +56,7 @@ new_fork = New Repository Fork new_org = New Organization new_project = New Project new_project_board = New Project board +add_project_issue = Add Issue manage_org = Manage Organizations admin_panel = Site Administration account_settings = Account Settings diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 0dbf2741ad4bd..afbbff211640c 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -51,6 +51,11 @@ func SearchIssues(ctx *context.APIContext) { // description: repository to prioritize in the results // type: integer // format: int64 + // - name: exclude_project_id + // in: query + // description: project to exclude from search when searching issues to add to a project + // type: integer + // format: int64 // - name: type // in: query // description: filter by type (issues / pulls) if set @@ -59,6 +64,10 @@ func SearchIssues(ctx *context.APIContext) { // in: query // description: page number of requested issues // type: integer + // - name: render_emoji_title + // in: query + // description: render emojis in issue title + // type: string // responses: // "200": // "$ref": "#/responses/IssueList" @@ -73,6 +82,16 @@ func SearchIssues(ctx *context.APIContext) { isClosed = util.OptionalBoolFalse } + var renderEmojiTitle util.OptionalBool + switch ctx.Query("render_emoji_title") { + case "true": + renderEmojiTitle = util.OptionalBoolTrue + case "false": + renderEmojiTitle = util.OptionalBoolFalse + default: + renderEmojiTitle = util.OptionalBoolFalse + } + // find repos user can access (for issue search) repoIDs := make([]int64, 0) opts := &models.SearchRepoOptions{ @@ -158,6 +177,8 @@ func SearchIssues(ctx *context.APIContext) { SortType: "priorityrepo", PriorityRepoID: ctx.QueryInt64("priority_repo_id"), IsPull: isPull, + ExcludeProjectID: ctx.QueryInt64("exclude_project_id"), + RenderEmojiTitle: renderEmojiTitle, } if issues, err = models.Issues(issuesOpt); err != nil { @@ -228,6 +249,10 @@ func ListIssues(ctx *context.APIContext) { // in: query // description: page size of results // type: integer + // - name: render_emoji_title + // in: query + // description: render emojis in issue title + // type: string // responses: // "200": // "$ref": "#/responses/IssueList" @@ -242,6 +267,16 @@ func ListIssues(ctx *context.APIContext) { isClosed = util.OptionalBoolFalse } + var renderEmojiTitle util.OptionalBool + switch ctx.Query("render_emoji_title") { + case "true": + renderEmojiTitle = util.OptionalBoolTrue + case "false": + renderEmojiTitle = util.OptionalBoolFalse + default: + renderEmojiTitle = util.OptionalBoolFalse + } + var issues []*models.Issue var filteredCount int64 @@ -314,13 +349,15 @@ func ListIssues(ctx *context.APIContext) { // This would otherwise return all issues if no issues were found by the search. if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 { issuesOpt := &models.IssuesOptions{ - ListOptions: listOptions, - RepoIDs: []int64{ctx.Repo.Repository.ID}, - IsClosed: isClosed, - IssueIDs: issueIDs, - LabelIDs: labelIDs, - MilestoneIDs: mileIDs, - IsPull: isPull, + ListOptions: listOptions, + RepoIDs: []int64{ctx.Repo.Repository.ID}, + IsClosed: isClosed, + IssueIDs: issueIDs, + LabelIDs: labelIDs, + MilestoneIDs: mileIDs, + IsPull: isPull, + ExcludeProjectID: ctx.QueryInt64("exclude_project_id"), + RenderEmojiTitle: renderEmojiTitle, } if issues, err = models.Issues(issuesOpt); err != nil { diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 009af784e775a..86a5032b8e477 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -44,6 +44,7 @@ const ( tplIssueChoose base.TplName = "repo/issue/choose" tplIssueView base.TplName = "repo/issue/view" + tplSidebar base.TplName = "repo/issue/view_content/sidebar" tplReactions base.TplName = "repo/issue/view_content/reactions" issueTemplateKey = "IssueTemplate" @@ -1486,7 +1487,14 @@ func ViewIssue(ctx *context.Context) { ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.User.IsAdmin) ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons ctx.Data["RefEndName"] = git.RefEndName(issue.Ref) - ctx.HTML(200, tplIssueView) + + if ctx.Query("sidebar") == "true" { + ctx.Data["Sidebar"] = true + ctx.HTML(200, tplSidebar) + } else { + ctx.Data["Sidebar"] = false + ctx.HTML(200, tplIssueView) + } } // GetActionIssue will return the issue which is used in the context. diff --git a/routers/repo/projects.go b/routers/repo/projects.go index 07327df9eb06d..e172c37060c9d 100644 --- a/routers/repo/projects.go +++ b/routers/repo/projects.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -265,6 +266,7 @@ func ViewProject(ctx *context.Context) { } return } + if project.RepoID != ctx.Repo.Repository.ID { ctx.NotFound("", nil) return @@ -285,8 +287,7 @@ func ViewProject(ctx *context.Context) { allBoards := models.ProjectBoardList{uncategorizedBoard} allBoards = append(allBoards, boards...) - - if ctx.Data["Issues"], err = allBoards.LoadIssues(); err != nil { + if err = allBoards.LoadIssues(); err != nil { ctx.ServerError("LoadIssuesOfBoards", err) return } @@ -298,7 +299,12 @@ func ViewProject(ctx *context.Context) { ctx.Data["Boards"] = allBoards ctx.Data["PageIsProjects"] = true ctx.Data["RequiresDraggable"] = true - + if err := project.LoadRepository(); err != nil { + log.Error("failed loading project's repo %v\n", err) + } + if project.Repo != nil && project.Repo.ID != 0 { + ctx.Data["Repo"] = project.Repo + } ctx.HTML(200, tplProjectsView) } @@ -598,3 +604,40 @@ func CreateProjectPost(ctx *context.Context, form auth.UserCreateProjectForm) { ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title)) ctx.Redirect(setting.AppSubURL + "/") } + +// UpdateBoardsPriorityPost takes a slice of boards and updates their priority order on drag and drop +func UpdateBoardsPriorityPost(ctx *context.Context, form auth.UpdateBoardPriorityForm) { + if ctx.Written() { + return + } + + if ctx.HasError() { + return + } + + boards := form + if err := models.UpdateBoardsPriority(form.Boards); err != nil { + log.Error("failed updating boards %v", err) + ctx.JSON(500, err) + return + } + ctx.JSON(200, boards) +} + +// UpdateBoardIssuePriority takes a slice of ProjectIssue and updates their priority order on drag and drop +func UpdateBoardIssuePriority(ctx *context.Context, form auth.UpdateIssuePriorityBoardForm) { + if ctx.Written() { + return + } + + if ctx.HasError() { + return + } + + issues, err := models.UpdateBoardIssuesPriority(form.Issues) + if err != nil { + log.Error("failed updating issues %v", err) + } + + ctx.JSON(200, issues) +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 285510dbe355f..e3b51b18c6043 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -956,9 +956,11 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/edit", repo.EditProject) m.Post("/edit", bindIgnErr(auth.CreateProjectForm{}), repo.EditProjectPost) + m.Put("/issue/priority", bindIgnErr(auth.UpdateIssuePriorityBoardForm{}), repo.UpdateBoardIssuePriority) m.Post("/^:action(open|close)$", repo.ChangeProjectStatus) m.Group("/:boardID", func() { + m.Put("board/priority", bindIgnErr(auth.UpdateBoardPriorityForm{}), repo.UpdateBoardsPriorityPost) m.Put("", bindIgnErr(auth.EditProjectBoardTitleForm{}), repo.EditProjectBoardTitle) m.Delete("", repo.DeleteProjectBoard) diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index dad4f7e125bfb..742e11ff68efc 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -1,5 +1,8 @@
+ {{if .Sidebar}} +

#{{.Issue.Index}} {{RenderEmoji .Issue.Title}}

+ {{end}} {{template "repo/issue/branch_selector_field" .}} {{if .Issue.IsPull }} @@ -223,7 +226,7 @@ {{end}}
-
+
{{.i18n.Tr "repo.issues.new.no_projects"}}
{{if .Issue.ProjectID}} @@ -554,7 +557,7 @@
+ method="post" id="lock-form"> {{.CsrfTokenHtml}} {{ if not .Issue.IsLocked }} @@ -563,7 +566,7 @@
-