mirror of
https://github.com/go-gitea/gitea.git
synced 2024-07-01 02:05:30 +00:00
Merge a824db0d8b
into 0f09c22663
This commit is contained in:
commit
1db7104e01
|
@ -147,6 +147,8 @@ type Issue struct {
|
||||||
|
|
||||||
// For view issue page.
|
// For view issue page.
|
||||||
ShowRole RoleDescriptor `xorm:"-"`
|
ShowRole RoleDescriptor `xorm:"-"`
|
||||||
|
|
||||||
|
ProjectIssue *project_model.ProjectIssue `xorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -336,6 +338,10 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = issue.LoadProjectIssue(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err = issue.LoadAssignees(ctx); err != nil {
|
if err = issue.LoadAssignees(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,25 @@ func (issue *Issue) LoadProject(ctx context.Context) (err error) {
|
||||||
return err
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
issue.ProjectIssue.Project = issue.Project
|
||||||
|
|
||||||
|
return issue.ProjectIssue.LoadProjectColumn(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
func (issue *Issue) projectID(ctx context.Context) int64 {
|
func (issue *Issue) projectID(ctx context.Context) int64 {
|
||||||
var ip project_model.ProjectIssue
|
var ip project_model.ProjectIssue
|
||||||
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
|
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
|
||||||
|
|
|
@ -14,12 +14,14 @@ import (
|
||||||
|
|
||||||
// ProjectIssue saves relation from issue to a project
|
// ProjectIssue saves relation from issue to a project
|
||||||
type ProjectIssue struct { //revive:disable-line:exported
|
type ProjectIssue struct { //revive:disable-line:exported
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
IssueID int64 `xorm:"INDEX"`
|
IssueID int64 `xorm:"INDEX"`
|
||||||
ProjectID 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 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
|
// the sorting order on the column
|
||||||
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
|
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||||
|
@ -34,6 +36,50 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error
|
||||||
return err
|
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
|
// NumIssues return counter of all issues assigned to a project
|
||||||
func (p *Project) NumIssues(ctx context.Context) int {
|
func (p *Project) NumIssues(ctx context.Context) int {
|
||||||
c, err := db.GetEngine(ctx).Table("project_issue").
|
c, err := db.GetEngine(ctx).Table("project_issue").
|
||||||
|
@ -100,6 +146,41 @@ func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueI
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MoveIssueToColumnTail(ctx context.Context, issue *ProjectIssue, toColumn *Column) error {
|
||||||
|
nextSorting, err := toColumn.getNextSorting(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
_, err = db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?",
|
||||||
|
toColumn.ID, nextSorting, issue.IssueID)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Column) getNextSorting(ctx context.Context) (int64, error) {
|
||||||
|
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=?", c.ProjectID).
|
||||||
|
And("project_board_id=?", c.ID).
|
||||||
|
Get(&res); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.IssueCount > 0 {
|
||||||
|
return res.MaxSorting + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
|
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
|
||||||
if c.ProjectID != newColumn.ProjectID {
|
if c.ProjectID != newColumn.ProjectID {
|
||||||
return fmt.Errorf("columns have to be in the same project")
|
return fmt.Errorf("columns have to be in the same project")
|
||||||
|
@ -109,15 +190,8 @@ func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Colum
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res := struct {
|
nextSorting, err := newColumn.getNextSorting(ctx)
|
||||||
MaxSorting int64
|
if err != nil {
|
||||||
IssueCount int64
|
|
||||||
}{}
|
|
||||||
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).
|
|
||||||
Get(&res); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +203,6 @@ func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Colum
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
for i, issue := range issues {
|
for i, issue := range issues {
|
||||||
issue.ProjectColumnID = newColumn.ID
|
issue.ProjectColumnID = newColumn.ID
|
||||||
|
|
|
@ -1753,6 +1753,7 @@ issues.content_history.delete_from_history = Delete from history
|
||||||
issues.content_history.delete_from_history_confirm = Delete from history?
|
issues.content_history.delete_from_history_confirm = Delete from history?
|
||||||
issues.content_history.options = Options
|
issues.content_history.options = Options
|
||||||
issues.reference_link = Reference: %s
|
issues.reference_link = Reference: %s
|
||||||
|
issues.move_project_boad = Status
|
||||||
|
|
||||||
compare.compare_base = base
|
compare.compare_base = base
|
||||||
compare.compare_head = compare
|
compare.compare_head = compare
|
||||||
|
|
|
@ -2046,6 +2046,17 @@ func ViewIssue(ctx *context.Context) {
|
||||||
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
|
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["ProjectColumns"], err = issue.Project.GetColumns(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("Project.GetBoards", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplIssueView)
|
ctx.HTML(http.StatusOK, tplIssueView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -579,6 +579,72 @@ func SetDefaultProjectColumn(ctx *context.Context) {
|
||||||
ctx.JSONOK()
|
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
|
// MoveIssues moves or keeps issues in a column and sorts them inside that column
|
||||||
func MoveIssues(ctx *context.Context) {
|
func MoveIssues(ctx *context.Context) {
|
||||||
if ctx.Doer == nil {
|
if ctx.Doer == nil {
|
||||||
|
|
|
@ -1212,6 +1212,7 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue)
|
m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue)
|
||||||
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
|
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
|
||||||
m.Post("/delete", reqRepoAdmin, repo.DeleteIssue)
|
m.Post("/delete", reqRepoAdmin, repo.DeleteIssue)
|
||||||
|
m.Post("/move_project_column/{columnID}", repo.MoveColumnForIssue)
|
||||||
}, context.RepoMustNotBeArchived())
|
}, context.RepoMustNotBeArchived())
|
||||||
|
|
||||||
m.Group("/{index}", func() {
|
m.Group("/{index}", func() {
|
||||||
|
|
|
@ -193,13 +193,25 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<span class="no-select item {{if .Issue.Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
|
||||||
<div class="selected">
|
<div class="selected">
|
||||||
{{if .Issue.Project}}
|
{{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}}
|
{{svg .Issue.Project.IconName 18 "tw-mr-2"}}{{.Issue.Project.Title}}
|
||||||
</a>
|
</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}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -737,3 +737,29 @@ export function initArchivedLabelHandler() {
|
||||||
toggleElem(label, label.classList.contains('checked'));
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
|
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
|
||||||
initRepoIssueTitleEdit, initRepoIssueWipToggle,
|
initRepoIssueTitleEdit, initRepoIssueWipToggle,
|
||||||
initRepoPullRequestUpdate, updateIssuesMeta, initIssueTemplateCommentEditors, initSingleCommentEditor,
|
initRepoPullRequestUpdate, updateIssuesMeta, initIssueTemplateCommentEditors, initSingleCommentEditor,
|
||||||
|
initIssueProjectColumnSelector,
|
||||||
} from './repo-issue.js';
|
} from './repo-issue.js';
|
||||||
import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
|
import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
|
||||||
import {svg} from '../svg.js';
|
import {svg} from '../svg.js';
|
||||||
|
@ -393,6 +394,7 @@ export function initRepository() {
|
||||||
initRepoIssueCodeCommentCancel();
|
initRepoIssueCodeCommentCancel();
|
||||||
initRepoPullRequestUpdate();
|
initRepoPullRequestUpdate();
|
||||||
initCompReactionSelector();
|
initCompReactionSelector();
|
||||||
|
initIssueProjectColumnSelector();
|
||||||
|
|
||||||
initRepoPullRequestMergeForm();
|
initRepoPullRequestMergeForm();
|
||||||
initRepoPullRequestCommitStatus();
|
initRepoPullRequestCommitStatus();
|
||||||
|
|
Loading…
Reference in New Issue
Block a user