Add project column picker to issue and pull request sidebar (#37037)
Why? You are working on a ticket, it's ready to be moved to the QA column in your project. Currently you have to go to the project, find the issue card, then move it. With this change you can move the issue's column on the issue page. When an issue or pull request belongs to a project board, a dropdown appears in the sidebar to move it between columns without opening the board view. Read-only users see the current column name instead. * Fix #13520 * Replace #30617 This was written using Claude Code and Opus. Closed: <img width="1346" height="507" alt="image" src="https://github.com/user-attachments/assets/7c1ea7ee-b71c-40af-bb14-aeb1d2beff73" /> Open: <img width="1315" height="577" alt="image" src="https://github.com/user-attachments/assets/4d64b065-44c2-42c7-8d20-84b5caea589a" /> --------- Signed-off-by: silverwind <me@silverwind.io> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Nicolas <bircni@icloud.com> Co-authored-by: Cursor <cursor@cursor.com>
This commit is contained in:
@@ -592,6 +592,17 @@ func GetIssueByID(ctx context.Context, id int64) (*Issue, error) {
|
||||
return issue, nil
|
||||
}
|
||||
|
||||
func GetIssueByRepoID(ctx context.Context, repoID, issueID int64) (*Issue, error) {
|
||||
issue := new(Issue)
|
||||
has, err := db.GetEngine(ctx).ID(issueID).Where("repo_id=?", repoID).Get(issue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrIssueNotExist{issueID, repoID, 0}
|
||||
}
|
||||
return issue, nil
|
||||
}
|
||||
|
||||
// GetIssuesByIDs return issues with the given IDs.
|
||||
// If keepOrder is true, the order of the returned issues will be the same as the given IDs.
|
||||
func GetIssuesByIDs(ctx context.Context, issueIDs []int64, keepOrder ...bool) (IssueList, error) {
|
||||
|
||||
@@ -115,17 +115,10 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
|
||||
panic("newColumnID must not be zero") // shouldn't happen
|
||||
}
|
||||
|
||||
res := struct {
|
||||
MaxSorting int64
|
||||
IssueCount int64
|
||||
}{}
|
||||
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue").
|
||||
Where("project_id=?", newProjectID).
|
||||
And("project_board_id=?", newColumnID).
|
||||
Get(&res); err != nil {
|
||||
newSorting, err := project_model.GetColumnIssueNextSorting(ctx, newProjectID, newColumnID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
|
||||
return db.Insert(ctx, &project_model.ProjectIssue{
|
||||
IssueID: issue.ID,
|
||||
ProjectID: newProjectID,
|
||||
|
||||
@@ -185,7 +185,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = column.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
|
||||
if err = moveIssuesToAnotherColumn(ctx, column, defaultColumn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ func Test_moveIssuesToAnotherColumn(t *testing.T) {
|
||||
assert.Len(t, issues, 1)
|
||||
assert.EqualValues(t, 3, issues[0].ID)
|
||||
|
||||
err = column1.moveIssuesToAnotherColumn(t.Context(), column2)
|
||||
err = moveIssuesToAnotherColumn(t.Context(), column1, column2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issues, err = column1.GetIssues(t.Context())
|
||||
|
||||
@@ -33,38 +33,45 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
|
||||
if c.ProjectID != newColumn.ProjectID {
|
||||
return errors.New("columns have to be in the same project")
|
||||
}
|
||||
|
||||
if c.ID == newColumn.ID {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetColumnIssueNextSorting returns the sorting value to append an issue at the end of the column.
|
||||
func GetColumnIssueNextSorting(ctx context.Context, projectID, columnID int64) (int64, error) {
|
||||
res := struct {
|
||||
MaxSorting int64
|
||||
IssueCount int64
|
||||
}{}
|
||||
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").
|
||||
if _, err := db.GetEngine(ctx).Select("max(sorting) AS max_sorting, count(*) AS issue_count").
|
||||
Table("project_issue").
|
||||
Where("project_id=?", newColumn.ProjectID).
|
||||
And("project_board_id=?", newColumn.ID).
|
||||
Where("project_id=?", projectID).
|
||||
And("project_board_id=?", columnID).
|
||||
Get(&res); err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
return util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0), nil
|
||||
}
|
||||
|
||||
func moveIssuesToAnotherColumn(ctx context.Context, oldColumn, newColumn *Column) error {
|
||||
if oldColumn.ProjectID != newColumn.ProjectID {
|
||||
return errors.New("columns have to be in the same project")
|
||||
}
|
||||
|
||||
issues, err := c.GetIssues(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
if oldColumn.ID == newColumn.ID {
|
||||
return nil
|
||||
}
|
||||
|
||||
nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
|
||||
movedIssues, err := oldColumn.GetIssues(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(movedIssues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
nextSorting, err := GetColumnIssueNextSorting(ctx, newColumn.ProjectID, newColumn.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
for i, issue := range issues {
|
||||
for i, issue := range movedIssues {
|
||||
issue.ProjectColumnID = newColumn.ID
|
||||
issue.Sorting = nextSorting + int64(i)
|
||||
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user