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>tags/v1.22.0-rc0
@@ -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 |
@@ -568,6 +568,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 |
@@ -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() | |||
} |
@@ -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,40 +258,60 @@ 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{ | |||
if _, err := GetBoard(ctx, boardID); err != nil { | |||
return err | |||
} | |||
if _, err := db.GetEngine(ctx).Where(builder.Eq{ | |||
"project_id": projectID, | |||
"`default`": true, | |||
}).Cols("`default`").Update(&Board{Default: false}) | |||
if err != nil { | |||
}).Cols("`default`").Update(&Board{Default: false}); 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}) | |||
} | |||
_, err := db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}). | |||
Cols("`default`").Update(&Board{Default: true}) | |||
return err | |||
} |
@@ -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) | |||
} |
@@ -1392,7 +1392,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" | |||
@@ -1400,10 +1399,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 | |||
} | |||
@@ -315,10 +315,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) | |||
@@ -583,21 +579,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 { | |||
@@ -628,28 +609,19 @@ func MoveIssues(ctx *context.Context) { | |||
return | |||
} | |||
var board *project_model.Board | |||
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 | |||
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 { |
@@ -1008,7 +1008,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) | |||
}) | |||
@@ -1348,7 +1347,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, |