forked from Shiloh/githaven
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>
This commit is contained in:
parent
072b029b33
commit
98751108b1
@ -828,7 +828,7 @@ and
|
|||||||
|
|
||||||
## Project (`project`)
|
## Project (`project`)
|
||||||
|
|
||||||
Default templates for project boards:
|
Default templates for project board view:
|
||||||
|
|
||||||
- `PROJECT_BOARD_BASIC_KANBAN_TYPE`: **To Do, In Progress, Done**
|
- `PROJECT_BOARD_BASIC_KANBAN_TYPE`: **To Do, In Progress, Done**
|
||||||
- `PROJECT_BOARD_BUG_TRIAGE_TYPE`: **Needs Triage, High Priority, Low Priority, Closed**
|
- `PROJECT_BOARD_BUG_TRIAGE_TYPE`: **Needs Triage, High Priority, Low Priority, Closed**
|
||||||
|
@ -37,7 +37,7 @@ You can try it out using [the online demo](https://try.gitea.io/).
|
|||||||
|
|
||||||
- CI/CD: Gitea Actions supports CI/CD functionality, compatible with GitHub Actions. Users can write workflows in familiar YAML format and reuse a variety of existing Actions plugins. Actions plugins support downloading from any Git website.
|
- CI/CD: Gitea Actions supports CI/CD functionality, compatible with GitHub Actions. Users can write workflows in familiar YAML format and reuse a variety of existing Actions plugins. Actions plugins support downloading from any Git website.
|
||||||
|
|
||||||
- Project Management: Gitea tracks project requirements, features, and bugs through boards and issues. Issues support features like branches, tags, milestones, assignments, time tracking, due dates, dependencies, and more.
|
- Project Management: Gitea tracks project requirements, features, and bugs through columns and issues. Issues support features like branches, tags, milestones, assignments, time tracking, due dates, dependencies, and more.
|
||||||
|
|
||||||
- Artifact Repository: Gitea supports over 20 different types of public or private software package management, including Cargo, Chef, Composer, Conan, Conda, Container, Helm, Maven, npm, NuGet, Pub, PyPI, RubyGems, Vagrant, and more.
|
- Artifact Repository: Gitea supports over 20 different types of public or private software package management, including Cargo, Chef, Composer, Conan, Conda, Container, Helm, Maven, npm, NuGet, Pub, PyPI, RubyGems, Vagrant, and more.
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ _Symbols used in table:_
|
|||||||
| Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
| Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
||||||
| Lock Discussion | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
| Lock Discussion | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
||||||
| Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
| Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
||||||
| Issue Boards (Kanban) | [/](https://github.com/go-gitea/gitea/issues/14710) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
| Projects | [/](https://github.com/go-gitea/gitea/issues/14710) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
||||||
| Create branch from issue | [✘](https://github.com/go-gitea/gitea/issues/20226) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
| Create branch from issue | [✘](https://github.com/go-gitea/gitea/issues/20226) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
||||||
| Convert comment to new issue | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
| Convert comment to new issue | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
|
||||||
| Issue search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| Issue search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
||||||
|
@ -48,7 +48,7 @@ With different permissions, people could do different things with these units.
|
|||||||
| Wiki | View wiki pages. Clone the wiki repository. | Create/Edit wiki pages, push | - |
|
| Wiki | View wiki pages. Clone the wiki repository. | Create/Edit wiki pages, push | - |
|
||||||
| ExternalWiki | Link to an external wiki | - | - |
|
| ExternalWiki | Link to an external wiki | - | - |
|
||||||
| ExternalTracker | Link to an external issue tracker | - | - |
|
| ExternalTracker | Link to an external issue tracker | - | - |
|
||||||
| Projects | View the boards | Change issues across boards | - |
|
| Projects | View the columns of projects | Change issues across columns | - |
|
||||||
| Packages | View the packages | Upload/Delete packages | - |
|
| Packages | View the packages | Upload/Delete packages | - |
|
||||||
| Actions | View the Actions logs | Approve / Cancel / Restart | - |
|
| Actions | View the Actions logs | Approve / Cancel / Restart | - |
|
||||||
| Settings | - | - | Manage the repository |
|
| Settings | - | - | Manage the repository |
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ const (
|
|||||||
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",
|
||||||
|
@ -37,20 +37,20 @@ 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",
|
||||||
})
|
})
|
||||||
@ -60,7 +60,7 @@ 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",
|
||||||
})
|
})
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
@ -155,7 +155,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
|
|||||||
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,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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())
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
359
models/project/column.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
@ -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)
|
@ -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
|
||||||
|
@ -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,26 +62,26 @@ 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"`
|
||||||
@ -98,7 +92,7 @@ type Project struct {
|
|||||||
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
|
||||||
|
|
||||||
@ -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,12 +248,7 @@ 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 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer committer.Close()
|
|
||||||
|
|
||||||
if err := db.Insert(ctx, p); err != nil {
|
if err := db.Insert(ctx, p); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -279,11 +259,8 @@ func NewProject(ctx context.Context, p *Project) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := createBoardsForProjectsType(ctx, p); err != nil {
|
return createDefaultColumnsForProject(ctx, p)
|
||||||
return err
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return committer.Commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ func TestProject(t *testing.T) {
|
|||||||
|
|
||||||
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,
|
||||||
|
45
models/project/template.go
Normal file
45
models/project/template.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -224,8 +224,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() {
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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() {
|
||||||
|
@ -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},
|
||||||
},
|
},
|
||||||
|
@ -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"`
|
||||||
@ -90,7 +90,7 @@ 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
|
||||||
|
|
||||||
|
@ -338,38 +338,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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -706,7 +706,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,
|
||||||
|
@ -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() {
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -1215,7 +1215,7 @@ branches = Branches
|
|||||||
tags = Tags
|
tags = Tags
|
||||||
issues = Issues
|
issues = Issues
|
||||||
pulls = Pull Requests
|
pulls = Pull Requests
|
||||||
project_board = Projects
|
projects = Projects
|
||||||
packages = Packages
|
packages = Packages
|
||||||
actions = Actions
|
actions = Actions
|
||||||
labels = Labels
|
labels = Labels
|
||||||
@ -1379,7 +1379,7 @@ ext_issues = Access to External Issues
|
|||||||
ext_issues.desc = Link to an external issue tracker.
|
ext_issues.desc = Link to an external issue tracker.
|
||||||
|
|
||||||
projects = Projects
|
projects = Projects
|
||||||
projects.desc = Manage issues and pulls in project boards.
|
projects.desc = Manage issues and pulls in projects.
|
||||||
projects.description = Description (optional)
|
projects.description = Description (optional)
|
||||||
projects.description_placeholder = Description
|
projects.description_placeholder = Description
|
||||||
projects.create = Create Project
|
projects.create = Create Project
|
||||||
|
@ -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
|
||||||
@ -172,7 +172,7 @@ func NewProjectPost(ctx *context.Context) {
|
|||||||
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -2830,7 +2830,7 @@ func ListIssues(ctx *context.Context) {
|
|||||||
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 {
|
||||||
|
@ -36,7 +36,7 @@ const (
|
|||||||
// MustEnableRepoProjects check if repo projects are enabled in settings
|
// MustEnableRepoProjects check if repo projects are enabled in settings
|
||||||
func MustEnableRepoProjects(ctx *context.Context) {
|
func MustEnableRepoProjects(ctx *context.Context) {
|
||||||
if unit.TypeProjects.UnitGlobalDisabled() {
|
if unit.TypeProjects.UnitGlobalDisabled() {
|
||||||
ctx.NotFound("EnableKanbanBoard", nil)
|
ctx.NotFound("EnableRepoProjects", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ func MustEnableRepoProjects(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")
|
||||||
|
|
||||||
@ -132,7 +132,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"
|
||||||
@ -154,7 +154,7 @@ func NewProjectPost(ctx *context.Context) {
|
|||||||
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 {
|
||||||
@ -289,7 +289,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 {
|
||||||
@ -305,15 +305,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,7 +368,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)
|
||||||
}
|
}
|
||||||
@ -406,8 +406,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.",
|
||||||
@ -432,36 +432,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.",
|
||||||
@ -479,20 +479,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.",
|
||||||
@ -517,62 +517,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -609,18 +607,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -664,8 +662,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -1000,7 +1000,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)
|
||||||
|
|
||||||
@ -1008,10 +1008,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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1356,7 +1356,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)
|
||||||
|
|
||||||
@ -1364,10 +1364,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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -505,45 +505,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)"`
|
||||||
@ -557,13 +533,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
|
||||||
@ -591,13 +560,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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -180,7 +180,7 @@
|
|||||||
{{$projectsUnit := .Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeProjects}}
|
{{$projectsUnit := .Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeProjects}}
|
||||||
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
|
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
|
||||||
<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.projects"}}
|
||||||
{{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}}
|
||||||
|
@ -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">
|
||||||
|
@ -467,7 +467,7 @@
|
|||||||
{{$isProjectsGlobalDisabled := ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled}}
|
{{$isProjectsGlobalDisabled := ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled}}
|
||||||
{{$projectsUnit := .Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeProjects}}
|
{{$projectsUnit := .Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeProjects}}
|
||||||
<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" data-target="#projects_box" {{if $isProjectsEnabled}}checked{{end}}>
|
<input class="enable-system" name="enable_projects" type="checkbox" data-target="#projects_box" {{if $isProjectsEnabled}}checked{{end}}>
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
|
||||||
|
@ -42,20 +42,20 @@ func TestMoveRepoProjectColumns(t *testing.T) {
|
|||||||
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)
|
||||||
@ -76,7 +76,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)
|
||||||
|
@ -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;
|
||||||
|
@ -216,7 +216,7 @@
|
|||||||
--color-expand-button: #2f363d;
|
--color-expand-button: #2f363d;
|
||||||
--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: #e8f3ff12;
|
--color-reaction-bg: #e8f3ff12;
|
||||||
--color-reaction-hover-bg: var(--color-primary-light-4);
|
--color-reaction-hover-bg: var(--color-primary-light-4);
|
||||||
|
@ -216,7 +216,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);
|
||||||
|
Loading…
Reference in New Issue
Block a user