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:
Denys Konovalov 2024-03-27 21:54:32 +01:00 committed by Earl Warren
parent b019ecce89
commit 8ffb9c6fb1
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
17 changed files with 400 additions and 196 deletions

View file

@ -45,3 +45,27 @@
type: 2 type: 2
created_unix: 1688973000 created_unix: 1688973000
updated_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

View file

@ -3,6 +3,7 @@
project_id: 1 project_id: 1
title: To Do title: To Do
creator_id: 2 creator_id: 2
default: true
created_unix: 1588117528 created_unix: 1588117528
updated_unix: 1588117528 updated_unix: 1588117528
@ -29,3 +30,48 @@
creator_id: 2 creator_id: 2
created_unix: 1588117528 created_unix: 1588117528
updated_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

View file

@ -49,18 +49,13 @@ func (issue *Issue) ProjectBoardID(ctx context.Context) int64 {
// LoadIssuesFromBoard load issues assigned to this board // LoadIssuesFromBoard load issues assigned to this board
func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) { func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) {
issueList := make(IssueList, 0, 10) issueList, err := Issues(ctx, &IssuesOptions{
ProjectBoardID: b.ID,
if b.ID > 0 { ProjectID: b.ProjectID,
issues, err := Issues(ctx, &IssuesOptions{ SortType: "project-column-sorting",
ProjectBoardID: b.ID, })
ProjectID: b.ProjectID, if err != nil {
SortType: "project-column-sorting", return nil, err
})
if err != nil {
return nil, err
}
issueList = issues
} }
if b.Default { if b.Default {

View file

@ -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

View file

@ -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

View file

@ -570,6 +570,8 @@ var migrations = []Migration{
NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable), NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable),
// v291 -> v292 // v291 -> v292
NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment), 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 // GetCurrentDBVersion returns the current db version

View 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()
}

View 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)
}

View file

@ -123,6 +123,17 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error {
return nil 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 { if len(items) == 0 {
return nil return nil
} }
@ -176,6 +187,10 @@ func deleteBoardByID(ctx context.Context, boardID int64) error {
return err return err
} }
if board.Default {
return fmt.Errorf("deleteBoardByID: cannot delete default board")
}
if err = board.removeIssues(ctx); err != nil { if err = board.removeIssues(ctx); err != nil {
return err return err
} }
@ -228,7 +243,6 @@ func UpdateBoard(ctx context.Context, board *Board) error {
} }
// GetBoards fetches all boards related to a project // 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) { func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
boards := make([]*Board, 0, 5) boards := make([]*Board, 0, 5)
@ -244,41 +258,61 @@ func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
return append([]*Board{defaultB}, boards...), nil 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) { func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) {
var board Board var boards []Board
exist, err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, true).Get(&board) if err := db.GetEngine(ctx).Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil {
if err != nil {
return nil, err 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 return &board, nil
} }
// represents a board for issues not assigned to one // unset default boards where too many default boards exist
return &Board{ if len(boards) > 1 {
ProjectID: p.ID, var boardsToUpdate []int64
Title: "Uncategorized", for id, b := range boards {
Default: true, if id > 0 {
}, nil 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 // 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 { func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error {
_, err := db.GetEngine(ctx).Where(builder.Eq{ if _, err := GetBoard(ctx, boardID); err != nil {
"project_id": projectID,
"`default`": true,
}).Cols("`default`").Update(&Board{Default: false})
if err != nil {
return err return err
} }
if boardID > 0 { if _, err := db.GetEngine(ctx).Where(builder.Eq{
_, err = db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}). "project_id": projectID,
Cols("`default`").Update(&Board{Default: true}) "`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 return err
} }

View 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)
}

View file

@ -92,19 +92,19 @@ func TestProjectsSort(t *testing.T) {
}{ }{
{ {
sortType: "default", sortType: "default",
wants: []int64{1, 3, 2, 4}, wants: []int64{1, 3, 2, 6, 5, 4},
}, },
{ {
sortType: "oldest", sortType: "oldest",
wants: []int64{4, 2, 3, 1}, wants: []int64{4, 5, 6, 2, 3, 1},
}, },
{ {
sortType: "recentupdate", sortType: "recentupdate",
wants: []int64{1, 3, 2, 4}, wants: []int64{1, 3, 2, 6, 5, 4},
}, },
{ {
sortType: "leastupdate", 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), OrderBy: GetSearchOrderByBySortType(tt.sortType),
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, int64(4), count) assert.EqualValues(t, int64(6), count)
if assert.Len(t, projects, 4) { if assert.Len(t, projects, 6) {
for i := range projects { for i := range projects {
assert.EqualValues(t, tt.wants[i], projects[i].ID) assert.EqualValues(t, tt.wants[i], projects[i].ID)
} }

View file

@ -1423,7 +1423,6 @@ projects.type.basic_kanban = Basic Kanban
projects.type.bug_triage = Bug Triage projects.type.bug_triage = Bug Triage
projects.template.desc = Template projects.template.desc = Template
projects.template.desc_helper = Select a project template to get started projects.template.desc_helper = Select a project template to get started
projects.type.uncategorized = Uncategorized
projects.column.edit = Edit Column projects.column.edit = Edit Column
projects.column.edit_title = Name projects.column.edit_title = Name
projects.column.new_title = Name projects.column.new_title = Name
@ -1431,10 +1430,8 @@ projects.column.new_submit = Create Column
projects.column.new = New Column projects.column.new = New Column
projects.column.set_default = Set Default projects.column.set_default = Set Default
projects.column.set_default_desc = Set this column as default for uncategorized issues and pulls 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.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.column.color = Color
projects.open = Open projects.open = Open
projects.close = Close projects.close = Close

View file

@ -207,11 +207,7 @@ func ChangeProjectStatus(ctx *context.Context) {
id := ctx.ParamsInt64(":id") id := ctx.ParamsInt64(":id")
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil { if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", err)
} else {
ctx.ServerError("ChangeProjectStatusByRepoIDAndID", err)
}
return return
} }
ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects?state=" + url.QueryEscape(ctx.Params(":action"))) 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) { func DeleteProject(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
if p.OwnerID != ctx.ContextUser.ID { if p.OwnerID != ctx.ContextUser.ID {
@ -254,11 +246,7 @@ func RenderEditProject(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
if p.OwnerID != ctx.ContextUser.ID { if p.OwnerID != ctx.ContextUser.ID {
@ -303,11 +291,7 @@ func EditProjectPost(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, projectID) p, err := project_model.GetProjectByID(ctx, projectID)
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
if p.OwnerID != ctx.ContextUser.ID { if p.OwnerID != ctx.ContextUser.ID {
@ -335,11 +319,7 @@ func EditProjectPost(ctx *context.Context) {
func ViewProject(ctx *context.Context) { func ViewProject(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
if project.OwnerID != ctx.ContextUser.ID { if project.OwnerID != ctx.ContextUser.ID {
@ -353,10 +333,6 @@ func ViewProject(ctx *context.Context) {
return return
} }
if boards[0].ID == 0 {
boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
}
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
if err != nil { if err != nil {
ctx.ServerError("LoadIssuesOfBoards", err) ctx.ServerError("LoadIssuesOfBoards", err)
@ -493,11 +469,7 @@ func DeleteProjectBoard(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
@ -534,11 +506,7 @@ func AddBoardToProjectPost(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
@ -566,11 +534,7 @@ func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return nil, nil return nil, nil
} }
@ -636,21 +600,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
ctx.JSONOK() 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 // 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 {
@ -662,11 +611,7 @@ func MoveIssues(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("ProjectNotExist", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
if project.OwnerID != ctx.ContextUser.ID { if project.OwnerID != ctx.ContextUser.ID {
@ -674,28 +619,15 @@ func MoveIssues(ctx *context.Context) {
return 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 { if board.ProjectID != project.ID {
board = &project_model.Board{ ctx.NotFound("BoardNotInProject", nil)
ID: 0, return
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
}
} }
type movedIssuesForm struct { type movedIssuesForm struct {
@ -718,11 +650,7 @@ func MoveIssues(ctx *context.Context) {
} }
movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
if err != nil { if err != nil {
if issues_model.IsErrIssueNotExist(err) { ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err)
ctx.NotFound("IssueNotExisting", nil)
} else {
ctx.ServerError("GetIssueByID", err)
}
return return
} }

View file

@ -314,10 +314,6 @@ func ViewProject(ctx *context.Context) {
return return
} }
if boards[0].ID == 0 {
boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
}
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
if err != nil { if err != nil {
ctx.ServerError("LoadIssuesOfBoards", err) ctx.ServerError("LoadIssuesOfBoards", err)
@ -582,21 +578,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
ctx.JSONOK() 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 // 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 {
@ -627,28 +608,19 @@ func MoveIssues(ctx *context.Context) {
return 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 { if board.ProjectID != project.ID {
board = &project_model.Board{ ctx.NotFound("BoardNotInProject", nil)
ID: 0, return
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
}
} }
type movedIssuesForm struct { type movedIssuesForm struct {

View file

@ -986,7 +986,6 @@ func registerRoutes(m *web.Route) {
m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard) m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard)
m.Delete("", org.DeleteProjectBoard) m.Delete("", org.DeleteProjectBoard)
m.Post("/default", org.SetDefaultProjectBoard) m.Post("/default", org.SetDefaultProjectBoard)
m.Post("/unsetdefault", org.UnsetDefaultProjectBoard)
m.Post("/move", org.MoveIssues) m.Post("/move", org.MoveIssues)
}) })
@ -1360,7 +1359,6 @@ func registerRoutes(m *web.Route) {
m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard) m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
m.Delete("", repo.DeleteProjectBoard) m.Delete("", repo.DeleteProjectBoard)
m.Post("/default", repo.SetDefaultProjectBoard) m.Post("/default", repo.SetDefaultProjectBoard)
m.Post("/unsetdefault", repo.UnSetDefaultProjectBoard)
m.Post("/move", repo.MoveIssues) m.Post("/move", repo.MoveIssues)
}) })

View file

@ -74,7 +74,7 @@
</div> </div>
{{.Title}} {{.Title}}
</div> </div>
{{if and $canWriteProject (ne .ID 0)}} {{if $canWriteProject}}
<div class="ui dropdown jump item"> <div class="ui dropdown jump item">
<div class="tw-px-2"> <div class="tw-px-2">
{{svg "octicon-kebab-horizontal"}} {{svg "octicon-kebab-horizontal"}}
@ -86,29 +86,20 @@
</a> </a>
{{if not .Default}} {{if not .Default}}
<a class="item show-modal button default-project-column-show" <a class="item show-modal button default-project-column-show"
data-modal="#default-project-column-modal-{{.ID}}" 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-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-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}"
data-url="{{$.Link}}/{{.ID}}/default"> data-url="{{$.Link}}/{{.ID}}/default">
{{svg "octicon-pin"}} {{svg "octicon-pin"}}
{{ctx.Locale.Tr "repo.projects.column.set_default"}} {{ctx.Locale.Tr "repo.projects.column.set_default"}}
</a> </a>
{{else}} <a class="item show-modal button show-delete-project-column-modal"
<a class="item show-modal button default-project-column-show" data-modal="#delete-project-column-modal-{{.ID}}"
data-modal="#default-project-column-modal-{{.ID}}" data-url="{{$.Link}}/{{.ID}}">
data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.unset_default"}}" {{svg "octicon-trash"}}
data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.unset_default_desc"}}" {{ctx.Locale.Tr "repo.projects.column.delete"}}
data-url="{{$.Link}}/{{.ID}}/unsetdefault">
{{svg "octicon-pin-slash"}}
{{ctx.Locale.Tr "repo.projects.column.unset_default"}}
</a> </a>
{{end}} {{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="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}">
<div class="header"> <div class="header">
@ -165,7 +156,7 @@
<div class="divider"></div> <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)}} {{range (index $.IssuesMap .ID)}}
<div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}"> <div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">
{{template "repo/issue/card" (dict "Issue" . "Page" $)}} {{template "repo/issue/card" (dict "Issue" . "Page" $)}}

View file

@ -58,7 +58,6 @@ async function initRepoProjectSortable() {
createSortable(mainBoard, { createSortable(mainBoard, {
group: 'project-column', group: 'project-column',
draggable: '.project-column', draggable: '.project-column',
filter: '[data-id="0"]',
animation: 150, animation: 150,
ghostClass: 'card-ghost', ghostClass: 'card-ghost',
delayOnTouchOnly: true, delayOnTouchOnly: true,