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 @@ -140,6 +140,8 @@ type Issue struct {

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

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

var (
Expand Down Expand Up @@ -315,6 +317,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
17 changes: 17 additions & 0 deletions models/issues/issue_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ 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
}

return issue.ProjectIssue.LoadProjectBoard(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
19 changes: 16 additions & 3 deletions models/project/board.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,17 @@ func (Board) TableName() string {
}

// NumIssues return counter of all issues assigned to the board
func (b *Board) NumIssues(ctx context.Context) int {
func (b *Board) NumIssues(ctx context.Context) (int64, error) {
c, err := db.GetEngine(ctx).Table("project_issue").
Where("project_id=?", b.ProjectID).
And("project_board_id=?", b.ID).
GroupBy("issue_id").
Cols("issue_id").
Count()
if err != nil {
return 0
return 0, err
}
return int(c)
return c, nil
}

func init() {
Expand Down Expand Up @@ -219,6 +219,19 @@ func GetBoard(ctx context.Context, boardID int64) (*Board, error) {
return board, nil
}

// GetBoard fetches the current default board of a project
func GetDefaultBoard(ctx context.Context, projectID int64) (*Board, error) {
a1012112796 marked this conversation as resolved.
Show resolved Hide resolved
board := new(Board)
has, err := db.GetEngine(ctx).Where("project_id = ? AND `default` = ?", projectID, true).Get(board)
if err != nil {
return nil, err
} else if !has {
return nil, ErrProjectBoardNotExist{BoardID: -1}
}

return board, nil
}

// UpdateBoard updates a project board
func UpdateBoard(ctx context.Context, board *Board) error {
var fieldToUpdate []string
Expand Down
68 changes: 67 additions & 1 deletion models/project/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ type ProjectIssue struct { //revive:disable-line:exported
ProjectID int64 `xorm:"INDEX"`

// If 0, then it has not been added to a specific board in the project
ProjectBoardID int64 `xorm:"INDEX"`
ProjectBoardID int64 `xorm:"INDEX"`
ProjectBoard *Board `xorm:"-"`

// the sorting order on the board
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
Expand All @@ -33,6 +34,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) LoadProjectBoard(ctx context.Context) error {
if issue.ProjectBoard != nil {
return nil
}

var err error

if issue.ProjectBoardID == 0 {
a1012112796 marked this conversation as resolved.
Show resolved Hide resolved
issue.ProjectBoard, err = GetDefaultBoard(ctx, issue.ProjectID)
return err
}

issue.ProjectBoard, err = GetBoard(ctx, issue.ProjectBoardID)
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 @@ -102,6 +147,27 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
})
}

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

num, err := toBoard.NumIssues(ctx)
if err != nil {
return err
}

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

return committer.Commit()
}

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
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 @@ -1748,6 +1748,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 @@ -2044,6 +2044,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["ProjectBoards"], err = issue.Project.GetBoards(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 @@ -574,6 +574,72 @@ func SetDefaultProjectBoard(ctx *context.Context) {
ctx.JSONOK()
}

// MoveBoardForIssue move a issue to other board
func MoveBoardForIssue(ctx *context.Context) {
a1012112796 marked this conversation as resolved.
Show resolved Hide resolved
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
}

board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
if err != nil {
if project_model.IsErrProjectBoardNotExist(err) {
ctx.NotFound("ProjectBoardNotExist", nil)
} else {
ctx.ServerError("GetProjectBoard", err)
}
return
}

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

err = project_model.MoveIssueToBoardTail(ctx, issue.ProjectIssue, board)
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 @@ -1206,6 +1206,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_board/{boardID}", repo.MoveBoardForIssue)
}, 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_board/">
<span class="text">
{{ctx.Locale.Tr "repo.issues.move_project_boad"}}: {{.Issue.ProjectIssue.ProjectBoard.Title}}
</span>
<div class="menu">
{{if .ProjectBoards}}
{{range .ProjectBoards}}
<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 @@ -746,3 +746,29 @@ export function initArchivedLabelHandler() {
toggleElem(label, label.classList.contains('checked'));
}
}

export function initIssueProjectBoardSelector() {
a1012112796 marked this conversation as resolved.
Show resolved Hide resolved
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,
initIssueProjectBoardSelector,
} from './repo-issue.js';
import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
import {svg} from '../svg.js';
Expand Down Expand Up @@ -394,6 +395,7 @@ export function initRepository() {
initRepoIssueCodeCommentCancel();
initRepoPullRequestUpdate();
initCompReactionSelector();
initIssueProjectBoardSelector();

initRepoPullRequestMergeForm();
initRepoPullRequestCommitStatus();
Expand Down