1
0
mirror of https://github.com/go-gitea/gitea.git synced 2024-09-01 14:56:30 +00:00

Add default board to new projects, remove uncategorized pseudo-board ()

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 
Fixes  along the way
Fixes 

Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
Denys Konovalov 2024-03-27 21:54:32 +01:00 committed by GitHub
parent 4eb86d6823
commit e5160185ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 400 additions and 196 deletions
models
options/locale
routers/web
templates/projects
web_src/js/features

View File

@ -45,3 +45,27 @@
type: 2 type: 2
created_unix: 1688973000 created_unix: 1688973000
updated_unix: 1688973000 updated_unix: 1688973000
-
id: 5
title: project without default column
owner_id: 2
repo_id: 0
is_closed: false
creator_id: 2
board_type: 1
type: 2
created_unix: 1688973000
updated_unix: 1688973000
-
id: 6
title: project with multiple default columns
owner_id: 2
repo_id: 0
is_closed: false
creator_id: 2
board_type: 1
type: 2
created_unix: 1688973000
updated_unix: 1688973000

View File

@ -3,6 +3,7 @@
project_id: 1 project_id: 1
title: To Do title: To Do
creator_id: 2 creator_id: 2
default: true
created_unix: 1588117528 created_unix: 1588117528
updated_unix: 1588117528 updated_unix: 1588117528
@ -29,3 +30,48 @@
creator_id: 2 creator_id: 2
created_unix: 1588117528 created_unix: 1588117528
updated_unix: 1588117528 updated_unix: 1588117528
-
id: 5
project_id: 2
title: Backlog
creator_id: 2
default: true
created_unix: 1588117528
updated_unix: 1588117528
-
id: 6
project_id: 4
title: Backlog
creator_id: 2
default: true
created_unix: 1588117528
updated_unix: 1588117528
-
id: 7
project_id: 5
title: Done
creator_id: 2
default: false
created_unix: 1588117528
updated_unix: 1588117528
-
id: 8
project_id: 6
title: Backlog
creator_id: 2
default: true
created_unix: 1588117528
updated_unix: 1588117528
-
id: 9
project_id: 6
title: Uncategorized
creator_id: 2
default: true
created_unix: 1588117528
updated_unix: 1588117528

View File

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

View File

@ -0,0 +1,23 @@
-
id: 1
title: project without default column
owner_id: 2
repo_id: 0
is_closed: false
creator_id: 2
board_type: 1
type: 2
created_unix: 1688973000
updated_unix: 1688973000
-
id: 2
title: project with multiple default columns
owner_id: 2
repo_id: 0
is_closed: false
creator_id: 2
board_type: 1
type: 2
created_unix: 1688973000
updated_unix: 1688973000

View File

@ -0,0 +1,26 @@
-
id: 1
project_id: 1
title: Done
creator_id: 2
default: false
created_unix: 1588117528
updated_unix: 1588117528
-
id: 2
project_id: 2
title: Backlog
creator_id: 2
default: true
created_unix: 1588117528
updated_unix: 1588117528
-
id: 3
project_id: 2
title: Uncategorized
creator_id: 2
default: true
created_unix: 1588117528
updated_unix: 1588117528

View File

@ -568,6 +568,8 @@ var migrations = []Migration{
NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable), NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable),
// v291 -> v292 // v291 -> v292
NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment), NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment),
// v292 -> v293
NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
} }
// GetCurrentDBVersion returns the current db version // GetCurrentDBVersion returns the current db version

View File

@ -0,0 +1,85 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/setting"
"xorm.io/builder"
"xorm.io/xorm"
)
// CheckProjectColumnsConsistency ensures there is exactly one default board per project present
func CheckProjectColumnsConsistency(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
limit := setting.Database.IterateBufferSize
if limit <= 0 {
limit = 50
}
start := 0
for {
var projects []project.Project
if err := sess.SQL("SELECT DISTINCT `p`.`id`, `p`.`creator_id` FROM `project` `p` WHERE (SELECT COUNT(*) FROM `project_board` `pb` WHERE `pb`.`project_id` = `p`.`id` AND `pb`.`default` = ?) != 1", true).
Limit(limit, start).
Find(&projects); err != nil {
return err
}
if len(projects) == 0 {
break
}
start += len(projects)
for _, p := range projects {
var boards []project.Board
if err := sess.Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil {
return err
}
if len(boards) == 0 {
if _, err := sess.Insert(project.Board{
ProjectID: p.ID,
Default: true,
Title: "Uncategorized",
CreatorID: p.CreatorID,
}); err != nil {
return err
}
continue
}
var boardsToUpdate []int64
for id, b := range boards {
if id > 0 {
boardsToUpdate = append(boardsToUpdate, b.ID)
}
}
if _, err := sess.Where(builder.Eq{"project_id": p.ID}.And(builder.In("id", boardsToUpdate))).
Cols("`default`").Update(&project.Board{Default: false}); err != nil {
return err
}
}
if start%1000 == 0 {
if err := sess.Commit(); err != nil {
return err
}
if err := sess.Begin(); err != nil {
return err
}
}
}
return sess.Commit()
}

View File

@ -0,0 +1,44 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/models/project"
"github.com/stretchr/testify/assert"
)
func Test_CheckProjectColumnsConsistency(t *testing.T) {
// Prepare and load the testing database
x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board))
defer deferable()
if x == nil || t.Failed() {
return
}
assert.NoError(t, CheckProjectColumnsConsistency(x))
// check if default board was added
var defaultBoard project.Board
has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard)
assert.NoError(t, err)
assert.True(t, has)
assert.Equal(t, int64(1), defaultBoard.ProjectID)
assert.True(t, defaultBoard.Default)
// check if multiple defaults were removed
expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2)
assert.NoError(t, err)
assert.Equal(t, int64(2), expectDefaultBoard.ProjectID)
assert.True(t, expectDefaultBoard.Default)
expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3)
assert.NoError(t, err)
assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID)
assert.False(t, expectNonDefaultBoard.Default)
}

View File

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

View File

@ -0,0 +1,40 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestGetDefaultBoard(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5)
assert.NoError(t, err)
// check if default board was added
board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext)
assert.NoError(t, err)
assert.Equal(t, int64(5), board.ProjectID)
assert.Equal(t, "Uncategorized", board.Title)
projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
assert.NoError(t, err)
// check if multiple defaults were removed
board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext)
assert.NoError(t, err)
assert.Equal(t, int64(6), board.ProjectID)
assert.Equal(t, int64(8), board.ID)
board, err = GetBoard(db.DefaultContext, 9)
assert.NoError(t, err)
assert.Equal(t, int64(6), board.ProjectID)
assert.False(t, board.Default)
}

View File

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

View File

@ -1392,7 +1392,6 @@ projects.type.basic_kanban = "Basic Kanban"
projects.type.bug_triage = "Bug Triage" projects.type.bug_triage = "Bug Triage"
projects.template.desc = "Template" projects.template.desc = "Template"
projects.template.desc_helper = "Select a project template to get started" projects.template.desc_helper = "Select a project template to get started"
projects.type.uncategorized = Uncategorized
projects.column.edit = "Edit Column" projects.column.edit = "Edit Column"
projects.column.edit_title = "Name" projects.column.edit_title = "Name"
projects.column.new_title = "Name" projects.column.new_title = "Name"
@ -1400,10 +1399,8 @@ projects.column.new_submit = "Create Column"
projects.column.new = "New Column" projects.column.new = "New Column"
projects.column.set_default = "Set Default" projects.column.set_default = "Set Default"
projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls" projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls"
projects.column.unset_default = "Unset Default"
projects.column.unset_default_desc = "Unset this column as default"
projects.column.delete = "Delete Column" projects.column.delete = "Delete Column"
projects.column.deletion_desc = "Deleting a project column moves all related issues to 'Uncategorized'. Continue?" projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?"
projects.column.color = "Color" projects.column.color = "Color"
projects.open = Open projects.open = Open
projects.close = Close projects.close = Close

View File

@ -207,11 +207,7 @@ func ChangeProjectStatus(ctx *context.Context) {
id := ctx.ParamsInt64(":id") id := ctx.ParamsInt64(":id")
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil { if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", err)
} else {
ctx.ServerError("ChangeProjectStatusByRepoIDAndID", err)
}
return return
} }
ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects?state=" + url.QueryEscape(ctx.Params(":action"))) ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects?state=" + url.QueryEscape(ctx.Params(":action")))
@ -221,11 +217,7 @@ func ChangeProjectStatus(ctx *context.Context) {
func DeleteProject(ctx *context.Context) { func DeleteProject(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
if p.OwnerID != ctx.ContextUser.ID { if p.OwnerID != ctx.ContextUser.ID {
@ -254,11 +246,7 @@ func RenderEditProject(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
if p.OwnerID != ctx.ContextUser.ID { if p.OwnerID != ctx.ContextUser.ID {
@ -303,11 +291,7 @@ func EditProjectPost(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, projectID) p, err := project_model.GetProjectByID(ctx, projectID)
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
if p.OwnerID != ctx.ContextUser.ID { if p.OwnerID != ctx.ContextUser.ID {
@ -335,11 +319,7 @@ func EditProjectPost(ctx *context.Context) {
func ViewProject(ctx *context.Context) { func ViewProject(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
if project.OwnerID != ctx.ContextUser.ID { if project.OwnerID != ctx.ContextUser.ID {
@ -353,10 +333,6 @@ func ViewProject(ctx *context.Context) {
return return
} }
if boards[0].ID == 0 {
boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
}
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
if err != nil { if err != nil {
ctx.ServerError("LoadIssuesOfBoards", err) ctx.ServerError("LoadIssuesOfBoards", err)
@ -493,11 +469,7 @@ func DeleteProjectBoard(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
@ -534,11 +506,7 @@ func AddBoardToProjectPost(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
@ -566,11 +534,7 @@ func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return nil, nil return nil, nil
} }
@ -636,21 +600,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
ctx.JSONOK() ctx.JSONOK()
} }
// UnsetDefaultProjectBoard unset default board for uncategorized issues/pulls
func UnsetDefaultProjectBoard(ctx *context.Context) {
project, _ := CheckProjectBoardChangePermissions(ctx)
if ctx.Written() {
return
}
if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil {
ctx.ServerError("SetDefaultBoard", err)
return
}
ctx.JSONOK()
}
// MoveIssues moves or keeps issues in a column and sorts them inside that column // MoveIssues moves or keeps issues in a column and sorts them inside that column
func MoveIssues(ctx *context.Context) { func MoveIssues(ctx *context.Context) {
if ctx.Doer == nil { if ctx.Doer == nil {
@ -662,11 +611,7 @@ func MoveIssues(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
if project_model.IsErrProjectNotExist(err) { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
ctx.NotFound("ProjectNotExist", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return return
} }
if project.OwnerID != ctx.ContextUser.ID { if project.OwnerID != ctx.ContextUser.ID {
@ -674,29 +619,16 @@ func MoveIssues(ctx *context.Context) {
return return
} }
var board *project_model.Board board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
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 err != nil {
if project_model.IsErrProjectBoardNotExist(err) { ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err)
ctx.NotFound("ProjectBoardNotExist", nil)
} else {
ctx.ServerError("GetProjectBoard", err)
}
return return
} }
if board.ProjectID != project.ID { if board.ProjectID != project.ID {
ctx.NotFound("BoardNotInProject", nil) ctx.NotFound("BoardNotInProject", nil)
return return
} }
}
type movedIssuesForm struct { type movedIssuesForm struct {
Issues []struct { Issues []struct {
@ -718,11 +650,7 @@ func MoveIssues(ctx *context.Context) {
} }
movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
if err != nil { if err != nil {
if issues_model.IsErrIssueNotExist(err) { ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err)
ctx.NotFound("IssueNotExisting", nil)
} else {
ctx.ServerError("GetIssueByID", err)
}
return return
} }

View File

@ -315,10 +315,6 @@ func ViewProject(ctx *context.Context) {
return return
} }
if boards[0].ID == 0 {
boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
}
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
if err != nil { if err != nil {
ctx.ServerError("LoadIssuesOfBoards", err) ctx.ServerError("LoadIssuesOfBoards", err)
@ -583,21 +579,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
ctx.JSONOK() ctx.JSONOK()
} }
// UnSetDefaultProjectBoard unset default board for uncategorized issues/pulls
func UnSetDefaultProjectBoard(ctx *context.Context) {
project, _ := checkProjectBoardChangePermissions(ctx)
if ctx.Written() {
return
}
if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil {
ctx.ServerError("SetDefaultBoard", err)
return
}
ctx.JSONOK()
}
// MoveIssues moves or keeps issues in a column and sorts them inside that column // MoveIssues moves or keeps issues in a column and sorts them inside that column
func MoveIssues(ctx *context.Context) { func MoveIssues(ctx *context.Context) {
if ctx.Doer == nil { if ctx.Doer == nil {
@ -628,16 +609,7 @@ func MoveIssues(ctx *context.Context) {
return return
} }
var board *project_model.Board board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
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 err != nil {
if project_model.IsErrProjectBoardNotExist(err) { if project_model.IsErrProjectBoardNotExist(err) {
ctx.NotFound("ProjectBoardNotExist", nil) ctx.NotFound("ProjectBoardNotExist", nil)
@ -646,11 +618,11 @@ func MoveIssues(ctx *context.Context) {
} }
return return
} }
if board.ProjectID != project.ID { if board.ProjectID != project.ID {
ctx.NotFound("BoardNotInProject", nil) ctx.NotFound("BoardNotInProject", nil)
return return
} }
}
type movedIssuesForm struct { type movedIssuesForm struct {
Issues []struct { Issues []struct {

View File

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

View File

@ -74,7 +74,7 @@
</div> </div>
{{.Title}} {{.Title}}
</div> </div>
{{if and $canWriteProject (ne .ID 0)}} {{if $canWriteProject}}
<div class="ui dropdown jump item"> <div class="ui dropdown jump item">
<div class="tw-px-2"> <div class="tw-px-2">
{{svg "octicon-kebab-horizontal"}} {{svg "octicon-kebab-horizontal"}}
@ -93,22 +93,13 @@
{{svg "octicon-pin"}} {{svg "octicon-pin"}}
{{ctx.Locale.Tr "repo.projects.column.set_default"}} {{ctx.Locale.Tr "repo.projects.column.set_default"}}
</a> </a>
{{else}}
<a class="item show-modal button 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>
{{end}}
<a class="item show-modal button show-delete-project-column-modal" <a class="item show-modal button show-delete-project-column-modal"
data-modal="#delete-project-column-modal-{{.ID}}" data-modal="#delete-project-column-modal-{{.ID}}"
data-url="{{$.Link}}/{{.ID}}"> data-url="{{$.Link}}/{{.ID}}">
{{svg "octicon-trash"}} {{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.projects.column.delete"}} {{ctx.Locale.Tr "repo.projects.column.delete"}}
</a> </a>
{{end}}
<div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}"> <div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}">
<div class="header"> <div class="header">
@ -165,7 +156,7 @@
<div class="divider"></div> <div class="divider"></div>
<div class="ui cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}tw-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}"> <div class="ui cards{{if $canWriteProject}} tw-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
{{range (index $.IssuesMap .ID)}} {{range (index $.IssuesMap .ID)}}
<div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}"> <div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">
{{template "repo/issue/card" (dict "Issue" . "Page" $)}} {{template "repo/issue/card" (dict "Issue" . "Page" $)}}

View File

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