mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	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>
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
							
								
								
									
										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) | ||||
| 			} | ||||
|   | ||||
| @@ -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 | ||||
| 	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 { | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Denys Konovalov
					Denys Konovalov