Add default board to new projects, remove uncategorized pseudo-board (#29874)
On creation of an empty project (no template) a default board will be created instead of falling back to the uneditable pseudo-board. Every project now has to have exactly one default boards. As a consequence, you cannot unset a board as default, instead you have to set another board as default. Existing projects will be modified using a cron job, additionally this check will run every midnight by default. Deleting the default board is not allowed, you have to set another board as default to do it. Fixes #29873 Fixes #14679 along the way Fixes #29853 Co-authored-by: delvh <dev.lh@web.de> (cherry picked from commit e5160185ed65fd1c2bcb2fc7dc7e0b5514ddb299) Conflicts: options/locale/locale_en-US.ini trivial conflict because Forgejo strings do not have surrounding double quotes
This commit is contained in:
parent
b019ecce89
commit
8ffb9c6fb1
|
@ -45,3 +45,27 @@
|
|||
type: 2
|
||||
created_unix: 1688973000
|
||||
updated_unix: 1688973000
|
||||
|
||||
-
|
||||
id: 5
|
||||
title: project without default column
|
||||
owner_id: 2
|
||||
repo_id: 0
|
||||
is_closed: false
|
||||
creator_id: 2
|
||||
board_type: 1
|
||||
type: 2
|
||||
created_unix: 1688973000
|
||||
updated_unix: 1688973000
|
||||
|
||||
-
|
||||
id: 6
|
||||
title: project with multiple default columns
|
||||
owner_id: 2
|
||||
repo_id: 0
|
||||
is_closed: false
|
||||
creator_id: 2
|
||||
board_type: 1
|
||||
type: 2
|
||||
created_unix: 1688973000
|
||||
updated_unix: 1688973000
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
project_id: 1
|
||||
title: To Do
|
||||
creator_id: 2
|
||||
default: true
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
|
@ -29,3 +30,48 @@
|
|||
creator_id: 2
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
-
|
||||
id: 5
|
||||
project_id: 2
|
||||
title: Backlog
|
||||
creator_id: 2
|
||||
default: true
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
-
|
||||
id: 6
|
||||
project_id: 4
|
||||
title: Backlog
|
||||
creator_id: 2
|
||||
default: true
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
-
|
||||
id: 7
|
||||
project_id: 5
|
||||
title: Done
|
||||
creator_id: 2
|
||||
default: false
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
-
|
||||
id: 8
|
||||
project_id: 6
|
||||
title: Backlog
|
||||
creator_id: 2
|
||||
default: true
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
-
|
||||
id: 9
|
||||
project_id: 6
|
||||
title: Uncategorized
|
||||
creator_id: 2
|
||||
default: true
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
|
|
@ -49,18 +49,13 @@ func (issue *Issue) ProjectBoardID(ctx context.Context) int64 {
|
|||
|
||||
// LoadIssuesFromBoard load issues assigned to this board
|
||||
func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) {
|
||||
issueList := make(IssueList, 0, 10)
|
||||
|
||||
if b.ID > 0 {
|
||||
issues, err := Issues(ctx, &IssuesOptions{
|
||||
ProjectBoardID: b.ID,
|
||||
ProjectID: b.ProjectID,
|
||||
SortType: "project-column-sorting",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issueList = issues
|
||||
issueList, err := Issues(ctx, &IssuesOptions{
|
||||
ProjectBoardID: b.ID,
|
||||
ProjectID: b.ProjectID,
|
||||
SortType: "project-column-sorting",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if b.Default {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
-
|
||||
id: 1
|
||||
title: project without default column
|
||||
owner_id: 2
|
||||
repo_id: 0
|
||||
is_closed: false
|
||||
creator_id: 2
|
||||
board_type: 1
|
||||
type: 2
|
||||
created_unix: 1688973000
|
||||
updated_unix: 1688973000
|
||||
|
||||
-
|
||||
id: 2
|
||||
title: project with multiple default columns
|
||||
owner_id: 2
|
||||
repo_id: 0
|
||||
is_closed: false
|
||||
creator_id: 2
|
||||
board_type: 1
|
||||
type: 2
|
||||
created_unix: 1688973000
|
||||
updated_unix: 1688973000
|
|
@ -0,0 +1,26 @@
|
|||
-
|
||||
id: 1
|
||||
project_id: 1
|
||||
title: Done
|
||||
creator_id: 2
|
||||
default: false
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
-
|
||||
id: 2
|
||||
project_id: 2
|
||||
title: Backlog
|
||||
creator_id: 2
|
||||
default: true
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
-
|
||||
id: 3
|
||||
project_id: 2
|
||||
title: Uncategorized
|
||||
creator_id: 2
|
||||
default: true
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
|
@ -570,6 +570,8 @@ var migrations = []Migration{
|
|||
NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable),
|
||||
// v291 -> v292
|
||||
NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment),
|
||||
// v292 -> v293
|
||||
NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
|
85
models/migrations/v1_22/v292.go
Normal file
85
models/migrations/v1_22/v292.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/project"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CheckProjectColumnsConsistency ensures there is exactly one default board per project present
|
||||
func CheckProjectColumnsConsistency(x *xorm.Engine) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
limit := setting.Database.IterateBufferSize
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
start := 0
|
||||
|
||||
for {
|
||||
var projects []project.Project
|
||||
if err := sess.SQL("SELECT DISTINCT `p`.`id`, `p`.`creator_id` FROM `project` `p` WHERE (SELECT COUNT(*) FROM `project_board` `pb` WHERE `pb`.`project_id` = `p`.`id` AND `pb`.`default` = ?) != 1", true).
|
||||
Limit(limit, start).
|
||||
Find(&projects); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(projects) == 0 {
|
||||
break
|
||||
}
|
||||
start += len(projects)
|
||||
|
||||
for _, p := range projects {
|
||||
var boards []project.Board
|
||||
if err := sess.Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(boards) == 0 {
|
||||
if _, err := sess.Insert(project.Board{
|
||||
ProjectID: p.ID,
|
||||
Default: true,
|
||||
Title: "Uncategorized",
|
||||
CreatorID: p.CreatorID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var boardsToUpdate []int64
|
||||
for id, b := range boards {
|
||||
if id > 0 {
|
||||
boardsToUpdate = append(boardsToUpdate, b.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := sess.Where(builder.Eq{"project_id": p.ID}.And(builder.In("id", boardsToUpdate))).
|
||||
Cols("`default`").Update(&project.Board{Default: false}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if start%1000 == 0 {
|
||||
if err := sess.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
44
models/migrations/v1_22/v292_test.go
Normal file
44
models/migrations/v1_22/v292_test.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
"code.gitea.io/gitea/models/project"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_CheckProjectColumnsConsistency(t *testing.T) {
|
||||
// Prepare and load the testing database
|
||||
x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board))
|
||||
defer deferable()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, CheckProjectColumnsConsistency(x))
|
||||
|
||||
// check if default board was added
|
||||
var defaultBoard project.Board
|
||||
has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
assert.Equal(t, int64(1), defaultBoard.ProjectID)
|
||||
assert.True(t, defaultBoard.Default)
|
||||
|
||||
// check if multiple defaults were removed
|
||||
expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), expectDefaultBoard.ProjectID)
|
||||
assert.True(t, expectDefaultBoard.Default)
|
||||
|
||||
expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID)
|
||||
assert.False(t, expectNonDefaultBoard.Default)
|
||||
}
|
|
@ -123,6 +123,17 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
board := Board{
|
||||
CreatedUnix: timeutil.TimeStampNow(),
|
||||
CreatorID: project.CreatorID,
|
||||
Title: "Backlog",
|
||||
ProjectID: project.ID,
|
||||
Default: true,
|
||||
}
|
||||
if err := db.Insert(ctx, board); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -176,6 +187,10 @@ func deleteBoardByID(ctx context.Context, boardID int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if board.Default {
|
||||
return fmt.Errorf("deleteBoardByID: cannot delete default board")
|
||||
}
|
||||
|
||||
if err = board.removeIssues(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -228,7 +243,6 @@ func UpdateBoard(ctx context.Context, board *Board) error {
|
|||
}
|
||||
|
||||
// GetBoards fetches all boards related to a project
|
||||
// if no default board set, first board is a temporary "Uncategorized" board
|
||||
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
|
||||
boards := make([]*Board, 0, 5)
|
||||
|
||||
|
@ -244,41 +258,61 @@ func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
|
|||
return append([]*Board{defaultB}, boards...), nil
|
||||
}
|
||||
|
||||
// getDefaultBoard return default board and create a dummy if none exist
|
||||
// getDefaultBoard return default board and ensure only one exists
|
||||
func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) {
|
||||
var board Board
|
||||
exist, err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, true).Get(&board)
|
||||
if err != nil {
|
||||
var boards []Board
|
||||
if err := db.GetEngine(ctx).Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
|
||||
// create a default board if none is found
|
||||
if len(boards) == 0 {
|
||||
board := Board{
|
||||
ProjectID: p.ID,
|
||||
Default: true,
|
||||
Title: "Uncategorized",
|
||||
CreatorID: p.CreatorID,
|
||||
}
|
||||
if _, err := db.GetEngine(ctx).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &board, nil
|
||||
}
|
||||
|
||||
// represents a board for issues not assigned to one
|
||||
return &Board{
|
||||
ProjectID: p.ID,
|
||||
Title: "Uncategorized",
|
||||
Default: true,
|
||||
}, nil
|
||||
// unset default boards where too many default boards exist
|
||||
if len(boards) > 1 {
|
||||
var boardsToUpdate []int64
|
||||
for id, b := range boards {
|
||||
if id > 0 {
|
||||
boardsToUpdate = append(boardsToUpdate, b.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).Where(builder.Eq{"project_id": p.ID}.And(builder.In("id", boardsToUpdate))).
|
||||
Cols("`default`").Update(&Board{Default: false}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &boards[0], nil
|
||||
}
|
||||
|
||||
// SetDefaultBoard represents a board for issues not assigned to one
|
||||
// if boardID is 0 unset default
|
||||
func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where(builder.Eq{
|
||||
"project_id": projectID,
|
||||
"`default`": true,
|
||||
}).Cols("`default`").Update(&Board{Default: false})
|
||||
if err != nil {
|
||||
if _, err := GetBoard(ctx, boardID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if boardID > 0 {
|
||||
_, err = db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}).
|
||||
Cols("`default`").Update(&Board{Default: true})
|
||||
if _, err := db.GetEngine(ctx).Where(builder.Eq{
|
||||
"project_id": projectID,
|
||||
"`default`": true,
|
||||
}).Cols("`default`").Update(&Board{Default: false}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}).
|
||||
Cols("`default`").Update(&Board{Default: true})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
40
models/project/board_test.go
Normal file
40
models/project/board_test.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package project
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetDefaultBoard(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check if default board was added
|
||||
board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(5), board.ProjectID)
|
||||
assert.Equal(t, "Uncategorized", board.Title)
|
||||
|
||||
projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check if multiple defaults were removed
|
||||
board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(6), board.ProjectID)
|
||||
assert.Equal(t, int64(8), board.ID)
|
||||
|
||||
board, err = GetBoard(db.DefaultContext, 9)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(6), board.ProjectID)
|
||||
assert.False(t, board.Default)
|
||||
}
|
|
@ -92,19 +92,19 @@ func TestProjectsSort(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
sortType: "default",
|
||||
wants: []int64{1, 3, 2, 4},
|
||||
wants: []int64{1, 3, 2, 6, 5, 4},
|
||||
},
|
||||
{
|
||||
sortType: "oldest",
|
||||
wants: []int64{4, 2, 3, 1},
|
||||
wants: []int64{4, 5, 6, 2, 3, 1},
|
||||
},
|
||||
{
|
||||
sortType: "recentupdate",
|
||||
wants: []int64{1, 3, 2, 4},
|
||||
wants: []int64{1, 3, 2, 6, 5, 4},
|
||||
},
|
||||
{
|
||||
sortType: "leastupdate",
|
||||
wants: []int64{4, 2, 3, 1},
|
||||
wants: []int64{4, 5, 6, 2, 3, 1},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -113,8 +113,8 @@ func TestProjectsSort(t *testing.T) {
|
|||
OrderBy: GetSearchOrderByBySortType(tt.sortType),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, int64(4), count)
|
||||
if assert.Len(t, projects, 4) {
|
||||
assert.EqualValues(t, int64(6), count)
|
||||
if assert.Len(t, projects, 6) {
|
||||
for i := range projects {
|
||||
assert.EqualValues(t, tt.wants[i], projects[i].ID)
|
||||
}
|
||||
|
|
|
@ -1423,7 +1423,6 @@ projects.type.basic_kanban = Basic Kanban
|
|||
projects.type.bug_triage = Bug Triage
|
||||
projects.template.desc = Template
|
||||
projects.template.desc_helper = Select a project template to get started
|
||||
projects.type.uncategorized = Uncategorized
|
||||
projects.column.edit = Edit Column
|
||||
projects.column.edit_title = Name
|
||||
projects.column.new_title = Name
|
||||
|
@ -1431,10 +1430,8 @@ projects.column.new_submit = Create Column
|
|||
projects.column.new = New Column
|
||||
projects.column.set_default = Set Default
|
||||
projects.column.set_default_desc = Set this column as default for uncategorized issues and pulls
|
||||
projects.column.unset_default = Unset Default
|
||||
projects.column.unset_default_desc = Unset this column as default
|
||||
projects.column.delete = Delete Column
|
||||
projects.column.deletion_desc = Deleting a project column moves all related issues to "Uncategorized". Continue?
|
||||
projects.column.deletion_desc = Deleting a project column moves all related issues to the default column. Continue?
|
||||
projects.column.color = Color
|
||||
projects.open = Open
|
||||
projects.close = Close
|
||||
|
|
|
@ -207,11 +207,7 @@ func ChangeProjectStatus(ctx *context.Context) {
|
|||
id := ctx.ParamsInt64(":id")
|
||||
|
||||
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound("", err)
|
||||
} else {
|
||||
ctx.ServerError("ChangeProjectStatusByRepoIDAndID", err)
|
||||
}
|
||||
ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects?state=" + url.QueryEscape(ctx.Params(":action")))
|
||||
|
@ -221,11 +217,7 @@ func ChangeProjectStatus(ctx *context.Context) {
|
|||
func DeleteProject(ctx *context.Context) {
|
||||
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound("", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
}
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if p.OwnerID != ctx.ContextUser.ID {
|
||||
|
@ -254,11 +246,7 @@ func RenderEditProject(ctx *context.Context) {
|
|||
|
||||
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound("", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
}
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if p.OwnerID != ctx.ContextUser.ID {
|
||||
|
@ -303,11 +291,7 @@ func EditProjectPost(ctx *context.Context) {
|
|||
|
||||
p, err := project_model.GetProjectByID(ctx, projectID)
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound("", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
}
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if p.OwnerID != ctx.ContextUser.ID {
|
||||
|
@ -335,11 +319,7 @@ func EditProjectPost(ctx *context.Context) {
|
|||
func ViewProject(ctx *context.Context) {
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound("", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
}
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
|
@ -353,10 +333,6 @@ func ViewProject(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if boards[0].ID == 0 {
|
||||
boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
|
||||
}
|
||||
|
||||
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesOfBoards", err)
|
||||
|
@ -493,11 +469,7 @@ func DeleteProjectBoard(ctx *context.Context) {
|
|||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound("", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
}
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -534,11 +506,7 @@ func AddBoardToProjectPost(ctx *context.Context) {
|
|||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound("", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
}
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -566,11 +534,7 @@ func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
|
|||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound("", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
}
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -636,21 +600,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
|
|||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// UnsetDefaultProjectBoard unset default board for uncategorized issues/pulls
|
||||
func UnsetDefaultProjectBoard(ctx *context.Context) {
|
||||
project, _ := CheckProjectBoardChangePermissions(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil {
|
||||
ctx.ServerError("SetDefaultBoard", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// MoveIssues moves or keeps issues in a column and sorts them inside that column
|
||||
func MoveIssues(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
|
@ -662,11 +611,7 @@ func MoveIssues(ctx *context.Context) {
|
|||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound("ProjectNotExist", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
}
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
|
@ -674,28 +619,15 @@ func MoveIssues(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var board *project_model.Board
|
||||
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.ParamsInt64(":boardID") == 0 {
|
||||
board = &project_model.Board{
|
||||
ID: 0,
|
||||
ProjectID: project.ID,
|
||||
Title: ctx.Locale.TrString("repo.projects.type.uncategorized"),
|
||||
}
|
||||
} else {
|
||||
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 != project.ID {
|
||||
ctx.NotFound("BoardNotInProject", nil)
|
||||
return
|
||||
}
|
||||
if board.ProjectID != project.ID {
|
||||
ctx.NotFound("BoardNotInProject", nil)
|
||||
return
|
||||
}
|
||||
|
||||
type movedIssuesForm struct {
|
||||
|
@ -718,11 +650,7 @@ func MoveIssues(ctx *context.Context) {
|
|||
}
|
||||
movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.NotFound("IssueNotExisting", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetIssueByID", err)
|
||||
}
|
||||
ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -314,10 +314,6 @@ func ViewProject(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if boards[0].ID == 0 {
|
||||
boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
|
||||
}
|
||||
|
||||
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesOfBoards", err)
|
||||
|
@ -582,21 +578,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
|
|||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// UnSetDefaultProjectBoard unset default board for uncategorized issues/pulls
|
||||
func UnSetDefaultProjectBoard(ctx *context.Context) {
|
||||
project, _ := checkProjectBoardChangePermissions(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil {
|
||||
ctx.ServerError("SetDefaultBoard", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// MoveIssues moves or keeps issues in a column and sorts them inside that column
|
||||
func MoveIssues(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
|
@ -627,28 +608,19 @@ func MoveIssues(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var board *project_model.Board
|
||||
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 ctx.ParamsInt64(":boardID") == 0 {
|
||||
board = &project_model.Board{
|
||||
ID: 0,
|
||||
ProjectID: project.ID,
|
||||
Title: ctx.Locale.TrString("repo.projects.type.uncategorized"),
|
||||
}
|
||||
} else {
|
||||
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 != project.ID {
|
||||
ctx.NotFound("BoardNotInProject", nil)
|
||||
return
|
||||
}
|
||||
if board.ProjectID != project.ID {
|
||||
ctx.NotFound("BoardNotInProject", nil)
|
||||
return
|
||||
}
|
||||
|
||||
type movedIssuesForm struct {
|
||||
|
|
|
@ -986,7 +986,6 @@ func registerRoutes(m *web.Route) {
|
|||
m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard)
|
||||
m.Delete("", org.DeleteProjectBoard)
|
||||
m.Post("/default", org.SetDefaultProjectBoard)
|
||||
m.Post("/unsetdefault", org.UnsetDefaultProjectBoard)
|
||||
|
||||
m.Post("/move", org.MoveIssues)
|
||||
})
|
||||
|
@ -1360,7 +1359,6 @@ func registerRoutes(m *web.Route) {
|
|||
m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
|
||||
m.Delete("", repo.DeleteProjectBoard)
|
||||
m.Post("/default", repo.SetDefaultProjectBoard)
|
||||
m.Post("/unsetdefault", repo.UnSetDefaultProjectBoard)
|
||||
|
||||
m.Post("/move", repo.MoveIssues)
|
||||
})
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
</div>
|
||||
{{.Title}}
|
||||
</div>
|
||||
{{if and $canWriteProject (ne .ID 0)}}
|
||||
{{if $canWriteProject}}
|
||||
<div class="ui dropdown jump item">
|
||||
<div class="tw-px-2">
|
||||
{{svg "octicon-kebab-horizontal"}}
|
||||
|
@ -86,29 +86,20 @@
|
|||
</a>
|
||||
{{if not .Default}}
|
||||
<a class="item show-modal button default-project-column-show"
|
||||
data-modal="#default-project-column-modal-{{.ID}}"
|
||||
data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}"
|
||||
data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}"
|
||||
data-url="{{$.Link}}/{{.ID}}/default">
|
||||
data-modal="#default-project-column-modal-{{.ID}}"
|
||||
data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}"
|
||||
data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}"
|
||||
data-url="{{$.Link}}/{{.ID}}/default">
|
||||
{{svg "octicon-pin"}}
|
||||
{{ctx.Locale.Tr "repo.projects.column.set_default"}}
|
||||
</a>
|
||||
{{else}}
|
||||
<a class="item show-modal button default-project-column-show"
|
||||
data-modal="#default-project-column-modal-{{.ID}}"
|
||||
data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.unset_default"}}"
|
||||
data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.unset_default_desc"}}"
|
||||
data-url="{{$.Link}}/{{.ID}}/unsetdefault">
|
||||
{{svg "octicon-pin-slash"}}
|
||||
{{ctx.Locale.Tr "repo.projects.column.unset_default"}}
|
||||
<a class="item show-modal button show-delete-project-column-modal"
|
||||
data-modal="#delete-project-column-modal-{{.ID}}"
|
||||
data-url="{{$.Link}}/{{.ID}}">
|
||||
{{svg "octicon-trash"}}
|
||||
{{ctx.Locale.Tr "repo.projects.column.delete"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="item show-modal button show-delete-project-column-modal"
|
||||
data-modal="#delete-project-column-modal-{{.ID}}"
|
||||
data-url="{{$.Link}}/{{.ID}}">
|
||||
{{svg "octicon-trash"}}
|
||||
{{ctx.Locale.Tr "repo.projects.column.delete"}}
|
||||
</a>
|
||||
|
||||
<div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}">
|
||||
<div class="header">
|
||||
|
@ -165,7 +156,7 @@
|
|||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="ui cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}tw-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
|
||||
<div class="ui cards{{if $canWriteProject}} tw-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
|
||||
{{range (index $.IssuesMap .ID)}}
|
||||
<div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">
|
||||
{{template "repo/issue/card" (dict "Issue" . "Page" $)}}
|
||||
|
|
|
@ -58,7 +58,6 @@ async function initRepoProjectSortable() {
|
|||
createSortable(mainBoard, {
|
||||
group: 'project-column',
|
||||
draggable: '.project-column',
|
||||
filter: '[data-id="0"]',
|
||||
animation: 150,
|
||||
ghostClass: 'card-ghost',
|
||||
delayOnTouchOnly: true,
|
||||
|
|
Loading…
Reference in a new issue