mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +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