Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to pin Issues #24406

Merged
merged 54 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e03f4cf
Add the abillity to pin Issues
JakobDev Apr 28, 2023
961a95f
Merge branch 'main' into issuepin
JakobDev Apr 28, 2023
4d1d1d8
Fix eslint errors
JakobDev Apr 28, 2023
7aea9d6
Rename Pin to PinOrder
JakobDev Apr 28, 2023
22c0687
Add X Button
JakobDev Apr 28, 2023
31804ab
Remove newline
JakobDev Apr 28, 2023
65dd702
Fix typo
JakobDev Apr 28, 2023
dfacb3f
Update templates/repo/issue/list.tmpl
silverwind Apr 29, 2023
8492384
Merge branch 'main' into issuepin
JakobDev May 2, 2023
8b84877
Changes requested by silverwind
JakobDev May 2, 2023
8c94178
Add limit for pinned Issues
JakobDev May 2, 2023
023820f
Add pin and unpin events to history
JakobDev May 2, 2023
a31a96a
Fix backend lint
JakobDev May 2, 2023
c0ffcdb
Add Migration
JakobDev May 2, 2023
f74d573
Add tests
JakobDev May 2, 2023
3ee07af
Merge branch 'main' into issuepin
JakobDev May 2, 2023
034c8a6
Rename v256.go to v257.go
JakobDev May 3, 2023
2b4eccb
Merge branch 'main' into issuepin
JakobDev May 3, 2023
0d1ccaa
Do requested changes
JakobDev May 3, 2023
abbe6e1
Merge branch 'main' into issuepin
JakobDev May 8, 2023
1704b6f
Changes requested by wxiaoguang
JakobDev May 8, 2023
4735026
Unpin Issue before deletion
JakobDev May 8, 2023
8f25b3a
Change Layout
JakobDev May 9, 2023
7880472
Merge branch 'main' into issuepin
JakobDev May 9, 2023
9afc05e
issue sidebar button fixes
silverwind May 9, 2023
688ace1
various display and html layout tweaks
silverwind May 9, 2023
61a2dc1
misc fixes
silverwind May 9, 2023
59a5691
Update templates/shared/issueicon.tmpl
silverwind May 10, 2023
8010a7a
Merge branch 'main' into issuepin
JakobDev May 12, 2023
706c6cc
Do requested changes
JakobDev May 12, 2023
3dc4746
Merge branch 'main' into issuepin
JakobDev May 12, 2023
9d38fad
Use new Unpin Icon
JakobDev May 12, 2023
03761ea
Fix Style of new Icon
JakobDev May 12, 2023
ea9df52
Merge branch 'main' into issuepin
JakobDev May 15, 2023
20356cf
Do requested changes
JakobDev May 15, 2023
cac50e0
Rename files for resolving conflicts
JakobDev May 22, 2023
630a9f1
Merge branch 'main' into issuepin
JakobDev May 22, 2023
d5edde7
Fix misstage from resolving conflicts
JakobDev May 22, 2023
cd194a1
Do requested changes
JakobDev May 22, 2023
cadaa48
Use err
JakobDev May 22, 2023
08c59de
Change default config to 3 and add Documentation
JakobDev May 22, 2023
ab35b5e
Fix merge problem
JakobDev May 22, 2023
3f32853
Update templates/repo/issue/view_content/comments.tmpl
silverwind May 22, 2023
1d098d5
Update templates/repo/issue/view_content/comments.tmpl
JakobDev May 22, 2023
d3e365a
Allow unpining even if Pins are disabled
JakobDev May 22, 2023
bce5ff1
Merge branch 'main' into issuepin
silverwind May 22, 2023
4aa9488
Merge branch 'main' into issuepin
silverwind May 22, 2023
8643224
Merge branch 'main' into issuepin
JakobDev May 23, 2023
76ea8d9
Merge branch 'main' into issuepin
silverwind May 23, 2023
caeae1b
Delete issueicon.tmpl
JakobDev May 25, 2023
e3c67cf
Merge branch 'main' into issuepin
JakobDev May 25, 2023
a4114f0
Run make generate-swagger
JakobDev May 25, 2023
e2b81e0
Merge branch 'main' into issuepin
GiteaBot May 25, 2023
36a5478
Remove outdated comment
JakobDev May 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,9 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; List of reasons why a Pull Request or Issue can be locked
;LOCK_REASONS = Too heated,Off-topic,Resolved,Spam
;; Maximum number of pinned Issues
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

per repo!

;; Set to 0 to disable pinning Issues
;MAX_PINNED = 3

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ In addition there is _`StaticRootPath`_ which can be set as a built-in at build
### Repository - Issue (`repository.issue`)

- `LOCK_REASONS`: **Too heated,Off-topic,Resolved,Spam**: A list of reasons why a Pull Request or Issue can be locked
- `MAX_PINNED`: **3**: Maximum number of pinned Issues. Set to 0 to disable pinning Issues.

### Repository - Upload (`repository.upload`)

Expand Down
4 changes: 4 additions & 0 deletions models/issues/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ const (
CommentTypePRScheduledToAutoMerge // 34 pr was scheduled to auto merge when checks succeed
CommentTypePRUnScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed

CommentTypePin // 36 pin Issue
CommentTypeUnpin // 37 unpin Issue
)

var commentStrings = []string{
Expand Down Expand Up @@ -146,6 +148,8 @@ var commentStrings = []string{
"change_issue_ref",
"pull_scheduled_merge",
"pull_cancel_scheduled_merge",
"pin",
"unpin",
}

func (t CommentType) String() string {
Expand Down
181 changes: 181 additions & 0 deletions models/issues/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
Expand Down Expand Up @@ -116,6 +117,7 @@ type Issue struct {
PullRequest *PullRequest `xorm:"-"`
NumComments int
Ref string
PinOrder int `xorm:"DEFAULT 0"`

DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`

Expand Down Expand Up @@ -684,3 +686,182 @@ func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID }
func (issue *Issue) HasOriginalAuthor() bool {
return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
}

// IsPinned returns if a Issue is pinned
func (issue *Issue) IsPinned() bool {
return issue.PinOrder != 0
}

// Pin pins a Issue
func (issue *Issue) Pin(ctx context.Context, user *user_model.User) error {
// If the Issue is already pinned, we don't need to pin it twice
if issue.IsPinned() {
return nil
}

var maxPin int
_, err := db.GetEngine(ctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", issue.RepoID, issue.IsPull).Get(&maxPin)
JakobDev marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}

// Check if the maximum allowed Pins reached
if maxPin >= setting.Repository.Issue.MaxPinned {
return fmt.Errorf("You have reached the max number of pinned Issues")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

util.NewInvalidArgumentErrorf?

}

_, err = db.GetEngine(ctx).Table("issue").
Where("id = ?", issue.ID).
Update(map[string]interface{}{
"pin_order": maxPin + 1,
})
if err != nil {
return err
}

// Add the pin event to the history
opts := &CreateCommentOptions{
Type: CommentTypePin,
Doer: user,
Repo: issue.Repo,
Issue: issue,
}
if _, err = CreateComment(ctx, opts); err != nil {
return err
}

return nil
}

// UnpinIssue unpins a Issue
func (issue *Issue) Unpin(ctx context.Context, user *user_model.User) error {
// If the Issue is not pinned, we don't need to unpin it
if !issue.IsPinned() {
return nil
}

// This sets the Pin for all Issues that come after the unpined Issue to the correct value
_, err := db.GetEngine(ctx).Exec("UPDATE issue SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ?", issue.RepoID, issue.IsPull, issue.PinOrder)
if err != nil {
return err
}

_, err = db.GetEngine(ctx).Table("issue").
Where("id = ?", issue.ID).
Update(map[string]interface{}{
"pin_order": 0,
})
if err != nil {
return err
}

// Add the unpin event to the history
opts := &CreateCommentOptions{
Type: CommentTypeUnpin,
Doer: user,
Repo: issue.Repo,
Issue: issue,
}
if _, err = CreateComment(ctx, opts); err != nil {
return err
}

return nil
}

// PinOrUnpin pins or unpins a Issue
func (issue *Issue) PinOrUnpin(ctx context.Context, user *user_model.User) error {
if !issue.IsPinned() {
return issue.Pin(ctx, user)
}

return issue.Unpin(ctx, user)
}

// MovePin moves a Pinned Issue to a new Position
func (issue *Issue) MovePin(ctx context.Context, newPosition int) error {
// If the Issue is not pinned, we can't move them
if !issue.IsPinned() {
return nil
}

if newPosition < 1 {
return fmt.Errorf("The Position can't be lower than 1")
}

dbctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()

var maxPin int
_, err = db.GetEngine(dbctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", issue.RepoID, issue.IsPull).Get(&maxPin)
if err != nil {
return err
}

// If the new Position bigger than the current Maximum, set it to the Maximum
if newPosition > maxPin+1 {
newPosition = maxPin + 1
}

// TODO: Run the following commands in a Transaction and Rollback, if one fails

// Lower the Position of all Pinned Issue that came after the current Position
_, err = db.GetEngine(dbctx).Exec("UPDATE issue SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ?", issue.RepoID, issue.IsPull, issue.PinOrder)
if err != nil {
return err
}

// Higher the Position of all Pinned Issues that comes after the new Position
_, err = db.GetEngine(dbctx).Exec("UPDATE issue SET pin_order = pin_order + 1 WHERE repo_id = ? AND is_pull = ? AND pin_order >= ?", issue.RepoID, issue.IsPull, newPosition)
if err != nil {
return err
}

_, err = db.GetEngine(dbctx).Table("issue").
Where("id = ?", issue.ID).
Update(map[string]interface{}{
"pin_order": newPosition,
})
if err != nil {
return err
}

return committer.Commit()
}

// GetPinnedIssues returns the pinned Issues for the given Repo and type
func GetPinnedIssues(ctx context.Context, repoID int64, isPull bool) ([]*Issue, error) {
issues := make([]*Issue, 0)

err := db.GetEngine(ctx).
Table("issue").
Where("repo_id = ?", repoID).
And("is_pull = ?", isPull).
And("pin_order > 0").
OrderBy("pin_order").
Find(&issues)
if err != nil {
return nil, err
}

err = IssueList(issues).LoadAttributes()
if err != nil {
return nil, err
}

return issues, nil
}

// IsNewPinnedAllowed returns if a new Issue or Pull request can be pinned
func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, error) {
var maxPin int
_, err := db.GetEngine(ctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&maxPin)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use COUNT instead of MAX.
That will fail less often (i.e. if there are spaces between pin orders).

if err != nil {
return false, err
}

return maxPin < setting.Repository.Issue.MaxPinned, nil
}
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,8 @@ var migrations = []Migration{
NewMigration("Add is_internal column to package", v1_20.AddIsInternalColumnToPackage),
// v257 -> v258
NewMigration("Add Actions Artifact table", v1_20.CreateActionArtifactTable),
// v258 -> 259
NewMigration("Add PinOrder Column", v1_20.AddPinOrderToIssue),
JakobDev marked this conversation as resolved.
Show resolved Hide resolved
}

// GetCurrentDBVersion returns the current db version
Expand Down
16 changes: 16 additions & 0 deletions models/migrations/v1_20/v258.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_20 //nolint

import (
"xorm.io/xorm"
)

func AddPinOrderToIssue(x *xorm.Engine) error {
type Issue struct {
PinOrder int `xorm:"DEFAULT 0"`
}

return x.Sync(new(Issue))
}
3 changes: 3 additions & 0 deletions modules/setting/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ var (
// Issue Setting
Issue struct {
LockReasons []string
MaxPinned int
} `ini:"repository.issue"`

Release struct {
Expand Down Expand Up @@ -227,8 +228,10 @@ var (
// Issue settings
Issue: struct {
LockReasons []string
MaxPinned int
}{
LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","),
MaxPinned: 3,
},

Release: struct {
Expand Down
2 changes: 2 additions & 0 deletions modules/structs/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ type Issue struct {

PullRequest *PullRequestMeta `json:"pull_request"`
Repo *RepositoryMeta `json:"repository"`

PinOrder int `json:"pin_order"`
}

// CreateIssueOption options to create one issue
Expand Down
2 changes: 2 additions & 0 deletions modules/structs/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type PullRequest struct {
Updated *time.Time `json:"updated_at"`
// swagger:strfmt date-time
Closed *time.Time `json:"closed_at"`

PinOrder int `json:"pin_order"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, pinned PRs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you can pin Pull Requests

}

// PRBranchInfo information about a branch
Expand Down
6 changes: 6 additions & 0 deletions modules/structs/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,9 @@ type RepoTransfer struct {
Recipient *User `json:"recipient"`
Teams []*Team `json:"teams"`
}

// NewIssuePinsAllowed represents an API response that says if new Issue Pins are allowed
type NewIssuePinsAllowed struct {
Issues bool `json:"issues"`
PullRequests bool `json:"pull_requests"`
}
7 changes: 7 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ unknown = Unknown

rss_feed = RSS Feed

pin = Pin
unpin = Unpin

artifacts = Artifacts

concept_system_global = Global
Expand Down Expand Up @@ -1482,6 +1485,10 @@ issues.attachment.open_tab = `Click to see "%s" in a new tab`
issues.attachment.download = `Click to download "%s"`
issues.subscribe = Subscribe
issues.unsubscribe = Unsubscribe
issues.unpin_issue = Unpin Issue
issues.max_pinned = "You can't pin more issues"
issues.pin_comment = "pinned this %s"
issues.unpin_comment = "unpinned this %s"
issues.lock = Lock conversation
issues.unlock = Unlock conversation
issues.lock.unknown_reason = Cannot lock an issue with an unknown reason.
Expand Down
9 changes: 9 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,7 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Group("/issues", func() {
m.Combo("").Get(repo.ListIssues).
Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
m.Get("/pinned", repo.ListPinnedIssues)
m.Group("/comments", func() {
m.Get("", repo.ListRepoIssueComments)
m.Group("/{id}", func() {
Expand Down Expand Up @@ -1045,6 +1046,12 @@ func Routes(ctx gocontext.Context) *web.Route {
Get(repo.GetIssueBlocks).
Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.CreateIssueBlocking).
Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.RemoveIssueBlocking)
m.Group("/pin", func() {
m.Combo("").
Post(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), repo.PinIssue).
Delete(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), repo.UnpinIssue)
m.Patch("/{position}", reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), repo.MoveIssuePin)
})
})
}, mustEnableIssuesOrPulls)
m.Group("/labels", func() {
Expand Down Expand Up @@ -1107,6 +1114,7 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Group("/pulls", func() {
m.Combo("").Get(repo.ListPullRequests).
Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
m.Get("/pinned", repo.ListPinnedPullRequests)
m.Group("/{index}", func() {
m.Combo("").Get(repo.GetPullRequest).
Patch(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
Expand Down Expand Up @@ -1184,6 +1192,7 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig)
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed)
}, repoAssignment())
})

Expand Down
Loading