mirror of
https://github.com/go-gitea/gitea
synced 2024-09-20 18:56:05 +00:00
add project board choice option on issue sidebar
Signed-off-by: a1012112796 <1012112796@qq.com>
This commit is contained in:
parent
b06aac40e6
commit
6524bd51ca
@ -140,6 +140,8 @@ type Issue struct {
|
||||
|
||||
// For view issue page.
|
||||
ShowRole RoleDescriptor `xorm:"-"`
|
||||
|
||||
ProjectIssue *project_model.ProjectIssue `xorm:"-"`
|
||||
}
|
||||
|
||||
var (
|
||||
@ -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
|
||||
}
|
||||
|
@ -28,6 +28,23 @@ func (issue *Issue) LoadProject(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (issue *Issue) LoadProjectIssue(ctx context.Context) (err error) {
|
||||
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)
|
||||
|
@ -69,7 +69,7 @@ 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).
|
||||
@ -77,9 +77,9 @@ func (b *Board) NumIssues(ctx context.Context) int {
|
||||
Cols("issue_id").
|
||||
Count()
|
||||
if err != nil {
|
||||
return 0
|
||||
return 0, err
|
||||
}
|
||||
return int(c)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -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) {
|
||||
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
|
||||
|
@ -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"`
|
||||
@ -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 {
|
||||
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").
|
||||
@ -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
|
||||
|
@ -1749,6 +1749,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
|
||||
|
@ -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 {
|
||||
ctx.Data["ProjectBoards"], err = issue.Project.GetBoards(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("Project.GetBoards", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplIssueView)
|
||||
}
|
||||
|
||||
|
@ -574,6 +574,72 @@ func SetDefaultProjectBoard(ctx *context.Context) {
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// MoveBoardForIssue move a issue to other board
|
||||
func MoveBoardForIssue(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
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -1204,6 +1204,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() {
|
||||
|
@ -193,13 +193,25 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui select-project list">
|
||||
<div class="ui select-project-curent 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>
|
||||
|
@ -746,3 +746,29 @@ export function initArchivedLabelHandler() {
|
||||
toggleElem(label, label.classList.contains('checked'));
|
||||
}
|
||||
}
|
||||
|
||||
export function initIssueProjectBoardSelector() {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
@ -182,7 +183,7 @@ export function initRepoCommentForm() {
|
||||
|
||||
// TODO: Which thing should be done for choosing review requests
|
||||
// to make chosen items be shown on time here?
|
||||
if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
|
||||
if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify' || selector === 'select-issue-project-board') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -222,7 +223,7 @@ export function initRepoCommentForm() {
|
||||
$(this).find('.octicon-check').addClass('tw-invisible');
|
||||
});
|
||||
|
||||
if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
|
||||
if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify' || selector === 'select-issue-project-board') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -394,6 +395,7 @@ export function initRepository() {
|
||||
initRepoIssueCodeCommentCancel();
|
||||
initRepoPullRequestUpdate();
|
||||
initCompReactionSelector();
|
||||
initIssueProjectBoardSelector();
|
||||
|
||||
initRepoPullRequestMergeForm();
|
||||
initRepoPullRequestCommitStatus();
|
||||
|
Loading…
Reference in New Issue
Block a user