loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

Denys Konovalov
delvh
and committed by
Earl Warren
8ffb9c6f b019ecce

+399 -195
+24
models/fixtures/project.yml
··· 45 45 type: 2 46 46 created_unix: 1688973000 47 47 updated_unix: 1688973000 48 + 49 + - 50 + id: 5 51 + title: project without default column 52 + owner_id: 2 53 + repo_id: 0 54 + is_closed: false 55 + creator_id: 2 56 + board_type: 1 57 + type: 2 58 + created_unix: 1688973000 59 + updated_unix: 1688973000 60 + 61 + - 62 + id: 6 63 + title: project with multiple default columns 64 + owner_id: 2 65 + repo_id: 0 66 + is_closed: false 67 + creator_id: 2 68 + board_type: 1 69 + type: 2 70 + created_unix: 1688973000 71 + updated_unix: 1688973000
+46
models/fixtures/project_board.yml
··· 3 3 project_id: 1 4 4 title: To Do 5 5 creator_id: 2 6 + default: true 6 7 created_unix: 1588117528 7 8 updated_unix: 1588117528 8 9 ··· 29 30 creator_id: 2 30 31 created_unix: 1588117528 31 32 updated_unix: 1588117528 33 + 34 + - 35 + id: 5 36 + project_id: 2 37 + title: Backlog 38 + creator_id: 2 39 + default: true 40 + created_unix: 1588117528 41 + updated_unix: 1588117528 42 + 43 + - 44 + id: 6 45 + project_id: 4 46 + title: Backlog 47 + creator_id: 2 48 + default: true 49 + created_unix: 1588117528 50 + updated_unix: 1588117528 51 + 52 + - 53 + id: 7 54 + project_id: 5 55 + title: Done 56 + creator_id: 2 57 + default: false 58 + created_unix: 1588117528 59 + updated_unix: 1588117528 60 + 61 + - 62 + id: 8 63 + project_id: 6 64 + title: Backlog 65 + creator_id: 2 66 + default: true 67 + created_unix: 1588117528 68 + updated_unix: 1588117528 69 + 70 + - 71 + id: 9 72 + project_id: 6 73 + title: Uncategorized 74 + creator_id: 2 75 + default: true 76 + created_unix: 1588117528 77 + updated_unix: 1588117528
+7 -12
models/issues/issue_project.go
··· 49 49 50 50 // LoadIssuesFromBoard load issues assigned to this board 51 51 func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) { 52 - issueList := make(IssueList, 0, 10) 53 - 54 - if b.ID > 0 { 55 - issues, err := Issues(ctx, &IssuesOptions{ 56 - ProjectBoardID: b.ID, 57 - ProjectID: b.ProjectID, 58 - SortType: "project-column-sorting", 59 - }) 60 - if err != nil { 61 - return nil, err 62 - } 63 - issueList = issues 52 + issueList, err := Issues(ctx, &IssuesOptions{ 53 + ProjectBoardID: b.ID, 54 + ProjectID: b.ProjectID, 55 + SortType: "project-column-sorting", 56 + }) 57 + if err != nil { 58 + return nil, err 64 59 } 65 60 66 61 if b.Default {
+23
models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project.yml
··· 1 + - 2 + id: 1 3 + title: project without default column 4 + owner_id: 2 5 + repo_id: 0 6 + is_closed: false 7 + creator_id: 2 8 + board_type: 1 9 + type: 2 10 + created_unix: 1688973000 11 + updated_unix: 1688973000 12 + 13 + - 14 + id: 2 15 + title: project with multiple default columns 16 + owner_id: 2 17 + repo_id: 0 18 + is_closed: false 19 + creator_id: 2 20 + board_type: 1 21 + type: 2 22 + created_unix: 1688973000 23 + updated_unix: 1688973000
+26
models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml
··· 1 + - 2 + id: 1 3 + project_id: 1 4 + title: Done 5 + creator_id: 2 6 + default: false 7 + created_unix: 1588117528 8 + updated_unix: 1588117528 9 + 10 + - 11 + id: 2 12 + project_id: 2 13 + title: Backlog 14 + creator_id: 2 15 + default: true 16 + created_unix: 1588117528 17 + updated_unix: 1588117528 18 + 19 + - 20 + id: 3 21 + project_id: 2 22 + title: Uncategorized 23 + creator_id: 2 24 + default: true 25 + created_unix: 1588117528 26 + updated_unix: 1588117528
+2
models/migrations/migrations.go
··· 570 570 NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable), 571 571 // v291 -> v292 572 572 NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment), 573 + // v292 -> v293 574 + NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency), 573 575 } 574 576 575 577 // GetCurrentDBVersion returns the current db version
+85
models/migrations/v1_22/v292.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package v1_22 //nolint 5 + 6 + import ( 7 + "code.gitea.io/gitea/models/project" 8 + "code.gitea.io/gitea/modules/setting" 9 + 10 + "xorm.io/builder" 11 + "xorm.io/xorm" 12 + ) 13 + 14 + // CheckProjectColumnsConsistency ensures there is exactly one default board per project present 15 + func CheckProjectColumnsConsistency(x *xorm.Engine) error { 16 + sess := x.NewSession() 17 + defer sess.Close() 18 + 19 + if err := sess.Begin(); err != nil { 20 + return err 21 + } 22 + 23 + limit := setting.Database.IterateBufferSize 24 + if limit <= 0 { 25 + limit = 50 26 + } 27 + 28 + start := 0 29 + 30 + for { 31 + var projects []project.Project 32 + 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). 33 + Limit(limit, start). 34 + Find(&projects); err != nil { 35 + return err 36 + } 37 + 38 + if len(projects) == 0 { 39 + break 40 + } 41 + start += len(projects) 42 + 43 + for _, p := range projects { 44 + var boards []project.Board 45 + if err := sess.Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil { 46 + return err 47 + } 48 + 49 + if len(boards) == 0 { 50 + if _, err := sess.Insert(project.Board{ 51 + ProjectID: p.ID, 52 + Default: true, 53 + Title: "Uncategorized", 54 + CreatorID: p.CreatorID, 55 + }); err != nil { 56 + return err 57 + } 58 + continue 59 + } 60 + 61 + var boardsToUpdate []int64 62 + for id, b := range boards { 63 + if id > 0 { 64 + boardsToUpdate = append(boardsToUpdate, b.ID) 65 + } 66 + } 67 + 68 + if _, err := sess.Where(builder.Eq{"project_id": p.ID}.And(builder.In("id", boardsToUpdate))). 69 + Cols("`default`").Update(&project.Board{Default: false}); err != nil { 70 + return err 71 + } 72 + } 73 + 74 + if start%1000 == 0 { 75 + if err := sess.Commit(); err != nil { 76 + return err 77 + } 78 + if err := sess.Begin(); err != nil { 79 + return err 80 + } 81 + } 82 + } 83 + 84 + return sess.Commit() 85 + }
+44
models/migrations/v1_22/v292_test.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package v1_22 //nolint 5 + 6 + import ( 7 + "testing" 8 + 9 + "code.gitea.io/gitea/models/db" 10 + "code.gitea.io/gitea/models/migrations/base" 11 + "code.gitea.io/gitea/models/project" 12 + 13 + "github.com/stretchr/testify/assert" 14 + ) 15 + 16 + func Test_CheckProjectColumnsConsistency(t *testing.T) { 17 + // Prepare and load the testing database 18 + x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board)) 19 + defer deferable() 20 + if x == nil || t.Failed() { 21 + return 22 + } 23 + 24 + assert.NoError(t, CheckProjectColumnsConsistency(x)) 25 + 26 + // check if default board was added 27 + var defaultBoard project.Board 28 + has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard) 29 + assert.NoError(t, err) 30 + assert.True(t, has) 31 + assert.Equal(t, int64(1), defaultBoard.ProjectID) 32 + assert.True(t, defaultBoard.Default) 33 + 34 + // check if multiple defaults were removed 35 + expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2) 36 + assert.NoError(t, err) 37 + assert.Equal(t, int64(2), expectDefaultBoard.ProjectID) 38 + assert.True(t, expectDefaultBoard.Default) 39 + 40 + expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3) 41 + assert.NoError(t, err) 42 + assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID) 43 + assert.False(t, expectNonDefaultBoard.Default) 44 + }
+54 -20
models/project/board.go
··· 123 123 return nil 124 124 } 125 125 126 + board := Board{ 127 + CreatedUnix: timeutil.TimeStampNow(), 128 + CreatorID: project.CreatorID, 129 + Title: "Backlog", 130 + ProjectID: project.ID, 131 + Default: true, 132 + } 133 + if err := db.Insert(ctx, board); err != nil { 134 + return err 135 + } 136 + 126 137 if len(items) == 0 { 127 138 return nil 128 139 } ··· 174 185 } 175 186 176 187 return err 188 + } 189 + 190 + if board.Default { 191 + return fmt.Errorf("deleteBoardByID: cannot delete default board") 177 192 } 178 193 179 194 if err = board.removeIssues(ctx); err != nil { ··· 228 243 } 229 244 230 245 // GetBoards fetches all boards related to a project 231 - // if no default board set, first board is a temporary "Uncategorized" board 232 246 func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { 233 247 boards := make([]*Board, 0, 5) 234 248 ··· 244 258 return append([]*Board{defaultB}, boards...), nil 245 259 } 246 260 247 - // getDefaultBoard return default board and create a dummy if none exist 261 + // getDefaultBoard return default board and ensure only one exists 248 262 func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) { 249 - var board Board 250 - exist, err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, true).Get(&board) 251 - if err != nil { 263 + var boards []Board 264 + if err := db.GetEngine(ctx).Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil { 252 265 return nil, err 253 266 } 254 - if exist { 267 + 268 + // create a default board if none is found 269 + if len(boards) == 0 { 270 + board := Board{ 271 + ProjectID: p.ID, 272 + Default: true, 273 + Title: "Uncategorized", 274 + CreatorID: p.CreatorID, 275 + } 276 + if _, err := db.GetEngine(ctx).Insert(); err != nil { 277 + return nil, err 278 + } 255 279 return &board, nil 256 280 } 257 281 258 - // represents a board for issues not assigned to one 259 - return &Board{ 260 - ProjectID: p.ID, 261 - Title: "Uncategorized", 262 - Default: true, 263 - }, nil 282 + // unset default boards where too many default boards exist 283 + if len(boards) > 1 { 284 + var boardsToUpdate []int64 285 + for id, b := range boards { 286 + if id > 0 { 287 + boardsToUpdate = append(boardsToUpdate, b.ID) 288 + } 289 + } 290 + 291 + if _, err := db.GetEngine(ctx).Where(builder.Eq{"project_id": p.ID}.And(builder.In("id", boardsToUpdate))). 292 + Cols("`default`").Update(&Board{Default: false}); err != nil { 293 + return nil, err 294 + } 295 + } 296 + 297 + return &boards[0], nil 264 298 } 265 299 266 300 // SetDefaultBoard represents a board for issues not assigned to one 267 - // if boardID is 0 unset default 268 301 func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error { 269 - _, err := db.GetEngine(ctx).Where(builder.Eq{ 302 + if _, err := GetBoard(ctx, boardID); err != nil { 303 + return err 304 + } 305 + 306 + if _, err := db.GetEngine(ctx).Where(builder.Eq{ 270 307 "project_id": projectID, 271 308 "`default`": true, 272 - }).Cols("`default`").Update(&Board{Default: false}) 273 - if err != nil { 309 + }).Cols("`default`").Update(&Board{Default: false}); err != nil { 274 310 return err 275 311 } 276 312 277 - if boardID > 0 { 278 - _, err = db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}). 279 - Cols("`default`").Update(&Board{Default: true}) 280 - } 313 + _, err := db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}). 314 + Cols("`default`").Update(&Board{Default: true}) 281 315 282 316 return err 283 317 }
+40
models/project/board_test.go
··· 1 + // Copyright 2020 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package project 5 + 6 + import ( 7 + "testing" 8 + 9 + "code.gitea.io/gitea/models/db" 10 + "code.gitea.io/gitea/models/unittest" 11 + 12 + "github.com/stretchr/testify/assert" 13 + ) 14 + 15 + func TestGetDefaultBoard(t *testing.T) { 16 + assert.NoError(t, unittest.PrepareTestDatabase()) 17 + 18 + projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5) 19 + assert.NoError(t, err) 20 + 21 + // check if default board was added 22 + board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext) 23 + assert.NoError(t, err) 24 + assert.Equal(t, int64(5), board.ProjectID) 25 + assert.Equal(t, "Uncategorized", board.Title) 26 + 27 + projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6) 28 + assert.NoError(t, err) 29 + 30 + // check if multiple defaults were removed 31 + board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext) 32 + assert.NoError(t, err) 33 + assert.Equal(t, int64(6), board.ProjectID) 34 + assert.Equal(t, int64(8), board.ID) 35 + 36 + board, err = GetBoard(db.DefaultContext, 9) 37 + assert.NoError(t, err) 38 + assert.Equal(t, int64(6), board.ProjectID) 39 + assert.False(t, board.Default) 40 + }
+6 -6
models/project/project_test.go
··· 92 92 }{ 93 93 { 94 94 sortType: "default", 95 - wants: []int64{1, 3, 2, 4}, 95 + wants: []int64{1, 3, 2, 6, 5, 4}, 96 96 }, 97 97 { 98 98 sortType: "oldest", 99 - wants: []int64{4, 2, 3, 1}, 99 + wants: []int64{4, 5, 6, 2, 3, 1}, 100 100 }, 101 101 { 102 102 sortType: "recentupdate", 103 - wants: []int64{1, 3, 2, 4}, 103 + wants: []int64{1, 3, 2, 6, 5, 4}, 104 104 }, 105 105 { 106 106 sortType: "leastupdate", 107 - wants: []int64{4, 2, 3, 1}, 107 + wants: []int64{4, 5, 6, 2, 3, 1}, 108 108 }, 109 109 } 110 110 ··· 113 113 OrderBy: GetSearchOrderByBySortType(tt.sortType), 114 114 }) 115 115 assert.NoError(t, err) 116 - assert.EqualValues(t, int64(4), count) 117 - if assert.Len(t, projects, 4) { 116 + assert.EqualValues(t, int64(6), count) 117 + if assert.Len(t, projects, 6) { 118 118 for i := range projects { 119 119 assert.EqualValues(t, tt.wants[i], projects[i].ID) 120 120 }
+1 -4
options/locale/locale_en-US.ini
··· 1423 1423 projects.type.bug_triage = Bug Triage 1424 1424 projects.template.desc = Template 1425 1425 projects.template.desc_helper = Select a project template to get started 1426 - projects.type.uncategorized = Uncategorized 1427 1426 projects.column.edit = Edit Column 1428 1427 projects.column.edit_title = Name 1429 1428 projects.column.new_title = Name ··· 1431 1430 projects.column.new = New Column 1432 1431 projects.column.set_default = Set Default 1433 1432 projects.column.set_default_desc = Set this column as default for uncategorized issues and pulls 1434 - projects.column.unset_default = Unset Default 1435 - projects.column.unset_default_desc = Unset this column as default 1436 1433 projects.column.delete = Delete Column 1437 - projects.column.deletion_desc = Deleting a project column moves all related issues to "Uncategorized". Continue? 1434 + projects.column.deletion_desc = Deleting a project column moves all related issues to the default column. Continue? 1438 1435 projects.column.color = Color 1439 1436 projects.open = Open 1440 1437 projects.close = Close
+18 -90
routers/web/org/projects.go
··· 207 207 id := ctx.ParamsInt64(":id") 208 208 209 209 if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil { 210 - if project_model.IsErrProjectNotExist(err) { 211 - ctx.NotFound("", err) 212 - } else { 213 - ctx.ServerError("ChangeProjectStatusByRepoIDAndID", err) 214 - } 210 + ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) 215 211 return 216 212 } 217 213 ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects?state=" + url.QueryEscape(ctx.Params(":action"))) ··· 221 217 func DeleteProject(ctx *context.Context) { 222 218 p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 223 219 if err != nil { 224 - if project_model.IsErrProjectNotExist(err) { 225 - ctx.NotFound("", nil) 226 - } else { 227 - ctx.ServerError("GetProjectByID", err) 228 - } 220 + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) 229 221 return 230 222 } 231 223 if p.OwnerID != ctx.ContextUser.ID { ··· 254 246 255 247 p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 256 248 if err != nil { 257 - if project_model.IsErrProjectNotExist(err) { 258 - ctx.NotFound("", nil) 259 - } else { 260 - ctx.ServerError("GetProjectByID", err) 261 - } 249 + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) 262 250 return 263 251 } 264 252 if p.OwnerID != ctx.ContextUser.ID { ··· 303 291 304 292 p, err := project_model.GetProjectByID(ctx, projectID) 305 293 if err != nil { 306 - if project_model.IsErrProjectNotExist(err) { 307 - ctx.NotFound("", nil) 308 - } else { 309 - ctx.ServerError("GetProjectByID", err) 310 - } 294 + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) 311 295 return 312 296 } 313 297 if p.OwnerID != ctx.ContextUser.ID { ··· 335 319 func ViewProject(ctx *context.Context) { 336 320 project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 337 321 if err != nil { 338 - if project_model.IsErrProjectNotExist(err) { 339 - ctx.NotFound("", nil) 340 - } else { 341 - ctx.ServerError("GetProjectByID", err) 342 - } 322 + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) 343 323 return 344 324 } 345 325 if project.OwnerID != ctx.ContextUser.ID { ··· 351 331 if err != nil { 352 332 ctx.ServerError("GetProjectBoards", err) 353 333 return 354 - } 355 - 356 - if boards[0].ID == 0 { 357 - boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized") 358 334 } 359 335 360 336 issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) ··· 493 469 494 470 project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 495 471 if err != nil { 496 - if project_model.IsErrProjectNotExist(err) { 497 - ctx.NotFound("", nil) 498 - } else { 499 - ctx.ServerError("GetProjectByID", err) 500 - } 472 + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) 501 473 return 502 474 } 503 475 ··· 534 506 535 507 project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 536 508 if err != nil { 537 - if project_model.IsErrProjectNotExist(err) { 538 - ctx.NotFound("", nil) 539 - } else { 540 - ctx.ServerError("GetProjectByID", err) 541 - } 509 + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) 542 510 return 543 511 } 544 512 ··· 566 534 567 535 project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 568 536 if err != nil { 569 - if project_model.IsErrProjectNotExist(err) { 570 - ctx.NotFound("", nil) 571 - } else { 572 - ctx.ServerError("GetProjectByID", err) 573 - } 537 + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) 574 538 return nil, nil 575 539 } 576 540 ··· 636 600 ctx.JSONOK() 637 601 } 638 602 639 - // UnsetDefaultProjectBoard unset default board for uncategorized issues/pulls 640 - func UnsetDefaultProjectBoard(ctx *context.Context) { 641 - project, _ := CheckProjectBoardChangePermissions(ctx) 642 - if ctx.Written() { 643 - return 644 - } 645 - 646 - if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil { 647 - ctx.ServerError("SetDefaultBoard", err) 648 - return 649 - } 650 - 651 - ctx.JSONOK() 652 - } 653 - 654 603 // MoveIssues moves or keeps issues in a column and sorts them inside that column 655 604 func MoveIssues(ctx *context.Context) { 656 605 if ctx.Doer == nil { ··· 662 611 663 612 project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 664 613 if err != nil { 665 - if project_model.IsErrProjectNotExist(err) { 666 - ctx.NotFound("ProjectNotExist", nil) 667 - } else { 668 - ctx.ServerError("GetProjectByID", err) 669 - } 614 + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) 670 615 return 671 616 } 672 617 if project.OwnerID != ctx.ContextUser.ID { ··· 674 619 return 675 620 } 676 621 677 - var board *project_model.Board 622 + board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) 623 + if err != nil { 624 + ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err) 625 + return 626 + } 678 627 679 - if ctx.ParamsInt64(":boardID") == 0 { 680 - board = &project_model.Board{ 681 - ID: 0, 682 - ProjectID: project.ID, 683 - Title: ctx.Locale.TrString("repo.projects.type.uncategorized"), 684 - } 685 - } else { 686 - board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) 687 - if err != nil { 688 - if project_model.IsErrProjectBoardNotExist(err) { 689 - ctx.NotFound("ProjectBoardNotExist", nil) 690 - } else { 691 - ctx.ServerError("GetProjectBoard", err) 692 - } 693 - return 694 - } 695 - if board.ProjectID != project.ID { 696 - ctx.NotFound("BoardNotInProject", nil) 697 - return 698 - } 628 + if board.ProjectID != project.ID { 629 + ctx.NotFound("BoardNotInProject", nil) 630 + return 699 631 } 700 632 701 633 type movedIssuesForm struct { ··· 718 650 } 719 651 movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) 720 652 if err != nil { 721 - if issues_model.IsErrIssueNotExist(err) { 722 - ctx.NotFound("IssueNotExisting", nil) 723 - } else { 724 - ctx.ServerError("GetIssueByID", err) 725 - } 653 + ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err) 726 654 return 727 655 } 728 656
+12 -40
routers/web/repo/projects.go
··· 314 314 return 315 315 } 316 316 317 - if boards[0].ID == 0 { 318 - boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized") 319 - } 320 - 321 317 issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) 322 318 if err != nil { 323 319 ctx.ServerError("LoadIssuesOfBoards", err) ··· 582 578 ctx.JSONOK() 583 579 } 584 580 585 - // UnSetDefaultProjectBoard unset default board for uncategorized issues/pulls 586 - func UnSetDefaultProjectBoard(ctx *context.Context) { 587 - project, _ := checkProjectBoardChangePermissions(ctx) 588 - if ctx.Written() { 589 - return 590 - } 591 - 592 - if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil { 593 - ctx.ServerError("SetDefaultBoard", err) 594 - return 595 - } 596 - 597 - ctx.JSONOK() 598 - } 599 - 600 581 // MoveIssues moves or keeps issues in a column and sorts them inside that column 601 582 func MoveIssues(ctx *context.Context) { 602 583 if ctx.Doer == nil { ··· 627 608 return 628 609 } 629 610 630 - var board *project_model.Board 631 - 632 - if ctx.ParamsInt64(":boardID") == 0 { 633 - board = &project_model.Board{ 634 - ID: 0, 635 - ProjectID: project.ID, 636 - Title: ctx.Locale.TrString("repo.projects.type.uncategorized"), 637 - } 638 - } else { 639 - board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) 640 - if err != nil { 641 - if project_model.IsErrProjectBoardNotExist(err) { 642 - ctx.NotFound("ProjectBoardNotExist", nil) 643 - } else { 644 - ctx.ServerError("GetProjectBoard", err) 645 - } 646 - return 647 - } 648 - if board.ProjectID != project.ID { 649 - ctx.NotFound("BoardNotInProject", nil) 650 - return 611 + board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) 612 + if err != nil { 613 + if project_model.IsErrProjectBoardNotExist(err) { 614 + ctx.NotFound("ProjectBoardNotExist", nil) 615 + } else { 616 + ctx.ServerError("GetProjectBoard", err) 651 617 } 618 + return 619 + } 620 + 621 + if board.ProjectID != project.ID { 622 + ctx.NotFound("BoardNotInProject", nil) 623 + return 652 624 } 653 625 654 626 type movedIssuesForm struct {
-2
routers/web/web.go
··· 986 986 m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard) 987 987 m.Delete("", org.DeleteProjectBoard) 988 988 m.Post("/default", org.SetDefaultProjectBoard) 989 - m.Post("/unsetdefault", org.UnsetDefaultProjectBoard) 990 989 991 990 m.Post("/move", org.MoveIssues) 992 991 }) ··· 1360 1359 m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard) 1361 1360 m.Delete("", repo.DeleteProjectBoard) 1362 1361 m.Post("/default", repo.SetDefaultProjectBoard) 1363 - m.Post("/unsetdefault", repo.UnSetDefaultProjectBoard) 1364 1362 1365 1363 m.Post("/move", repo.MoveIssues) 1366 1364 })
+11 -20
templates/projects/view.tmpl
··· 74 74 </div> 75 75 {{.Title}} 76 76 </div> 77 - {{if and $canWriteProject (ne .ID 0)}} 77 + {{if $canWriteProject}} 78 78 <div class="ui dropdown jump item"> 79 79 <div class="tw-px-2"> 80 80 {{svg "octicon-kebab-horizontal"}} ··· 86 86 </a> 87 87 {{if not .Default}} 88 88 <a class="item show-modal button default-project-column-show" 89 - data-modal="#default-project-column-modal-{{.ID}}" 90 - data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}" 91 - data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}" 92 - data-url="{{$.Link}}/{{.ID}}/default"> 89 + data-modal="#default-project-column-modal-{{.ID}}" 90 + data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}" 91 + data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}" 92 + data-url="{{$.Link}}/{{.ID}}/default"> 93 93 {{svg "octicon-pin"}} 94 94 {{ctx.Locale.Tr "repo.projects.column.set_default"}} 95 95 </a> 96 - {{else}} 97 - <a class="item show-modal button default-project-column-show" 98 - data-modal="#default-project-column-modal-{{.ID}}" 99 - data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.unset_default"}}" 100 - data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.unset_default_desc"}}" 101 - data-url="{{$.Link}}/{{.ID}}/unsetdefault"> 102 - {{svg "octicon-pin-slash"}} 103 - {{ctx.Locale.Tr "repo.projects.column.unset_default"}} 96 + <a class="item show-modal button show-delete-project-column-modal" 97 + data-modal="#delete-project-column-modal-{{.ID}}" 98 + data-url="{{$.Link}}/{{.ID}}"> 99 + {{svg "octicon-trash"}} 100 + {{ctx.Locale.Tr "repo.projects.column.delete"}} 104 101 </a> 105 102 {{end}} 106 - <a class="item show-modal button show-delete-project-column-modal" 107 - data-modal="#delete-project-column-modal-{{.ID}}" 108 - data-url="{{$.Link}}/{{.ID}}"> 109 - {{svg "octicon-trash"}} 110 - {{ctx.Locale.Tr "repo.projects.column.delete"}} 111 - </a> 112 103 113 104 <div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}"> 114 105 <div class="header"> ··· 165 156 166 157 <div class="divider"></div> 167 158 168 - <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}}"> 159 + <div class="ui cards{{if $canWriteProject}} tw-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}"> 169 160 {{range (index $.IssuesMap .ID)}} 170 161 <div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}"> 171 162 {{template "repo/issue/card" (dict "Issue" . "Page" $)}}
-1
web_src/js/features/repo-projects.js
··· 58 58 createSortable(mainBoard, { 59 59 group: 'project-column', 60 60 draggable: '.project-column', 61 - filter: '[data-id="0"]', 62 61 animation: 150, 63 62 ghostClass: 'card-ghost', 64 63 delayOnTouchOnly: true,