mirror of
1
Fork 0

Rename project board -> column to make the UI less confusing (#30170)

This PR split the `Board` into two parts. One is the struct has been
renamed to `Column` and the second we have a `Template Type`.

But to make it easier to review, this PR will not change the database
schemas, they are just renames. The database schema changes could be in
future PRs.

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: yp05327 <576951401@qq.com>
(cherry picked from commit 98751108b11dc748cc99230ca0fc1acfdf2c8929)

Conflicts:
	docs/content/administration/config-cheat-sheet.en-us.md
	docs/content/index.en-us.md
	docs/content/installation/comparison.en-us.md
	docs/content/usage/permissions.en-us.md
	non existent files

	options/locale/locale_en-US.ini
	routers/web/web.go
	templates/repo/header.tmpl
	templates/repo/settings/options.tmpl
	trivial context conflicts
This commit is contained in:
Lunny Xiao 2024-05-27 16:59:54 +08:00 committed by Earl Warren
parent ec05ab1e3c
commit a7591f9738
No known key found for this signature in database
GPG Key ID: 0579CB2928A78A00
40 changed files with 720 additions and 770 deletions

View File

@ -30,7 +30,7 @@ type Statistic struct {
Mirror, Release, AuthSource, Webhook, Mirror, Release, AuthSource, Webhook,
Milestone, Label, HookTask, Milestone, Label, HookTask,
Team, UpdateTask, Project, Team, UpdateTask, Project,
ProjectBoard, Attachment, ProjectColumn, Attachment,
Branches, Tags, CommitStatus int64 Branches, Tags, CommitStatus int64
IssueByLabel []IssueByLabelCount IssueByLabel []IssueByLabelCount
IssueByRepository []IssueByRepositoryCount IssueByRepository []IssueByRepositoryCount
@ -115,6 +115,6 @@ func GetStatistic(ctx context.Context) (stats Statistic) {
stats.Counter.Team, _ = e.Count(new(organization.Team)) stats.Counter.Team, _ = e.Count(new(organization.Team))
stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment)) stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment))
stats.Counter.Project, _ = e.Count(new(project_model.Project)) stats.Counter.Project, _ = e.Count(new(project_model.Project))
stats.Counter.ProjectBoard, _ = e.Count(new(project_model.Board)) stats.Counter.ProjectColumn, _ = e.Count(new(project_model.Column))
return stats return stats
} }

View File

@ -100,8 +100,8 @@ const (
CommentTypeMergePull // 28 merge pull request CommentTypeMergePull // 28 merge pull request
CommentTypePullRequestPush // 29 push to PR head branch CommentTypePullRequestPush // 29 push to PR head branch
CommentTypeProject // 30 Project changed CommentTypeProject // 30 Project changed
CommentTypeProjectBoard // 31 Project board changed CommentTypeProjectColumn // 31 Project column changed
CommentTypeDismissReview // 32 Dismiss Review CommentTypeDismissReview // 32 Dismiss Review
@ -146,7 +146,7 @@ var commentStrings = []string{
"merge_pull", "merge_pull",
"pull_push", "pull_push",
"project", "project",
"project_board", "project_board", // FIXME: the name should be project_column
"dismiss_review", "dismiss_review",
"change_issue_ref", "change_issue_ref",
"pull_scheduled_merge", "pull_scheduled_merge",

View File

@ -37,22 +37,22 @@ func (issue *Issue) projectID(ctx context.Context) int64 {
return ip.ProjectID return ip.ProjectID
} }
// ProjectBoardID return project board id if issue was assigned to one // ProjectColumnID return project column id if issue was assigned to one
func (issue *Issue) ProjectBoardID(ctx context.Context) int64 { func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
var ip project_model.ProjectIssue var ip project_model.ProjectIssue
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
if err != nil || !has { if err != nil || !has {
return 0 return 0
} }
return ip.ProjectBoardID return ip.ProjectColumnID
} }
// LoadIssuesFromBoard load issues assigned to this board // LoadIssuesFromColumn load issues assigned to this column
func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) { func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueList, error) {
issueList, err := Issues(ctx, &IssuesOptions{ issueList, err := Issues(ctx, &IssuesOptions{
ProjectBoardID: b.ID, ProjectColumnID: b.ID,
ProjectID: b.ProjectID, ProjectID: b.ProjectID,
SortType: "project-column-sorting", SortType: "project-column-sorting",
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -60,9 +60,9 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList
if b.Default { if b.Default {
issues, err := Issues(ctx, &IssuesOptions{ issues, err := Issues(ctx, &IssuesOptions{
ProjectBoardID: db.NoConditionID, ProjectColumnID: db.NoConditionID,
ProjectID: b.ProjectID, ProjectID: b.ProjectID,
SortType: "project-column-sorting", SortType: "project-column-sorting",
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -77,11 +77,11 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList
return issueList, nil return issueList, nil
} }
// LoadIssuesFromBoardList load issues assigned to the boards // LoadIssuesFromColumnList load issues assigned to the columns
func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (map[int64]IssueList, error) { func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList) (map[int64]IssueList, error) {
issuesMap := make(map[int64]IssueList, len(bs)) issuesMap := make(map[int64]IssueList, len(bs))
for i := range bs { for i := range bs {
il, err := LoadIssuesFromBoard(ctx, bs[i]) il, err := LoadIssuesFromColumn(ctx, bs[i])
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -110,7 +110,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID) return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
} }
if newColumnID == 0 { if newColumnID == 0 {
newDefaultColumn, err := newProject.GetDefaultBoard(ctx) newDefaultColumn, err := newProject.GetDefaultColumn(ctx)
if err != nil { if err != nil {
return err return err
} }
@ -153,10 +153,10 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
} }
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
return db.Insert(ctx, &project_model.ProjectIssue{ return db.Insert(ctx, &project_model.ProjectIssue{
IssueID: issue.ID, IssueID: issue.ID,
ProjectID: newProjectID, ProjectID: newProjectID,
ProjectBoardID: newColumnID, ProjectColumnID: newColumnID,
Sorting: newSorting, Sorting: newSorting,
}) })
}) })
} }

View File

@ -33,7 +33,7 @@ type IssuesOptions struct { //nolint
SubscriberID int64 SubscriberID int64
MilestoneIDs []int64 MilestoneIDs []int64
ProjectID int64 ProjectID int64
ProjectBoardID int64 ProjectColumnID int64
IsClosed optional.Option[bool] IsClosed optional.Option[bool]
IsPull optional.Option[bool] IsPull optional.Option[bool]
LabelIDs []int64 LabelIDs []int64
@ -169,12 +169,12 @@ func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sessio
return sess return sess
} }
func applyProjectBoardCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
// opts.ProjectBoardID == 0 means all project boards, // opts.ProjectColumnID == 0 means all project columns,
// do not need to apply any condition // do not need to apply any condition
if opts.ProjectBoardID > 0 { if opts.ProjectColumnID > 0 {
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID})) sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectColumnID}))
} else if opts.ProjectBoardID == db.NoConditionID { } else if opts.ProjectColumnID == db.NoConditionID {
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}))
} }
return sess return sess
@ -246,7 +246,7 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
applyProjectCondition(sess, opts) applyProjectCondition(sess, opts)
applyProjectBoardCondition(sess, opts) applyProjectColumnCondition(sess, opts)
if opts.IsPull.Has() { if opts.IsPull.Has() {
sess.And("issue.is_pull=?", opts.IsPull.Value()) sess.And("issue.is_pull=?", opts.IsPull.Value())

View File

@ -15,7 +15,7 @@ import (
func Test_CheckProjectColumnsConsistency(t *testing.T) { func Test_CheckProjectColumnsConsistency(t *testing.T) {
// Prepare and load the testing database // Prepare and load the testing database
x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board)) x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Column))
defer deferable() defer deferable()
if x == nil || t.Failed() { if x == nil || t.Failed() {
return return
@ -23,22 +23,22 @@ func Test_CheckProjectColumnsConsistency(t *testing.T) {
assert.NoError(t, CheckProjectColumnsConsistency(x)) assert.NoError(t, CheckProjectColumnsConsistency(x))
// check if default board was added // check if default column was added
var defaultBoard project.Board var defaultColumn project.Column
has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard) has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultColumn)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, has) assert.True(t, has)
assert.Equal(t, int64(1), defaultBoard.ProjectID) assert.Equal(t, int64(1), defaultColumn.ProjectID)
assert.True(t, defaultBoard.Default) assert.True(t, defaultColumn.Default)
// check if multiple defaults, previous were removed and last will be kept // check if multiple defaults, previous were removed and last will be kept
expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2) expectDefaultColumn, err := project.GetColumn(db.DefaultContext, 2)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, int64(2), expectDefaultBoard.ProjectID) assert.Equal(t, int64(2), expectDefaultColumn.ProjectID)
assert.False(t, expectDefaultBoard.Default) assert.False(t, expectDefaultColumn.Default)
expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3) expectNonDefaultColumn, err := project.GetColumn(db.DefaultContext, 3)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID) assert.Equal(t, int64(2), expectNonDefaultColumn.ProjectID)
assert.True(t, expectNonDefaultBoard.Default) assert.True(t, expectNonDefaultColumn.Default)
} }

View File

@ -1,389 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
import (
"context"
"errors"
"fmt"
"regexp"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
type (
// BoardType is used to represent a project board type
BoardType uint8
// CardType is used to represent a project board card type
CardType uint8
// BoardList is a list of all project boards in a repository
BoardList []*Board
)
const (
// BoardTypeNone is a project board type that has no predefined columns
BoardTypeNone BoardType = iota
// BoardTypeBasicKanban is a project board type that has basic predefined columns
BoardTypeBasicKanban
// BoardTypeBugTriage is a project board type that has predefined columns suited to hunting down bugs
BoardTypeBugTriage
)
const (
// CardTypeTextOnly is a project board card type that is text only
CardTypeTextOnly CardType = iota
// CardTypeImagesAndText is a project board card type that has images and text
CardTypeImagesAndText
)
// BoardColorPattern is a regexp witch can validate BoardColor
var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
// Board is used to represent boards on a project
type Board struct {
ID int64 `xorm:"pk autoincr"`
Title string
Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board
Sorting int8 `xorm:"NOT NULL DEFAULT 0"`
Color string `xorm:"VARCHAR(7)"`
ProjectID int64 `xorm:"INDEX NOT NULL"`
CreatorID int64 `xorm:"NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
// TableName return the real table name
func (Board) TableName() string {
return "project_board"
}
// NumIssues return counter of all issues assigned to the board
func (b *Board) NumIssues(ctx context.Context) int {
c, err := db.GetEngine(ctx).Table("project_issue").
Where("project_id=?", b.ProjectID).
And("project_board_id=?", b.ID).
GroupBy("issue_id").
Cols("issue_id").
Count()
if err != nil {
return 0
}
return int(c)
}
func (b *Board) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
issues := make([]*ProjectIssue, 0, 5)
if err := db.GetEngine(ctx).Where("project_id=?", b.ProjectID).
And("project_board_id=?", b.ID).
OrderBy("sorting, id").
Find(&issues); err != nil {
return nil, err
}
return issues, nil
}
func init() {
db.RegisterModel(new(Board))
}
// IsBoardTypeValid checks if the project board type is valid
func IsBoardTypeValid(p BoardType) bool {
switch p {
case BoardTypeNone, BoardTypeBasicKanban, BoardTypeBugTriage:
return true
default:
return false
}
}
// IsCardTypeValid checks if the project board card type is valid
func IsCardTypeValid(p CardType) bool {
switch p {
case CardTypeTextOnly, CardTypeImagesAndText:
return true
default:
return false
}
}
func createBoardsForProjectsType(ctx context.Context, project *Project) error {
var items []string
switch project.BoardType {
case BoardTypeBugTriage:
items = setting.Project.ProjectBoardBugTriageType
case BoardTypeBasicKanban:
items = setting.Project.ProjectBoardBasicKanbanType
case BoardTypeNone:
fallthrough
default:
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
}
boards := make([]Board, 0, len(items))
for _, v := range items {
boards = append(boards, Board{
CreatedUnix: timeutil.TimeStampNow(),
CreatorID: project.CreatorID,
Title: v,
ProjectID: project.ID,
})
}
return db.Insert(ctx, boards)
}
// maxProjectColumns max columns allowed in a project, this should not bigger than 127
// because sorting is int8 in database
const maxProjectColumns = 20
// NewBoard adds a new project board to a given project
func NewBoard(ctx context.Context, board *Board) error {
if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
return fmt.Errorf("bad color code: %s", board.Color)
}
res := struct {
MaxSorting int64
ColumnCount int64
}{}
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board").
Where("project_id=?", board.ProjectID).Get(&res); err != nil {
return err
}
if res.ColumnCount >= maxProjectColumns {
return fmt.Errorf("NewBoard: maximum number of columns reached")
}
board.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
_, err := db.GetEngine(ctx).Insert(board)
return err
}
// DeleteBoardByID removes all issues references to the project board.
func DeleteBoardByID(ctx context.Context, boardID int64) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
if err := deleteBoardByID(ctx, boardID); err != nil {
return err
}
return committer.Commit()
}
func deleteBoardByID(ctx context.Context, boardID int64) error {
board, err := GetBoard(ctx, boardID)
if err != nil {
if IsErrProjectBoardNotExist(err) {
return nil
}
return err
}
if board.Default {
return fmt.Errorf("deleteBoardByID: cannot delete default board")
}
// move all issues to the default column
project, err := GetProjectByID(ctx, board.ProjectID)
if err != nil {
return err
}
defaultColumn, err := project.GetDefaultBoard(ctx)
if err != nil {
return err
}
if err = board.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
return err
}
if _, err := db.GetEngine(ctx).ID(board.ID).NoAutoCondition().Delete(board); err != nil {
return err
}
return nil
}
func deleteBoardByProjectID(ctx context.Context, projectID int64) error {
_, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Board{})
return err
}
// GetBoard fetches the current board of a project
func GetBoard(ctx context.Context, boardID int64) (*Board, error) {
board := new(Board)
has, err := db.GetEngine(ctx).ID(boardID).Get(board)
if err != nil {
return nil, err
} else if !has {
return nil, ErrProjectBoardNotExist{BoardID: boardID}
}
return board, nil
}
// UpdateBoard updates a project board
func UpdateBoard(ctx context.Context, board *Board) error {
var fieldToUpdate []string
if board.Sorting != 0 {
fieldToUpdate = append(fieldToUpdate, "sorting")
}
if board.Title != "" {
fieldToUpdate = append(fieldToUpdate, "title")
}
if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
return fmt.Errorf("bad color code: %s", board.Color)
}
fieldToUpdate = append(fieldToUpdate, "color")
_, err := db.GetEngine(ctx).ID(board.ID).Cols(fieldToUpdate...).Update(board)
return err
}
// GetBoards fetches all boards related to a project
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
boards := make([]*Board, 0, 5)
if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&boards); err != nil {
return nil, err
}
return boards, nil
}
// GetDefaultBoard return default board and ensure only one exists
func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) {
var board Board
has, err := db.GetEngine(ctx).
Where("project_id=? AND `default` = ?", p.ID, true).
Desc("id").Get(&board)
if err != nil {
return nil, err
}
if has {
return &board, nil
}
// create a default board if none is found
board = Board{
ProjectID: p.ID,
Default: true,
Title: "Uncategorized",
CreatorID: p.CreatorID,
}
if _, err := db.GetEngine(ctx).Insert(&board); err != nil {
return nil, err
}
return &board, nil
}
// SetDefaultBoard represents a board for issues not assigned to one
func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if _, err := GetBoard(ctx, boardID); err != nil {
return err
}
if _, err := db.GetEngine(ctx).Where(builder.Eq{
"project_id": projectID,
"`default`": true,
}).Cols("`default`").Update(&Board{Default: false}); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(boardID).
Where(builder.Eq{"project_id": projectID}).
Cols("`default`").Update(&Board{Default: true})
return err
})
}
// UpdateBoardSorting update project board sorting
func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for i := range bs {
if _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols(
"sorting",
).Update(bs[i]); err != nil {
return err
}
}
return nil
})
}
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (BoardList, error) {
columns := make([]*Board, 0, 5)
if err := db.GetEngine(ctx).
Where("project_id =?", projectID).
In("id", columnsIDs).
OrderBy("sorting").Find(&columns); err != nil {
return nil, err
}
return columns, nil
}
// MoveColumnsOnProject sorts columns in a project
func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
sess := db.GetEngine(ctx)
columnIDs := util.ValuesOfMap(sortedColumnIDs)
movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs)
if err != nil {
return err
}
if len(movedColumns) != len(sortedColumnIDs) {
return errors.New("some columns do not exist")
}
for _, column := range movedColumns {
if column.ProjectID != project.ID {
return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID)
}
}
for sorting, columnID := range sortedColumnIDs {
if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil {
return err
}
}
return nil
})
}

359
models/project/column.go Normal file
View File

@ -0,0 +1,359 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
import (
"context"
"errors"
"fmt"
"regexp"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
type (
// CardType is used to represent a project column card type
CardType uint8
// ColumnList is a list of all project columns in a repository
ColumnList []*Column
)
const (
// CardTypeTextOnly is a project column card type that is text only
CardTypeTextOnly CardType = iota
// CardTypeImagesAndText is a project column card type that has images and text
CardTypeImagesAndText
)
// ColumnColorPattern is a regexp witch can validate ColumnColor
var ColumnColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
// Column is used to represent column on a project
type Column struct {
ID int64 `xorm:"pk autoincr"`
Title string
Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific column will be assigned to this column
Sorting int8 `xorm:"NOT NULL DEFAULT 0"`
Color string `xorm:"VARCHAR(7)"`
ProjectID int64 `xorm:"INDEX NOT NULL"`
CreatorID int64 `xorm:"NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
// TableName return the real table name
func (Column) TableName() string {
return "project_board" // TODO: the legacy table name should be project_column
}
// NumIssues return counter of all issues assigned to the column
func (c *Column) NumIssues(ctx context.Context) int {
total, err := db.GetEngine(ctx).Table("project_issue").
Where("project_id=?", c.ProjectID).
And("project_board_id=?", c.ID).
GroupBy("issue_id").
Cols("issue_id").
Count()
if err != nil {
return 0
}
return int(total)
}
func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
issues := make([]*ProjectIssue, 0, 5)
if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
And("project_board_id=?", c.ID).
OrderBy("sorting, id").
Find(&issues); err != nil {
return nil, err
}
return issues, nil
}
func init() {
db.RegisterModel(new(Column))
}
// IsCardTypeValid checks if the project column card type is valid
func IsCardTypeValid(p CardType) bool {
switch p {
case CardTypeTextOnly, CardTypeImagesAndText:
return true
default:
return false
}
}
func createDefaultColumnsForProject(ctx context.Context, project *Project) error {
var items []string
switch project.TemplateType {
case TemplateTypeBugTriage:
items = setting.Project.ProjectBoardBugTriageType
case TemplateTypeBasicKanban:
items = setting.Project.ProjectBoardBasicKanbanType
case TemplateTypeNone:
fallthrough
default:
return nil
}
return db.WithTx(ctx, func(ctx context.Context) error {
column := Column{
CreatedUnix: timeutil.TimeStampNow(),
CreatorID: project.CreatorID,
Title: "Backlog",
ProjectID: project.ID,
Default: true,
}
if err := db.Insert(ctx, column); err != nil {
return err
}
if len(items) == 0 {
return nil
}
columns := make([]Column, 0, len(items))
for _, v := range items {
columns = append(columns, Column{
CreatedUnix: timeutil.TimeStampNow(),
CreatorID: project.CreatorID,
Title: v,
ProjectID: project.ID,
})
}
return db.Insert(ctx, columns)
})
}
// maxProjectColumns max columns allowed in a project, this should not bigger than 127
// because sorting is int8 in database
const maxProjectColumns = 20
// NewColumn adds a new project column to a given project
func NewColumn(ctx context.Context, column *Column) error {
if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) {
return fmt.Errorf("bad color code: %s", column.Color)
}
res := struct {
MaxSorting int64
ColumnCount int64
}{}
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board").
Where("project_id=?", column.ProjectID).Get(&res); err != nil {
return err
}
if res.ColumnCount >= maxProjectColumns {
return fmt.Errorf("NewBoard: maximum number of columns reached")
}
column.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
_, err := db.GetEngine(ctx).Insert(column)
return err
}
// DeleteColumnByID removes all issues references to the project column.
func DeleteColumnByID(ctx context.Context, columnID int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
return deleteColumnByID(ctx, columnID)
})
}
func deleteColumnByID(ctx context.Context, columnID int64) error {
column, err := GetColumn(ctx, columnID)
if err != nil {
if IsErrProjectColumnNotExist(err) {
return nil
}
return err
}
if column.Default {
return fmt.Errorf("deleteColumnByID: cannot delete default column")
}
// move all issues to the default column
project, err := GetProjectByID(ctx, column.ProjectID)
if err != nil {
return err
}
defaultColumn, err := project.GetDefaultColumn(ctx)
if err != nil {
return err
}
if err = column.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
return err
}
if _, err := db.GetEngine(ctx).ID(column.ID).NoAutoCondition().Delete(column); err != nil {
return err
}
return nil
}
func deleteColumnByProjectID(ctx context.Context, projectID int64) error {
_, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Column{})
return err
}
// GetColumn fetches the current column of a project
func GetColumn(ctx context.Context, columnID int64) (*Column, error) {
column := new(Column)
has, err := db.GetEngine(ctx).ID(columnID).Get(column)
if err != nil {
return nil, err
} else if !has {
return nil, ErrProjectColumnNotExist{ColumnID: columnID}
}
return column, nil
}
// UpdateColumn updates a project column
func UpdateColumn(ctx context.Context, column *Column) error {
var fieldToUpdate []string
if column.Sorting != 0 {
fieldToUpdate = append(fieldToUpdate, "sorting")
}
if column.Title != "" {
fieldToUpdate = append(fieldToUpdate, "title")
}
if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) {
return fmt.Errorf("bad color code: %s", column.Color)
}
fieldToUpdate = append(fieldToUpdate, "color")
_, err := db.GetEngine(ctx).ID(column.ID).Cols(fieldToUpdate...).Update(column)
return err
}
// GetColumns fetches all columns related to a project
func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
columns := make([]*Column, 0, 5)
if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&columns); err != nil {
return nil, err
}
return columns, nil
}
// GetDefaultColumn return default column and ensure only one exists
func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
var column Column
has, err := db.GetEngine(ctx).
Where("project_id=? AND `default` = ?", p.ID, true).
Desc("id").Get(&column)
if err != nil {
return nil, err
}
if has {
return &column, nil
}
// create a default column if none is found
column = Column{
ProjectID: p.ID,
Default: true,
Title: "Uncategorized",
CreatorID: p.CreatorID,
}
if _, err := db.GetEngine(ctx).Insert(&column); err != nil {
return nil, err
}
return &column, nil
}
// SetDefaultColumn represents a column for issues not assigned to one
func SetDefaultColumn(ctx context.Context, projectID, columnID int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if _, err := GetColumn(ctx, columnID); err != nil {
return err
}
if _, err := db.GetEngine(ctx).Where(builder.Eq{
"project_id": projectID,
"`default`": true,
}).Cols("`default`").Update(&Column{Default: false}); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(columnID).
Where(builder.Eq{"project_id": projectID}).
Cols("`default`").Update(&Column{Default: true})
return err
})
}
// UpdateColumnSorting update project column sorting
func UpdateColumnSorting(ctx context.Context, cl ColumnList) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for i := range cl {
if _, err := db.GetEngine(ctx).ID(cl[i].ID).Cols(
"sorting",
).Update(cl[i]); err != nil {
return err
}
}
return nil
})
}
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
columns := make([]*Column, 0, 5)
if err := db.GetEngine(ctx).
Where("project_id =?", projectID).
In("id", columnsIDs).
OrderBy("sorting").Find(&columns); err != nil {
return nil, err
}
return columns, nil
}
// MoveColumnsOnProject sorts columns in a project
func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
sess := db.GetEngine(ctx)
columnIDs := util.ValuesOfMap(sortedColumnIDs)
movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs)
if err != nil {
return err
}
if len(movedColumns) != len(sortedColumnIDs) {
return errors.New("some columns do not exist")
}
for _, column := range movedColumns {
if column.ProjectID != project.ID {
return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID)
}
}
for sorting, columnID := range sortedColumnIDs {
if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil {
return err
}
}
return nil
})
}

View File

@ -14,48 +14,48 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestGetDefaultBoard(t *testing.T) { func TestGetDefaultColumn(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5) projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5)
assert.NoError(t, err) assert.NoError(t, err)
// check if default board was added // check if default column was added
board, err := projectWithoutDefault.GetDefaultBoard(db.DefaultContext) column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, int64(5), board.ProjectID) assert.Equal(t, int64(5), column.ProjectID)
assert.Equal(t, "Uncategorized", board.Title) assert.Equal(t, "Uncategorized", column.Title)
projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6) projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
assert.NoError(t, err) assert.NoError(t, err)
// check if multiple defaults were removed // check if multiple defaults were removed
board, err = projectWithMultipleDefaults.GetDefaultBoard(db.DefaultContext) column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, int64(6), board.ProjectID) assert.Equal(t, int64(6), column.ProjectID)
assert.Equal(t, int64(9), board.ID) assert.Equal(t, int64(9), column.ID)
// set 8 as default board // set 8 as default column
assert.NoError(t, SetDefaultBoard(db.DefaultContext, board.ProjectID, 8)) assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8))
// then 9 will become a non-default board // then 9 will become a non-default column
board, err = GetBoard(db.DefaultContext, 9) column, err = GetColumn(db.DefaultContext, 9)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, int64(6), board.ProjectID) assert.Equal(t, int64(6), column.ProjectID)
assert.False(t, board.Default) assert.False(t, column.Default)
} }
func Test_moveIssuesToAnotherColumn(t *testing.T) { func Test_moveIssuesToAnotherColumn(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
column1 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 1, ProjectID: 1}) column1 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1, ProjectID: 1})
issues, err := column1.GetIssues(db.DefaultContext) issues, err := column1.GetIssues(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, issues, 1) assert.Len(t, issues, 1)
assert.EqualValues(t, 1, issues[0].ID) assert.EqualValues(t, 1, issues[0].ID)
column2 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 2, ProjectID: 1}) column2 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 2, ProjectID: 1})
issues, err = column2.GetIssues(db.DefaultContext) issues, err = column2.GetIssues(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, issues, 1) assert.Len(t, issues, 1)
@ -81,7 +81,7 @@ func Test_MoveColumnsOnProject(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1}) project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
columns, err := project1.GetBoards(db.DefaultContext) columns, err := project1.GetColumns(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, columns, 3) assert.Len(t, columns, 3)
assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work
@ -95,7 +95,7 @@ func Test_MoveColumnsOnProject(t *testing.T) {
}) })
assert.NoError(t, err) assert.NoError(t, err)
columnsAfter, err := project1.GetBoards(db.DefaultContext) columnsAfter, err := project1.GetColumns(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, columnsAfter, 3) assert.Len(t, columnsAfter, 3)
assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID) assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
@ -103,23 +103,23 @@ func Test_MoveColumnsOnProject(t *testing.T) {
assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID) assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID)
} }
func Test_NewBoard(t *testing.T) { func Test_NewColumn(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1}) project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
columns, err := project1.GetBoards(db.DefaultContext) columns, err := project1.GetColumns(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, columns, 3) assert.Len(t, columns, 3)
for i := 0; i < maxProjectColumns-3; i++ { for i := 0; i < maxProjectColumns-3; i++ {
err := NewBoard(db.DefaultContext, &Board{ err := NewColumn(db.DefaultContext, &Column{
Title: fmt.Sprintf("board-%d", i+4), Title: fmt.Sprintf("column-%d", i+4),
ProjectID: project1.ID, ProjectID: project1.ID,
}) })
assert.NoError(t, err) assert.NoError(t, err)
} }
err = NewBoard(db.DefaultContext, &Board{ err = NewColumn(db.DefaultContext, &Column{
Title: "board-21", Title: "column-21",
ProjectID: project1.ID, ProjectID: project1.ID,
}) })
assert.Error(t, err) assert.Error(t, err)

View File

@ -18,10 +18,10 @@ type ProjectIssue struct { //revive:disable-line:exported
IssueID int64 `xorm:"INDEX"` IssueID int64 `xorm:"INDEX"`
ProjectID int64 `xorm:"INDEX"` ProjectID int64 `xorm:"INDEX"`
// ProjectBoardID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors. // ProjectColumnID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors.
ProjectBoardID int64 `xorm:"INDEX"` ProjectColumnID int64 `xorm:"'project_board_id' INDEX"`
// the sorting order on the board // the sorting order on the column
Sorting int64 `xorm:"NOT NULL DEFAULT 0"` Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
} }
@ -76,13 +76,13 @@ func (p *Project) NumOpenIssues(ctx context.Context) int {
return int(c) return int(c)
} }
// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column // MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error { func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueIDs map[int64]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error { return db.WithTx(ctx, func(ctx context.Context) error {
sess := db.GetEngine(ctx) sess := db.GetEngine(ctx)
issueIDs := util.ValuesOfMap(sortedIssueIDs) issueIDs := util.ValuesOfMap(sortedIssueIDs)
count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count() count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", column.ProjectID).In("issue_id", issueIDs).Count()
if err != nil { if err != nil {
return err return err
} }
@ -91,7 +91,7 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
} }
for sorting, issueID := range sortedIssueIDs { for sorting, issueID := range sortedIssueIDs {
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID) _, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
if err != nil { if err != nil {
return err return err
} }
@ -100,12 +100,12 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
}) })
} }
func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) error { func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
if b.ProjectID != newColumn.ProjectID { if c.ProjectID != newColumn.ProjectID {
return fmt.Errorf("columns have to be in the same project") return fmt.Errorf("columns have to be in the same project")
} }
if b.ID == newColumn.ID { if c.ID == newColumn.ID {
return nil return nil
} }
@ -121,7 +121,7 @@ func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board)
return err return err
} }
issues, err := b.GetIssues(ctx) issues, err := c.GetIssues(ctx)
if err != nil { if err != nil {
return err return err
} }
@ -132,7 +132,7 @@ func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board)
nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
return db.WithTx(ctx, func(ctx context.Context) error { return db.WithTx(ctx, func(ctx context.Context) error {
for i, issue := range issues { for i, issue := range issues {
issue.ProjectBoardID = newColumn.ID issue.ProjectColumnID = newColumn.ID
issue.Sorting = nextSorting + int64(i) issue.Sorting = nextSorting + int64(i)
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil { if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil {
return err return err

View File

@ -21,13 +21,7 @@ import (
) )
type ( type (
// BoardConfig is used to identify the type of board that is being created // CardConfig is used to identify the type of column card that is being used
BoardConfig struct {
BoardType BoardType
Translation string
}
// CardConfig is used to identify the type of board card that is being used
CardConfig struct { CardConfig struct {
CardType CardType CardType CardType
Translation string Translation string
@ -38,7 +32,7 @@ type (
) )
const ( const (
// TypeIndividual is a type of project board that is owned by an individual // TypeIndividual is a type of project column that is owned by an individual
TypeIndividual Type = iota + 1 TypeIndividual Type = iota + 1
// TypeRepository is a project that is tied to a repository // TypeRepository is a project that is tied to a repository
@ -68,39 +62,39 @@ func (err ErrProjectNotExist) Unwrap() error {
return util.ErrNotExist return util.ErrNotExist
} }
// ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error. // ErrProjectColumnNotExist represents a "ErrProjectColumnNotExist" kind of error.
type ErrProjectBoardNotExist struct { type ErrProjectColumnNotExist struct {
BoardID int64 ColumnID int64
} }
// IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist // IsErrProjectColumnNotExist checks if an error is a ErrProjectColumnNotExist
func IsErrProjectBoardNotExist(err error) bool { func IsErrProjectColumnNotExist(err error) bool {
_, ok := err.(ErrProjectBoardNotExist) _, ok := err.(ErrProjectColumnNotExist)
return ok return ok
} }
func (err ErrProjectBoardNotExist) Error() string { func (err ErrProjectColumnNotExist) Error() string {
return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID) return fmt.Sprintf("project column does not exist [id: %d]", err.ColumnID)
} }
func (err ErrProjectBoardNotExist) Unwrap() error { func (err ErrProjectColumnNotExist) Unwrap() error {
return util.ErrNotExist return util.ErrNotExist
} }
// Project represents a project board // Project represents a project
type Project struct { type Project struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
Title string `xorm:"INDEX NOT NULL"` Title string `xorm:"INDEX NOT NULL"`
Description string `xorm:"TEXT"` Description string `xorm:"TEXT"`
OwnerID int64 `xorm:"INDEX"` OwnerID int64 `xorm:"INDEX"`
Owner *user_model.User `xorm:"-"` Owner *user_model.User `xorm:"-"`
RepoID int64 `xorm:"INDEX"` RepoID int64 `xorm:"INDEX"`
Repo *repo_model.Repository `xorm:"-"` Repo *repo_model.Repository `xorm:"-"`
CreatorID int64 `xorm:"NOT NULL"` CreatorID int64 `xorm:"NOT NULL"`
IsClosed bool `xorm:"INDEX"` IsClosed bool `xorm:"INDEX"`
BoardType BoardType TemplateType TemplateType `xorm:"'board_type'"` // TODO: rename the column to template_type
CardType CardType CardType CardType
Type Type Type Type
RenderedContent template.HTML `xorm:"-"` RenderedContent template.HTML `xorm:"-"`
@ -172,16 +166,7 @@ func init() {
db.RegisterModel(new(Project)) db.RegisterModel(new(Project))
} }
// GetBoardConfig retrieves the types of configurations project boards could have // GetCardConfig retrieves the types of configurations project column cards could have
func GetBoardConfig() []BoardConfig {
return []BoardConfig{
{BoardTypeNone, "repo.projects.type.none"},
{BoardTypeBasicKanban, "repo.projects.type.basic_kanban"},
{BoardTypeBugTriage, "repo.projects.type.bug_triage"},
}
}
// GetCardConfig retrieves the types of configurations project board cards could have
func GetCardConfig() []CardConfig { func GetCardConfig() []CardConfig {
return []CardConfig{ return []CardConfig{
{CardTypeTextOnly, "repo.projects.card_type.text_only"}, {CardTypeTextOnly, "repo.projects.card_type.text_only"},
@ -251,8 +236,8 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy {
// NewProject creates a new Project // NewProject creates a new Project
func NewProject(ctx context.Context, p *Project) error { func NewProject(ctx context.Context, p *Project) error {
if !IsBoardTypeValid(p.BoardType) { if !IsTemplateTypeValid(p.TemplateType) {
p.BoardType = BoardTypeNone p.TemplateType = TemplateTypeNone
} }
if !IsCardTypeValid(p.CardType) { if !IsCardTypeValid(p.CardType) {
@ -263,27 +248,19 @@ func NewProject(ctx context.Context, p *Project) error {
return util.NewInvalidArgumentErrorf("project type is not valid") return util.NewInvalidArgumentErrorf("project type is not valid")
} }
ctx, committer, err := db.TxContext(ctx) return db.WithTx(ctx, func(ctx context.Context) error {
if err != nil { if err := db.Insert(ctx, p); err != nil {
return err
}
defer committer.Close()
if err := db.Insert(ctx, p); err != nil {
return err
}
if p.RepoID > 0 {
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
return err return err
} }
}
if err := createBoardsForProjectsType(ctx, p); err != nil { if p.RepoID > 0 {
return err if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
} return err
}
}
return committer.Commit() return createDefaultColumnsForProject(ctx, p)
})
} }
// GetProjectByID returns the projects in a repository // GetProjectByID returns the projects in a repository
@ -417,7 +394,7 @@ func DeleteProjectByID(ctx context.Context, id int64) error {
return err return err
} }
if err := deleteBoardByProjectID(ctx, id); err != nil { if err := deleteColumnByProjectID(ctx, id); err != nil {
return err return err
} }

View File

@ -51,13 +51,13 @@ func TestProject(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
project := &Project{ project := &Project{
Type: TypeRepository, Type: TypeRepository,
BoardType: BoardTypeBasicKanban, TemplateType: TemplateTypeBasicKanban,
CardType: CardTypeTextOnly, CardType: CardTypeTextOnly,
Title: "New Project", Title: "New Project",
RepoID: 1, RepoID: 1,
CreatedUnix: timeutil.TimeStampNow(), CreatedUnix: timeutil.TimeStampNow(),
CreatorID: 2, CreatorID: 2,
} }
assert.NoError(t, NewProject(db.DefaultContext, project)) assert.NoError(t, NewProject(db.DefaultContext, project))

View File

@ -0,0 +1,45 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
type (
// TemplateType is used to represent a project template type
TemplateType uint8
// TemplateConfig is used to identify the template type of project that is being created
TemplateConfig struct {
TemplateType TemplateType
Translation string
}
)
const (
// TemplateTypeNone is a project template type that has no predefined columns
TemplateTypeNone TemplateType = iota
// TemplateTypeBasicKanban is a project template type that has basic predefined columns
TemplateTypeBasicKanban
// TemplateTypeBugTriage is a project template type that has predefined columns suited to hunting down bugs
TemplateTypeBugTriage
)
// GetTemplateConfigs retrieves the template configs of configurations project columns could have
func GetTemplateConfigs() []TemplateConfig {
return []TemplateConfig{
{TemplateTypeNone, "repo.projects.type.none"},
{TemplateTypeBasicKanban, "repo.projects.type.basic_kanban"},
{TemplateTypeBugTriage, "repo.projects.type.bug_triage"},
}
}
// IsTemplateTypeValid checks if the project template type is valid
func IsTemplateTypeValid(p TemplateType) bool {
switch p {
case TemplateTypeNone, TemplateTypeBasicKanban, TemplateTypeBugTriage:
return true
default:
return false
}
}

View File

@ -28,7 +28,7 @@ const (
TypeWiki // 5 Wiki TypeWiki // 5 Wiki
TypeExternalWiki // 6 ExternalWiki TypeExternalWiki // 6 ExternalWiki
TypeExternalTracker // 7 ExternalTracker TypeExternalTracker // 7 ExternalTracker
TypeProjects // 8 Kanban board TypeProjects // 8 Projects
TypePackages // 9 Packages TypePackages // 9 Packages
TypeActions // 10 Actions TypeActions // 10 Actions
) )

View File

@ -230,8 +230,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
if options.ProjectID.Has() { if options.ProjectID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id")) queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id"))
} }
if options.ProjectBoardID.Has() { if options.ProjectColumnID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id")) queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id"))
} }
if options.PosterID.Has() { if options.PosterID.Has() {

View File

@ -61,7 +61,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
ReviewedID: convertID(options.ReviewedID), ReviewedID: convertID(options.ReviewedID),
SubscriberID: convertID(options.SubscriberID), SubscriberID: convertID(options.SubscriberID),
ProjectID: convertID(options.ProjectID), ProjectID: convertID(options.ProjectID),
ProjectBoardID: convertID(options.ProjectBoardID), ProjectColumnID: convertID(options.ProjectColumnID),
IsClosed: options.IsClosed, IsClosed: options.IsClosed,
IsPull: options.IsPull, IsPull: options.IsPull,
IncludedLabelNames: nil, IncludedLabelNames: nil,

View File

@ -50,7 +50,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
} }
searchOpt.ProjectID = convertID(opts.ProjectID) searchOpt.ProjectID = convertID(opts.ProjectID)
searchOpt.ProjectBoardID = convertID(opts.ProjectBoardID) searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
searchOpt.PosterID = convertID(opts.PosterID) searchOpt.PosterID = convertID(opts.PosterID)
searchOpt.AssigneeID = convertID(opts.AssigneeID) searchOpt.AssigneeID = convertID(opts.AssigneeID)
searchOpt.MentionID = convertID(opts.MentionedID) searchOpt.MentionID = convertID(opts.MentionedID)

View File

@ -197,8 +197,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
if options.ProjectID.Has() { if options.ProjectID.Has() {
query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value())) query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value()))
} }
if options.ProjectBoardID.Has() { if options.ProjectColumnID.Has() {
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value())) query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value()))
} }
if options.PosterID.Has() { if options.PosterID.Has() {

View File

@ -369,13 +369,13 @@ func searchIssueInProject(t *testing.T) {
}, },
{ {
SearchOptions{ SearchOptions{
ProjectBoardID: optional.Some(int64(1)), ProjectColumnID: optional.Some(int64(1)),
}, },
[]int64{1}, []int64{1},
}, },
{ {
SearchOptions{ SearchOptions{
ProjectBoardID: optional.Some(int64(0)), // issue with in default board ProjectColumnID: optional.Some(int64(0)), // issue with in default column
}, },
[]int64{2}, []int64{2},
}, },

View File

@ -27,7 +27,7 @@ type IndexerData struct {
NoLabel bool `json:"no_label"` // True if LabelIDs is empty NoLabel bool `json:"no_label"` // True if LabelIDs is empty
MilestoneID int64 `json:"milestone_id"` MilestoneID int64 `json:"milestone_id"`
ProjectID int64 `json:"project_id"` ProjectID int64 `json:"project_id"`
ProjectBoardID int64 `json:"project_board_id"` ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible
PosterID int64 `json:"poster_id"` PosterID int64 `json:"poster_id"`
AssigneeID int64 `json:"assignee_id"` AssigneeID int64 `json:"assignee_id"`
MentionIDs []int64 `json:"mention_ids"` MentionIDs []int64 `json:"mention_ids"`
@ -89,8 +89,8 @@ type SearchOptions struct {
MilestoneIDs []int64 // milestones the issues have MilestoneIDs []int64 // milestones the issues have
ProjectID optional.Option[int64] // project the issues belong to ProjectID optional.Option[int64] // project the issues belong to
ProjectBoardID optional.Option[int64] // project board the issues belong to ProjectColumnID optional.Option[int64] // project column the issues belong to
PosterID optional.Option[int64] // poster of the issues PosterID optional.Option[int64] // poster of the issues

View File

@ -352,38 +352,38 @@ var cases = []*testIndexerCase{
}, },
}, },
{ {
Name: "ProjectBoardID", Name: "ProjectColumnID",
SearchOptions: &internal.SearchOptions{ SearchOptions: &internal.SearchOptions{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
ProjectBoardID: optional.Some(int64(1)), ProjectColumnID: optional.Some(int64(1)),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
for _, v := range result.Hits { for _, v := range result.Hits {
assert.Equal(t, int64(1), data[v.ID].ProjectBoardID) assert.Equal(t, int64(1), data[v.ID].ProjectColumnID)
} }
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
return v.ProjectBoardID == 1 return v.ProjectColumnID == 1
}), result.Total) }), result.Total)
}, },
}, },
{ {
Name: "no ProjectBoardID", Name: "no ProjectColumnID",
SearchOptions: &internal.SearchOptions{ SearchOptions: &internal.SearchOptions{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
ProjectBoardID: optional.Some(int64(0)), ProjectColumnID: optional.Some(int64(0)),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
for _, v := range result.Hits { for _, v := range result.Hits {
assert.Equal(t, int64(0), data[v.ID].ProjectBoardID) assert.Equal(t, int64(0), data[v.ID].ProjectColumnID)
} }
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
return v.ProjectBoardID == 0 return v.ProjectColumnID == 0
}), result.Total) }), result.Total)
}, },
}, },
@ -720,7 +720,7 @@ func generateDefaultIndexerData() []*internal.IndexerData {
NoLabel: len(labelIDs) == 0, NoLabel: len(labelIDs) == 0,
MilestoneID: issueIndex % 4, MilestoneID: issueIndex % 4,
ProjectID: issueIndex % 5, ProjectID: issueIndex % 5,
ProjectBoardID: issueIndex % 6, ProjectColumnID: issueIndex % 6,
PosterID: id%10 + 1, // PosterID should not be 0 PosterID: id%10 + 1, // PosterID should not be 0
AssigneeID: issueIndex % 10, AssigneeID: issueIndex % 10,
MentionIDs: mentionIDs, MentionIDs: mentionIDs,

View File

@ -174,8 +174,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
if options.ProjectID.Has() { if options.ProjectID.Has() {
query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value())) query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value()))
} }
if options.ProjectBoardID.Has() { if options.ProjectColumnID.Has() {
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value())) query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value()))
} }
if options.PosterID.Has() { if options.PosterID.Has() {

View File

@ -105,7 +105,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
NoLabel: len(labels) == 0, NoLabel: len(labels) == 0,
MilestoneID: issue.MilestoneID, MilestoneID: issue.MilestoneID,
ProjectID: projectID, ProjectID: projectID,
ProjectBoardID: issue.ProjectBoardID(ctx), ProjectColumnID: issue.ProjectColumnID(ctx),
PosterID: issue.PosterID, PosterID: issue.PosterID,
AssigneeID: issue.AssigneeID, AssigneeID: issue.AssigneeID,
MentionIDs: mentionIDs, MentionIDs: mentionIDs,

View File

@ -36,7 +36,7 @@ type Collector struct {
Oauths *prometheus.Desc Oauths *prometheus.Desc
Organizations *prometheus.Desc Organizations *prometheus.Desc
Projects *prometheus.Desc Projects *prometheus.Desc
ProjectBoards *prometheus.Desc ProjectColumns *prometheus.Desc
PublicKeys *prometheus.Desc PublicKeys *prometheus.Desc
Releases *prometheus.Desc Releases *prometheus.Desc
Repositories *prometheus.Desc Repositories *prometheus.Desc
@ -146,9 +146,9 @@ func NewCollector() Collector {
"Number of projects", "Number of projects",
nil, nil, nil, nil,
), ),
ProjectBoards: prometheus.NewDesc( ProjectColumns: prometheus.NewDesc(
namespace+"projects_boards", namespace+"projects_boards", // TODO: change the key name will affect the consume's result history
"Number of project boards", "Number of project columns",
nil, nil, nil, nil,
), ),
PublicKeys: prometheus.NewDesc( PublicKeys: prometheus.NewDesc(
@ -219,7 +219,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.Oauths ch <- c.Oauths
ch <- c.Organizations ch <- c.Organizations
ch <- c.Projects ch <- c.Projects
ch <- c.ProjectBoards ch <- c.ProjectColumns
ch <- c.PublicKeys ch <- c.PublicKeys
ch <- c.Releases ch <- c.Releases
ch <- c.Repositories ch <- c.Repositories
@ -336,9 +336,9 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
float64(stats.Counter.Project), float64(stats.Counter.Project),
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.ProjectBoards, c.ProjectColumns,
prometheus.GaugeValue, prometheus.GaugeValue,
float64(stats.Counter.ProjectBoard), float64(stats.Counter.ProjectColumn),
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.PublicKeys, c.PublicKeys,

View File

@ -1237,7 +1237,7 @@ tag = Tag
tags = Tags tags = Tags
issues = Issues issues = Issues
pulls = Pull requests pulls = Pull requests
project_board = Projects project = Projects
packages = Packages packages = Packages
actions = Actions actions = Actions
release = Release release = Release

View File

@ -34,7 +34,7 @@ const (
// MustEnableProjects check if projects are enabled in settings // MustEnableProjects check if projects are enabled in settings
func MustEnableProjects(ctx *context.Context) { func MustEnableProjects(ctx *context.Context) {
if unit.TypeProjects.UnitGlobalDisabled() { if unit.TypeProjects.UnitGlobalDisabled() {
ctx.NotFound("EnableKanbanBoard", nil) ctx.NotFound("EnableProjects", nil)
return return
} }
} }
@ -42,7 +42,7 @@ func MustEnableProjects(ctx *context.Context) {
// Projects renders the home page of projects // Projects renders the home page of projects
func Projects(ctx *context.Context) { func Projects(ctx *context.Context) {
shared_user.PrepareContextForProfileBigAvatar(ctx) shared_user.PrepareContextForProfileBigAvatar(ctx)
ctx.Data["Title"] = ctx.Tr("repo.project_board") ctx.Data["Title"] = ctx.Tr("repo.projects")
sortType := ctx.FormTrim("sort") sortType := ctx.FormTrim("sort")
@ -139,7 +139,7 @@ func canWriteProjects(ctx *context.Context) bool {
// RenderNewProject render creating a project page // RenderNewProject render creating a project page
func RenderNewProject(ctx *context.Context) { func RenderNewProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.new") ctx.Data["Title"] = ctx.Tr("repo.projects.new")
ctx.Data["BoardTypes"] = project_model.GetBoardConfig() ctx.Data["TemplateConfigs"] = project_model.GetTemplateConfigs()
ctx.Data["CardTypes"] = project_model.GetCardConfig() ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
ctx.Data["PageIsViewProjects"] = true ctx.Data["PageIsViewProjects"] = true
@ -168,12 +168,12 @@ func NewProjectPost(ctx *context.Context) {
} }
newProject := project_model.Project{ newProject := project_model.Project{
OwnerID: ctx.ContextUser.ID, OwnerID: ctx.ContextUser.ID,
Title: form.Title, Title: form.Title,
Description: form.Content, Description: form.Content,
CreatorID: ctx.Doer.ID, CreatorID: ctx.Doer.ID,
BoardType: form.BoardType, TemplateType: form.TemplateType,
CardType: form.CardType, CardType: form.CardType,
} }
if ctx.ContextUser.IsOrganization() { if ctx.ContextUser.IsOrganization() {
@ -314,7 +314,7 @@ func EditProjectPost(ctx *context.Context) {
} }
} }
// ViewProject renders the project board for a project // ViewProject renders the project with board view for a project
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 {
@ -326,15 +326,15 @@ func ViewProject(ctx *context.Context) {
return return
} }
boards, err := project.GetBoards(ctx) columns, err := project.GetColumns(ctx)
if err != nil { if err != nil {
ctx.ServerError("GetProjectBoards", err) ctx.ServerError("GetProjectColumns", err)
return return
} }
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
if err != nil { if err != nil {
ctx.ServerError("LoadIssuesOfBoards", err) ctx.ServerError("LoadIssuesOfColumns", err)
return return
} }
@ -377,7 +377,7 @@ func ViewProject(ctx *context.Context) {
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
ctx.Data["Project"] = project ctx.Data["Project"] = project
ctx.Data["IssuesMap"] = issuesMap ctx.Data["IssuesMap"] = issuesMap
ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend ctx.Data["Columns"] = columns
shared_user.RenderUserHeader(ctx) shared_user.RenderUserHeader(ctx)
err = shared_user.LoadHeaderCount(ctx) err = shared_user.LoadHeaderCount(ctx)
@ -389,8 +389,8 @@ func ViewProject(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplProjectsView) ctx.HTML(http.StatusOK, tplProjectsView)
} }
// DeleteProjectBoard allows for the deletion of a project board // DeleteProjectColumn allows for the deletion of a project column
func DeleteProjectBoard(ctx *context.Context) { func DeleteProjectColumn(ctx *context.Context) {
if ctx.Doer == nil { if ctx.Doer == nil {
ctx.JSON(http.StatusForbidden, map[string]string{ ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.", "message": "Only signed in users are allowed to perform this action.",
@ -404,36 +404,36 @@ func DeleteProjectBoard(ctx *context.Context) {
return return
} }
pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) pb, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
if err != nil { if err != nil {
ctx.ServerError("GetProjectBoard", err) ctx.ServerError("GetProjectColumn", err)
return return
} }
if pb.ProjectID != ctx.ParamsInt64(":id") { if pb.ProjectID != ctx.ParamsInt64(":id") {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID), "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID),
}) })
return return
} }
if project.OwnerID != ctx.ContextUser.ID { if project.OwnerID != ctx.ContextUser.ID {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID), "message": fmt.Sprintf("ProjectColumn[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
}) })
return return
} }
if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil { if err := project_model.DeleteColumnByID(ctx, ctx.ParamsInt64(":columnID")); err != nil {
ctx.ServerError("DeleteProjectBoardByID", err) ctx.ServerError("DeleteProjectColumnByID", err)
return return
} }
ctx.JSONOK() ctx.JSONOK()
} }
// AddBoardToProjectPost allows a new board to be added to a project. // AddColumnToProjectPost allows a new column to be added to a project.
func AddBoardToProjectPost(ctx *context.Context) { func AddColumnToProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditProjectBoardForm) form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
@ -441,21 +441,21 @@ func AddBoardToProjectPost(ctx *context.Context) {
return return
} }
if err := project_model.NewBoard(ctx, &project_model.Board{ if err := project_model.NewColumn(ctx, &project_model.Column{
ProjectID: project.ID, ProjectID: project.ID,
Title: form.Title, Title: form.Title,
Color: form.Color, Color: form.Color,
CreatorID: ctx.Doer.ID, CreatorID: ctx.Doer.ID,
}); err != nil { }); err != nil {
ctx.ServerError("NewProjectBoard", err) ctx.ServerError("NewProjectColumn", err)
return return
} }
ctx.JSONOK() ctx.JSONOK()
} }
// CheckProjectBoardChangePermissions check permission // CheckProjectColumnChangePermissions check permission
func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) { func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Column) {
if ctx.Doer == nil { if ctx.Doer == nil {
ctx.JSON(http.StatusForbidden, map[string]string{ ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.", "message": "Only signed in users are allowed to perform this action.",
@ -469,62 +469,60 @@ func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
return nil, nil return nil, nil
} }
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
if err != nil { if err != nil {
ctx.ServerError("GetProjectBoard", err) ctx.ServerError("GetProjectColumn", err)
return nil, nil return nil, nil
} }
if board.ProjectID != ctx.ParamsInt64(":id") { if column.ProjectID != ctx.ParamsInt64(":id") {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID), "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID),
}) })
return nil, nil return nil, nil
} }
if project.OwnerID != ctx.ContextUser.ID { if project.OwnerID != ctx.ContextUser.ID {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, project.ID), "message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, project.ID),
}) })
return nil, nil return nil, nil
} }
return project, board return project, column
} }
// EditProjectBoard allows a project board's to be updated // EditProjectColumn allows a project column's to be updated
func EditProjectBoard(ctx *context.Context) { func EditProjectColumn(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditProjectBoardForm) form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
_, board := CheckProjectBoardChangePermissions(ctx) _, column := CheckProjectColumnChangePermissions(ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
if form.Title != "" { if form.Title != "" {
board.Title = form.Title column.Title = form.Title
} }
column.Color = form.Color
board.Color = form.Color
if form.Sorting != 0 { if form.Sorting != 0 {
board.Sorting = form.Sorting column.Sorting = form.Sorting
} }
if err := project_model.UpdateBoard(ctx, board); err != nil { if err := project_model.UpdateColumn(ctx, column); err != nil {
ctx.ServerError("UpdateProjectBoard", err) ctx.ServerError("UpdateProjectColumn", err)
return return
} }
ctx.JSONOK() ctx.JSONOK()
} }
// SetDefaultProjectBoard set default board for uncategorized issues/pulls // SetDefaultProjectColumn set default column for uncategorized issues/pulls
func SetDefaultProjectBoard(ctx *context.Context) { func SetDefaultProjectColumn(ctx *context.Context) {
project, board := CheckProjectBoardChangePermissions(ctx) project, column := CheckProjectColumnChangePermissions(ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil { if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil {
ctx.ServerError("SetDefaultBoard", err) ctx.ServerError("SetDefaultColumn", err)
return return
} }
@ -550,14 +548,14 @@ func MoveIssues(ctx *context.Context) {
return return
} }
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
if err != nil { if err != nil {
ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err) ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err)
return return
} }
if board.ProjectID != project.ID { if column.ProjectID != project.ID {
ctx.NotFound("BoardNotInProject", nil) ctx.NotFound("ColumnNotInProject", nil)
return return
} }
@ -602,8 +600,8 @@ func MoveIssues(ctx *context.Context) {
} }
} }
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil {
ctx.ServerError("MoveIssuesOnProjectBoard", err) ctx.ServerError("MoveIssuesOnProjectColumn", err)
return return
} }

View File

@ -13,16 +13,16 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestCheckProjectBoardChangePermissions(t *testing.T) { func TestCheckProjectColumnChangePermissions(t *testing.T) {
unittest.PrepareTestEnv(t) unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user2/-/projects/4/4") ctx, _ := contexttest.MockContext(t, "user2/-/projects/4/4")
contexttest.LoadUser(t, ctx, 2) contexttest.LoadUser(t, ctx, 2)
ctx.ContextUser = ctx.Doer // user2 ctx.ContextUser = ctx.Doer // user2
ctx.SetParams(":id", "4") ctx.SetParams(":id", "4")
ctx.SetParams(":boardID", "4") ctx.SetParams(":columnID", "4")
project, board := org.CheckProjectBoardChangePermissions(ctx) project, column := org.CheckProjectColumnChangePermissions(ctx)
assert.NotNil(t, project) assert.NotNil(t, project)
assert.NotNil(t, board) assert.NotNil(t, column)
assert.False(t, ctx.Written()) assert.False(t, ctx.Written())
} }

View File

@ -2822,12 +2822,12 @@ func ListIssues(ctx *context.Context) {
Page: ctx.FormInt("page"), Page: ctx.FormInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
}, },
Keyword: keyword, Keyword: keyword,
RepoIDs: []int64{ctx.Repo.Repository.ID}, RepoIDs: []int64{ctx.Repo.Repository.ID},
IsPull: isPull, IsPull: isPull,
IsClosed: isClosed, IsClosed: isClosed,
ProjectBoardID: projectID, ProjectID: projectID,
SortBy: issue_indexer.SortByCreatedDesc, SortBy: issue_indexer.SortByCreatedDesc,
} }
if since != 0 { if since != 0 {
searchOpt.UpdatedAfterUnix = optional.Some(since) searchOpt.UpdatedAfterUnix = optional.Some(since)

View File

@ -36,7 +36,7 @@ const (
// MustEnableProjects check if projects are enabled in settings // MustEnableProjects check if projects are enabled in settings
func MustEnableProjects(ctx *context.Context) { func MustEnableProjects(ctx *context.Context) {
if unit.TypeProjects.UnitGlobalDisabled() { if unit.TypeProjects.UnitGlobalDisabled() {
ctx.NotFound("EnableKanbanBoard", nil) ctx.NotFound("EnableRepoProjects", nil)
return return
} }
@ -50,7 +50,7 @@ func MustEnableProjects(ctx *context.Context) {
// Projects renders the home page of projects // Projects renders the home page of projects
func Projects(ctx *context.Context) { func Projects(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.project_board") ctx.Data["Title"] = ctx.Tr("repo.projects")
sortType := ctx.FormTrim("sort") sortType := ctx.FormTrim("sort")
@ -131,7 +131,7 @@ func Projects(ctx *context.Context) {
// RenderNewProject render creating a project page // RenderNewProject render creating a project page
func RenderNewProject(ctx *context.Context) { func RenderNewProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.new") ctx.Data["Title"] = ctx.Tr("repo.projects.new")
ctx.Data["BoardTypes"] = project_model.GetBoardConfig() ctx.Data["TemplateConfigs"] = project_model.GetTemplateConfigs()
ctx.Data["CardTypes"] = project_model.GetCardConfig() ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["CancelLink"] = ctx.Repo.Repository.Link() + "/projects" ctx.Data["CancelLink"] = ctx.Repo.Repository.Link() + "/projects"
@ -149,13 +149,13 @@ func NewProjectPost(ctx *context.Context) {
} }
if err := project_model.NewProject(ctx, &project_model.Project{ if err := project_model.NewProject(ctx, &project_model.Project{
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
Title: form.Title, Title: form.Title,
Description: form.Content, Description: form.Content,
CreatorID: ctx.Doer.ID, CreatorID: ctx.Doer.ID,
BoardType: form.BoardType, TemplateType: form.TemplateType,
CardType: form.CardType, CardType: form.CardType,
Type: project_model.TypeRepository, Type: project_model.TypeRepository,
}); err != nil { }); err != nil {
ctx.ServerError("NewProject", err) ctx.ServerError("NewProject", err)
return return
@ -288,7 +288,7 @@ func EditProjectPost(ctx *context.Context) {
} }
} }
// ViewProject renders the project board for a project // ViewProject renders the project with board view
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 {
@ -304,15 +304,15 @@ func ViewProject(ctx *context.Context) {
return return
} }
boards, err := project.GetBoards(ctx) columns, err := project.GetColumns(ctx)
if err != nil { if err != nil {
ctx.ServerError("GetProjectBoards", err) ctx.ServerError("GetProjectColumns", err)
return return
} }
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
if err != nil { if err != nil {
ctx.ServerError("LoadIssuesOfBoards", err) ctx.ServerError("LoadIssuesOfColumns", err)
return return
} }
@ -367,7 +367,7 @@ func ViewProject(ctx *context.Context) {
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["Project"] = project ctx.Data["Project"] = project
ctx.Data["IssuesMap"] = issuesMap ctx.Data["IssuesMap"] = issuesMap
ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend ctx.Data["Columns"] = columns
ctx.HTML(http.StatusOK, tplProjectsView) ctx.HTML(http.StatusOK, tplProjectsView)
} }
@ -405,8 +405,8 @@ func UpdateIssueProject(ctx *context.Context) {
ctx.JSONOK() ctx.JSONOK()
} }
// DeleteProjectBoard allows for the deletion of a project board // DeleteProjectColumn allows for the deletion of a project column
func DeleteProjectBoard(ctx *context.Context) { func DeleteProjectColumn(ctx *context.Context) {
if ctx.Doer == nil { if ctx.Doer == nil {
ctx.JSON(http.StatusForbidden, map[string]string{ ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.", "message": "Only signed in users are allowed to perform this action.",
@ -431,36 +431,36 @@ func DeleteProjectBoard(ctx *context.Context) {
return return
} }
pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) pb, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
if err != nil { if err != nil {
ctx.ServerError("GetProjectBoard", err) ctx.ServerError("GetProjectColumn", err)
return return
} }
if pb.ProjectID != ctx.ParamsInt64(":id") { if pb.ProjectID != ctx.ParamsInt64(":id") {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID), "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID),
}) })
return return
} }
if project.RepoID != ctx.Repo.Repository.ID { if project.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID), "message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID),
}) })
return return
} }
if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil { if err := project_model.DeleteColumnByID(ctx, ctx.ParamsInt64(":columnID")); err != nil {
ctx.ServerError("DeleteProjectBoardByID", err) ctx.ServerError("DeleteProjectColumnByID", err)
return return
} }
ctx.JSONOK() ctx.JSONOK()
} }
// AddBoardToProjectPost allows a new board to be added to a project. // AddColumnToProjectPost allows a new column to be added to a project.
func AddBoardToProjectPost(ctx *context.Context) { func AddColumnToProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditProjectBoardForm) form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) {
ctx.JSON(http.StatusForbidden, map[string]string{ ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only authorized users are allowed to perform this action.", "message": "Only authorized users are allowed to perform this action.",
@ -478,20 +478,20 @@ func AddBoardToProjectPost(ctx *context.Context) {
return return
} }
if err := project_model.NewBoard(ctx, &project_model.Board{ if err := project_model.NewColumn(ctx, &project_model.Column{
ProjectID: project.ID, ProjectID: project.ID,
Title: form.Title, Title: form.Title,
Color: form.Color, Color: form.Color,
CreatorID: ctx.Doer.ID, CreatorID: ctx.Doer.ID,
}); err != nil { }); err != nil {
ctx.ServerError("NewProjectBoard", err) ctx.ServerError("NewProjectColumn", err)
return return
} }
ctx.JSONOK() ctx.JSONOK()
} }
func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) { func checkProjectColumnChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Column) {
if ctx.Doer == nil { if ctx.Doer == nil {
ctx.JSON(http.StatusForbidden, map[string]string{ ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.", "message": "Only signed in users are allowed to perform this action.",
@ -516,62 +516,60 @@ func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
return nil, nil return nil, nil
} }
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
if err != nil { if err != nil {
ctx.ServerError("GetProjectBoard", err) ctx.ServerError("GetProjectColumn", err)
return nil, nil return nil, nil
} }
if board.ProjectID != ctx.ParamsInt64(":id") { if column.ProjectID != ctx.ParamsInt64(":id") {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID), "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID),
}) })
return nil, nil return nil, nil
} }
if project.RepoID != ctx.Repo.Repository.ID { if project.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID), "message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, ctx.Repo.Repository.ID),
}) })
return nil, nil return nil, nil
} }
return project, board return project, column
} }
// EditProjectBoard allows a project board's to be updated // EditProjectColumn allows a project column's to be updated
func EditProjectBoard(ctx *context.Context) { func EditProjectColumn(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditProjectBoardForm) form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
_, board := checkProjectBoardChangePermissions(ctx) _, column := checkProjectColumnChangePermissions(ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
if form.Title != "" { if form.Title != "" {
board.Title = form.Title column.Title = form.Title
} }
column.Color = form.Color
board.Color = form.Color
if form.Sorting != 0 { if form.Sorting != 0 {
board.Sorting = form.Sorting column.Sorting = form.Sorting
} }
if err := project_model.UpdateBoard(ctx, board); err != nil { if err := project_model.UpdateColumn(ctx, column); err != nil {
ctx.ServerError("UpdateProjectBoard", err) ctx.ServerError("UpdateProjectColumn", err)
return return
} }
ctx.JSONOK() ctx.JSONOK()
} }
// SetDefaultProjectBoard set default board for uncategorized issues/pulls // SetDefaultProjectColumn set default column for uncategorized issues/pulls
func SetDefaultProjectBoard(ctx *context.Context) { func SetDefaultProjectColumn(ctx *context.Context) {
project, board := checkProjectBoardChangePermissions(ctx) project, column := checkProjectColumnChangePermissions(ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil { if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil {
ctx.ServerError("SetDefaultBoard", err) ctx.ServerError("SetDefaultColumn", err)
return return
} }
@ -608,18 +606,18 @@ func MoveIssues(ctx *context.Context) {
return return
} }
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
if err != nil { if err != nil {
if project_model.IsErrProjectBoardNotExist(err) { if project_model.IsErrProjectColumnNotExist(err) {
ctx.NotFound("ProjectBoardNotExist", nil) ctx.NotFound("ProjectColumnNotExist", nil)
} else { } else {
ctx.ServerError("GetProjectBoard", err) ctx.ServerError("GetProjectColumn", err)
} }
return return
} }
if board.ProjectID != project.ID { if column.ProjectID != project.ID {
ctx.NotFound("BoardNotInProject", nil) ctx.NotFound("ColumnNotInProject", nil)
return return
} }
@ -663,8 +661,8 @@ func MoveIssues(ctx *context.Context) {
} }
} }
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil {
ctx.ServerError("MoveIssuesOnProjectBoard", err) ctx.ServerError("MoveIssuesOnProjectColumn", err)
return return
} }

View File

@ -12,16 +12,16 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestCheckProjectBoardChangePermissions(t *testing.T) { func TestCheckProjectColumnChangePermissions(t *testing.T) {
unittest.PrepareTestEnv(t) unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user2/repo1/projects/1/2") ctx, _ := contexttest.MockContext(t, "user2/repo1/projects/1/2")
contexttest.LoadUser(t, ctx, 2) contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
ctx.SetParams(":id", "1") ctx.SetParams(":id", "1")
ctx.SetParams(":boardID", "2") ctx.SetParams(":columnID", "2")
project, board := checkProjectBoardChangePermissions(ctx) project, column := checkProjectColumnChangePermissions(ctx)
assert.NotNil(t, project) assert.NotNil(t, project)
assert.NotNil(t, board) assert.NotNil(t, column)
assert.False(t, ctx.Written()) assert.False(t, ctx.Written())
} }

View File

@ -978,7 +978,7 @@ func registerRoutes(m *web.Route) {
m.Get("/new", org.RenderNewProject) m.Get("/new", org.RenderNewProject)
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
m.Group("/{id}", func() { m.Group("/{id}", func() {
m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost) m.Post("", web.Bind(forms.EditProjectColumnForm{}), org.AddColumnToProjectPost)
m.Post("/move", project.MoveColumns) m.Post("/move", project.MoveColumns)
m.Post("/delete", org.DeleteProject) m.Post("/delete", org.DeleteProject)
@ -986,10 +986,10 @@ func registerRoutes(m *web.Route) {
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost) m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost)
m.Post("/{action:open|close}", org.ChangeProjectStatus) m.Post("/{action:open|close}", org.ChangeProjectStatus)
m.Group("/{boardID}", func() { m.Group("/{columnID}", func() {
m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard) m.Put("", web.Bind(forms.EditProjectColumnForm{}), org.EditProjectColumn)
m.Delete("", org.DeleteProjectBoard) m.Delete("", org.DeleteProjectColumn)
m.Post("/default", org.SetDefaultProjectBoard) m.Post("/default", org.SetDefaultProjectColumn)
m.Post("/move", org.MoveIssues) m.Post("/move", org.MoveIssues)
}) })
@ -1352,7 +1352,7 @@ func registerRoutes(m *web.Route) {
m.Get("/new", repo.RenderNewProject) m.Get("/new", repo.RenderNewProject)
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
m.Group("/{id}", func() { m.Group("/{id}", func() {
m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost) m.Post("", web.Bind(forms.EditProjectColumnForm{}), repo.AddColumnToProjectPost)
m.Post("/move", project.MoveColumns) m.Post("/move", project.MoveColumns)
m.Post("/delete", repo.DeleteProject) m.Post("/delete", repo.DeleteProject)
@ -1360,10 +1360,10 @@ func registerRoutes(m *web.Route) {
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost) m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost)
m.Post("/{action:open|close}", repo.ChangeProjectStatus) m.Post("/{action:open|close}", repo.ChangeProjectStatus)
m.Group("/{boardID}", func() { m.Group("/{columnID}", func() {
m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard) m.Put("", web.Bind(forms.EditProjectColumnForm{}), repo.EditProjectColumn)
m.Delete("", repo.DeleteProjectBoard) m.Delete("", repo.DeleteProjectColumn)
m.Post("/default", repo.SetDefaultProjectBoard) m.Post("/default", repo.SetDefaultProjectColumn)
m.Post("/move", repo.MoveIssues) m.Post("/move", repo.MoveIssues)
}) })

View File

@ -370,45 +370,21 @@ func (i IssueLockForm) HasValidReason() bool {
return false return false
} }
// __________ __ __
// \______ \_______ ____ |__| ____ _____/ |_ ______
// | ___/\_ __ \/ _ \ | |/ __ \_/ ___\ __\/ ___/
// | | | | \( <_> ) | \ ___/\ \___| | \___ \
// |____| |__| \____/\__| |\___ >\___ >__| /____ >
// \______| \/ \/ \/
// CreateProjectForm form for creating a project // CreateProjectForm form for creating a project
type CreateProjectForm struct { type CreateProjectForm struct {
Title string `binding:"Required;MaxSize(100)"` Title string `binding:"Required;MaxSize(100)"`
Content string Content string
BoardType project_model.BoardType TemplateType project_model.TemplateType
CardType project_model.CardType CardType project_model.CardType
} }
// UserCreateProjectForm is a from for creating an individual or organization // EditProjectColumnForm is a form for editing a project column
// form. type EditProjectColumnForm struct {
type UserCreateProjectForm struct {
Title string `binding:"Required;MaxSize(100)"`
Content string
BoardType project_model.BoardType
CardType project_model.CardType
UID int64 `binding:"Required"`
}
// EditProjectBoardForm is a form for editing a project board
type EditProjectBoardForm struct {
Title string `binding:"Required;MaxSize(100)"` Title string `binding:"Required;MaxSize(100)"`
Sorting int8 Sorting int8
Color string `binding:"MaxSize(7)"` Color string `binding:"MaxSize(7)"`
} }
// _____ .__.__ __
// / \ |__| | ____ _______/ |_ ____ ____ ____
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
// / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/
// \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ >
// \/ \/ \/ \/ \/
// CreateMilestoneForm form for creating milestone // CreateMilestoneForm form for creating milestone
type CreateMilestoneForm struct { type CreateMilestoneForm struct {
Title string `binding:"Required;MaxSize(50)"` Title string `binding:"Required;MaxSize(50)"`
@ -422,13 +398,6 @@ func (f *CreateMilestoneForm) Validate(req *http.Request, errs binding.Errors) b
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
} }
// .____ ___. .__
// | | _____ \_ |__ ____ | |
// | | \__ \ | __ \_/ __ \| |
// | |___ / __ \| \_\ \ ___/| |__
// |_______ (____ /___ /\___ >____/
// \/ \/ \/ \/
// CreateLabelForm form for creating label // CreateLabelForm form for creating label
type CreateLabelForm struct { type CreateLabelForm struct {
ID int64 ID int64
@ -456,13 +425,6 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
} }
// __________ .__ .__ __________ __
// \______ \__ __| | | | \______ \ ____ ________ __ ____ _______/ |_
// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
// | | | | / |_| |__ | | \ ___< <_| | | /\ ___/ \___ \ | |
// |____| |____/|____/____/ |____|_ /\___ >__ |____/ \___ >____ > |__|
// \/ \/ |__| \/ \/
// MergePullRequestForm form for merging Pull Request // MergePullRequestForm form for merging Pull Request
// swagger:model MergePullRequestOption // swagger:model MergePullRequestOption
type MergePullRequestForm struct { type MergePullRequestForm struct {

View File

@ -65,7 +65,7 @@ var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{
}, },
"project": { "project": {
/*30*/ issues_model.CommentTypeProject, /*30*/ issues_model.CommentTypeProject,
/*31*/ issues_model.CommentTypeProjectBoard, /*31*/ issues_model.CommentTypeProjectColumn,
}, },
"issue_ref": { "issue_ref": {
/*33*/ issues_model.CommentTypeChangeIssueRef, /*33*/ issues_model.CommentTypeChangeIssueRef,

View File

@ -25,11 +25,11 @@
<div class="field"> <div class="field">
<label>{{ctx.Locale.Tr "repo.projects.template.desc"}}</label> <label>{{ctx.Locale.Tr "repo.projects.template.desc"}}</label>
<div class="ui selection dropdown"> <div class="ui selection dropdown">
<input type="hidden" name="board_type" value="{{.type}}"> <input type="hidden" name="template_type" value="{{.type}}">
<div class="default text">{{ctx.Locale.Tr "repo.projects.template.desc_helper"}}</div> <div class="default text">{{ctx.Locale.Tr "repo.projects.template.desc_helper"}}</div>
<div class="menu"> <div class="menu">
{{range $element := .BoardTypes}} {{range $element := .TemplateConfigs}}
<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{ctx.Locale.Tr $element.Translation}}</div> <div class="item" data-id="{{$element.TemplateType}}" data-value="{{$element.TemplateType}}">{{ctx.Locale.Tr $element.Translation}}</div>
{{end}} {{end}}
</div> </div>
</div> </div>

View File

@ -131,7 +131,7 @@
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}} {{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}}
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item"> <a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}} {{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project"}}
{{if .Repository.NumOpenProjects}} {{if .Repository.NumOpenProjects}}
<span class="ui small label">{{CountFmt .Repository.NumOpenProjects}}</span> <span class="ui small label">{{CountFmt .Repository.NumOpenProjects}}</span>
{{end}} {{end}}

View File

@ -71,7 +71,7 @@
<!-- Projects --> <!-- Projects -->
<div class="ui{{if not (or .OpenProjects .ClosedProjects)}} disabled{{end}} dropdown jump item"> <div class="ui{{if not (or .OpenProjects .ClosedProjects)}} disabled{{end}} dropdown jump item">
<span class="text"> <span class="text">
{{ctx.Locale.Tr "repo.project_board"}} {{ctx.Locale.Tr "repo.projects"}}
</span> </span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu"> <div class="menu">

View File

@ -15,7 +15,7 @@
{{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}} {{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}}
{{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}} {{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}}
<div class="inline field"> <div class="inline field">
<label>{{ctx.Locale.Tr "repo.project_board"}}</label> <label>{{ctx.Locale.Tr "repo.projects"}}</label>
<div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> <div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_projects" type="checkbox" {{if $isProjectsEnabled}}checked{{end}}> <input class="enable-system" name="enable_projects" type="checkbox" {{if $isProjectsEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label> <label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>

View File

@ -35,23 +35,23 @@ func TestMoveRepoProjectColumns(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
project1 := project_model.Project{ project1 := project_model.Project{
Title: "new created project", Title: "new created project",
RepoID: repo2.ID, RepoID: repo2.ID,
Type: project_model.TypeRepository, Type: project_model.TypeRepository,
BoardType: project_model.BoardTypeNone, TemplateType: project_model.TemplateTypeNone,
} }
err := project_model.NewProject(db.DefaultContext, &project1) err := project_model.NewProject(db.DefaultContext, &project1)
assert.NoError(t, err) assert.NoError(t, err)
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
err = project_model.NewBoard(db.DefaultContext, &project_model.Board{ err = project_model.NewColumn(db.DefaultContext, &project_model.Column{
Title: fmt.Sprintf("column %d", i+1), Title: fmt.Sprintf("column %d", i+1),
ProjectID: project1.ID, ProjectID: project1.ID,
}) })
assert.NoError(t, err) assert.NoError(t, err)
} }
columns, err := project1.GetBoards(db.DefaultContext) columns, err := project1.GetColumns(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, columns, 3) assert.Len(t, columns, 3)
assert.EqualValues(t, 0, columns[0].Sorting) assert.EqualValues(t, 0, columns[0].Sorting)
@ -72,7 +72,7 @@ func TestMoveRepoProjectColumns(t *testing.T) {
}) })
sess.MakeRequest(t, req, http.StatusOK) sess.MakeRequest(t, req, http.StatusOK)
columnsAfter, err := project1.GetBoards(db.DefaultContext) columnsAfter, err := project1.GetColumns(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, columns, 3) assert.Len(t, columns, 3)
assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID) assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)

View File

@ -7,7 +7,7 @@
} }
.project-column { .project-column {
background-color: var(--color-project-board-bg) !important; background-color: var(--color-project-column-bg) !important;
border: 1px solid var(--color-secondary) !important; border: 1px solid var(--color-secondary) !important;
margin: 0 0.5rem !important; margin: 0 0.5rem !important;
padding: 0.5rem !important; padding: 0.5rem !important;

View File

@ -218,7 +218,7 @@
--color-expand-button: #2b353e; --color-expand-button: #2b353e;
--color-placeholder-text: var(--color-text-light-3); --color-placeholder-text: var(--color-text-light-3);
--color-editor-line-highlight: var(--color-primary-light-5); --color-editor-line-highlight: var(--color-primary-light-5);
--color-project-board-bg: var(--color-secondary-light-2); --color-project-column-bg: var(--color-secondary-light-2);
--color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */ --color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
--color-reaction-bg: #e8e8ff12; --color-reaction-bg: #e8e8ff12;
--color-reaction-hover-bg: var(--color-primary-light-4); --color-reaction-hover-bg: var(--color-primary-light-4);

View File

@ -218,7 +218,7 @@
--color-expand-button: #cfe8fa; --color-expand-button: #cfe8fa;
--color-placeholder-text: var(--color-text-light-3); --color-placeholder-text: var(--color-text-light-3);
--color-editor-line-highlight: var(--color-primary-light-6); --color-editor-line-highlight: var(--color-primary-light-6);
--color-project-board-bg: var(--color-secondary-light-4); --color-project-column-bg: var(--color-secondary-light-4);
--color-caret: var(--color-text-dark); --color-caret: var(--color-text-dark);
--color-reaction-bg: #0000170a; --color-reaction-bg: #0000170a;
--color-reaction-hover-bg: var(--color-primary-light-5); --color-reaction-hover-bg: var(--color-primary-light-5);