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 project column choice option on issue sidebar #30617

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions models/issues/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ type Issue struct {

// For view issue page.
ShowRole RoleDescriptor `xorm:"-"`

ProjectIssue *project_model.ProjectIssue `xorm:"-"`
}

var (
Expand Down Expand Up @@ -336,6 +338,10 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
return err
}

if err = issue.LoadProjectIssue(ctx); err != nil {
return err
}

if err = issue.LoadAssignees(ctx); err != nil {
return err
}
Expand Down
19 changes: 19 additions & 0 deletions models/issues/issue_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ func (issue *Issue) LoadProject(ctx context.Context) (err error) {
return err
}

func (issue *Issue) LoadProjectIssue(ctx context.Context) (err error) {
lunny marked this conversation as resolved.
Show resolved Hide resolved
if issue.Project == nil {
return nil
}

if issue.ProjectIssue != nil {
return nil
}

issue.ProjectIssue, err = project_model.GetProjectIssueByIssueID(ctx, issue.ID)
if err != nil {
return err
}

issue.ProjectIssue.Project = issue.Project

return issue.ProjectIssue.LoadProjectColumn(ctx)
}

func (issue *Issue) projectID(ctx context.Context) int64 {
var ip project_model.ProjectIssue
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
Expand Down
6 changes: 3 additions & 3 deletions models/project/column.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,17 @@ func (Column) TableName() string {
}

// NumIssues return counter of all issues assigned to the column
func (c *Column) NumIssues(ctx context.Context) int {
func (c *Column) NumIssues(ctx context.Context) (int64, error) {
a1012112796 marked this conversation as resolved.
Show resolved Hide resolved
total, err := db.GetEngine(ctx).Table("project_issue").
Where("project_id=?", c.ProjectID).
And("project_board_id=?", c.ID).
GroupBy("issue_id").
Cols("issue_id").
Count()
if err != nil {
return 0
return 0, err
}
return int(total)
return total, nil
}

func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
Expand Down
75 changes: 71 additions & 4 deletions models/project/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import (

// ProjectIssue saves relation from issue to a project
type ProjectIssue struct { //revive:disable-line:exported
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"`
Project *Project `xorm:"-"`

// ProjectColumnID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors.
ProjectColumnID int64 `xorm:"'project_board_id' INDEX"`
ProjectColumnID int64 `xorm:"'project_board_id' INDEX"`
ProjectColumn *Column `xorm:"-"`

// the sorting order on the column
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
Expand All @@ -34,6 +36,50 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error
return err
}

type ErrProjectIssueNotExist struct {
IssueID int64
}

func (e ErrProjectIssueNotExist) Error() string {
return fmt.Sprintf("can't find project issue [issue_id: %d]", e.IssueID)
}

func IsErrProjectIssueNotExist(e error) bool {
_, ok := e.(ErrProjectIssueNotExist)
return ok
}

func GetProjectIssueByIssueID(ctx context.Context, issueID int64) (*ProjectIssue, error) {
issue := &ProjectIssue{}

has, err := db.GetEngine(ctx).Where("issue_id = ?", issueID).Get(issue)
if err != nil {
return nil, err
}

if !has {
return nil, ErrProjectIssueNotExist{IssueID: issueID}
}

return issue, nil
}

func (issue *ProjectIssue) LoadProjectColumn(ctx context.Context) error {
if issue.ProjectColumn != nil {
return nil
}

var err error

if issue.ProjectColumnID == 0 {
issue.ProjectColumn, err = issue.Project.GetDefaultColumn(ctx)
return err
}

issue.ProjectColumn, err = GetColumn(ctx, issue.ProjectColumnID)
return err
}

// NumIssues return counter of all issues assigned to a project
func (p *Project) NumIssues(ctx context.Context) int {
c, err := db.GetEngine(ctx).Table("project_issue").
Expand Down Expand Up @@ -100,6 +146,27 @@ func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueI
})
}

func MoveIssueToColumnTail(ctx context.Context, issue *ProjectIssue, toColumn *Column) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()

num, err := toColumn.NumIssues(ctx)
Copy link
Member

Choose a reason for hiding this comment

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

This is not right. If you update two columns, then they will have the same sorting.

Copy link
Member Author

Choose a reason for hiding this comment

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

but this function only can update one column.

Copy link
Member

Choose a reason for hiding this comment

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

I mean one by one.

Copy link
Member Author

Choose a reason for hiding this comment

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

I mean one by one.

I think it okay, becaue when call it one by one, NumIssues also will be canged one by one. else which sorting should be seted in this usage? zero?

Copy link
Member Author

Choose a reason for hiding this comment

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

looks in moveIssuesToAnotherColumn(), it use max() sql function to get new sorting, I have use same logic in a824db0, how about this?

if err != nil {
return err
}

_, err = db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?",
toColumn.ID, num, issue.IssueID)
if err != nil {
return err
}

return committer.Commit()
}

func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
if c.ProjectID != newColumn.ProjectID {
return fmt.Errorf("columns have to be in the same project")
Expand Down
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1752,6 +1752,7 @@ issues.content_history.delete_from_history = Delete from history
issues.content_history.delete_from_history_confirm = Delete from history?
issues.content_history.options = Options
issues.reference_link = Reference: %s
issues.move_project_boad = Status

compare.compare_base = base
compare.compare_head = compare
Expand Down
11 changes: 11 additions & 0 deletions routers/web/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -2046,6 +2046,17 @@ func ViewIssue(ctx *context.Context) {
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
}

canWriteProjects := ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["CanWriteProjects"] = canWriteProjects

if canWriteProjects && issue.Project != nil {
lunny marked this conversation as resolved.
Show resolved Hide resolved
ctx.Data["ProjectColumns"], err = issue.Project.GetColumns(ctx)
if err != nil {
ctx.ServerError("Project.GetBoards", err)
return
}
}

ctx.HTML(http.StatusOK, tplIssueView)
}

Expand Down
66 changes: 66 additions & 0 deletions routers/web/repo/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,72 @@ func SetDefaultProjectColumn(ctx *context.Context) {
ctx.JSONOK()
}

// MoveColumnForIssue move a issue to other board
func MoveColumnForIssue(ctx *context.Context) {
if ctx.Doer == nil {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.",
})
return
}

if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only authorized users are allowed to perform this action.",
})
return
}

issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound("GetIssueByIndex", err)
} else {
ctx.ServerError("GetIssueByIndex", err)
}
return
}

if err := issue.LoadProject(ctx); err != nil {
ctx.ServerError("LoadProject", err)
return
}
if issue.Project == nil {
ctx.NotFound("Project not found", nil)
return
}

if err = issue.LoadProjectIssue(ctx); err != nil {
ctx.ServerError("LoadProjectIssue", err)
return
}

column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
if err != nil {
if project_model.IsErrProjectColumnNotExist(err) {
ctx.NotFound("ErrProjectColumnNotExist", nil)
} else {
ctx.ServerError("GetColumn", err)
}
return
}

if column.ProjectID != issue.Project.ID {
ctx.NotFound("ColumnNotInProject", nil)
return
}

err = project_model.MoveIssueToColumnTail(ctx, issue.ProjectIssue, column)
if err != nil {
ctx.NotFound("MoveIssueToBoardTail", nil)
return
}

issue.Repo = ctx.Repo.Repository

ctx.JSONRedirect(issue.HTMLURL())
}

// MoveIssues moves or keeps issues in a column and sorts them inside that column
func MoveIssues(ctx *context.Context) {
if ctx.Doer == nil {
Expand Down
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,7 @@ func registerRoutes(m *web.Route) {
m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue)
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
m.Post("/delete", reqRepoAdmin, repo.DeleteIssue)
m.Post("/move_project_column/{columnID}", repo.MoveColumnForIssue)
}, context.RepoMustNotBeArchived())

m.Group("/{index}", func() {
Expand Down
16 changes: 14 additions & 2 deletions templates/repo/issue/view_content/sidebar.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,25 @@
{{end}}
</div>
</div>
<div class="ui select-project list">
<div class="ui select-project-current list">
<span class="no-select item {{if .Issue.Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
<div class="selected">
{{if .Issue.Project}}
<a class="item muted sidebar-item-link" href="{{.Issue.Project.Link ctx}}">
<a class="item muted sidebar-item-link tw-block" href="{{.Issue.Project.Link ctx}}">
{{svg .Issue.Project.IconName 18 "tw-mr-2"}}{{.Issue.Project.Title}}
</a>
<div class="ui dropdown jump {{if not .CanWriteProjects}}disabled{{end}} select-issue-project-board item tw-mx-0 tw-pr-2" data-url="{{$.Issue.Link}}/move_project_column/">
<span class="text">
{{ctx.Locale.Tr "repo.issues.move_project_boad"}}: {{.Issue.ProjectIssue.ProjectColumn.Title}}
</span>
<div class="menu">
{{if .ProjectColumns}}
{{range .ProjectColumns}}
<div class="item no-select" data-project-id="{{.ProjectID}}" data-board-id="{{.ID}}">{{.Title}}</div>
{{end}}
{{end}}
</div>
</div>
{{end}}
</div>
</div>
Expand Down
26 changes: 26 additions & 0 deletions web_src/js/features/repo-issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -735,3 +735,29 @@ export function initArchivedLabelHandler() {
toggleElem(label, label.classList.contains('checked'));
}
}

export function initIssueProjectColumnSelector() {
const root = document.querySelector('.select-issue-project-board');
if (!root) return;

const link = root.getAttribute('data-url');

for (const board of document.querySelectorAll('.select-issue-project-board .item')) {
board.addEventListener('click', async (e) => {
e.preventDefault();
e.stopImmediatePropagation();

try {
const response = await POST(`${link}${board.getAttribute('data-board-id')}`);
if (response.ok) {
const data = await response.json();
window.location.href = data.redirect;
}
} catch (error) {
console.error(error);
}

return false;
});
}
}
2 changes: 2 additions & 0 deletions web_src/js/features/repo-legacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
initRepoIssueTitleEdit, initRepoIssueWipToggle,
initRepoPullRequestUpdate, updateIssuesMeta, initIssueTemplateCommentEditors, initSingleCommentEditor,
initIssueProjectColumnSelector,
} from './repo-issue.js';
import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
import {svg} from '../svg.js';
Expand Down Expand Up @@ -393,6 +394,7 @@ export function initRepository() {
initRepoIssueCodeCommentCancel();
initRepoPullRequestUpdate();
initCompReactionSelector();
initIssueProjectColumnSelector();

initRepoPullRequestMergeForm();
initRepoPullRequestCommitStatus();
Expand Down