2022-03-29 17:16:31 +03:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2022-03-29 17:16:31 +03:00
package project
import (
"context"
2024-05-08 16:44:57 +03:00
"errors"
2022-03-29 17:16:31 +03:00
"fmt"
"regexp"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
2024-05-08 16:44:57 +03:00
"code.gitea.io/gitea/modules/util"
2022-03-29 17:16:31 +03:00
"xorm.io/builder"
)
type (
// BoardType is used to represent a project board type
BoardType uint8
2023-02-11 11:12:41 +03:00
// CardType is used to represent a project board card type
CardType uint8
2022-03-29 17:16:31 +03:00
// 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
)
2023-02-11 11:12:41 +03:00
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
)
2022-03-29 17:16:31 +03:00
// 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
2023-09-29 15:12:54 +03:00
func ( b * Board ) NumIssues ( ctx context . Context ) int {
c , err := db . GetEngine ( ctx ) . Table ( "project_issue" ) .
2022-03-29 17:16:31 +03:00
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 )
}
2024-05-08 16:44:57 +03:00
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
}
2022-03-29 17:16:31 +03:00
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
}
}
2023-02-11 11:12:41 +03:00
// 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
}
}
2022-03-29 17:16:31 +03:00
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
}
2024-03-27 23:54:32 +03:00
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
}
2022-03-29 17:16:31 +03:00
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 )
}
2024-05-08 16:44:57 +03:00
// maxProjectColumns max columns allowed in a project, this should not bigger than 127
// because sorting is int8 in database
const maxProjectColumns = 20
2022-03-29 17:16:31 +03:00
// NewBoard adds a new project board to a given project
2023-09-29 15:12:54 +03:00
func NewBoard ( ctx context . Context , board * Board ) error {
2022-03-29 17:16:31 +03:00
if len ( board . Color ) != 0 && ! BoardColorPattern . MatchString ( board . Color ) {
return fmt . Errorf ( "bad color code: %s" , board . Color )
}
2024-05-08 16:44:57 +03:00
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 ) )
2023-09-29 15:12:54 +03:00
_ , err := db . GetEngine ( ctx ) . Insert ( board )
2022-03-29 17:16:31 +03:00
return err
}
// DeleteBoardByID removes all issues references to the project board.
2023-09-29 15:12:54 +03:00
func DeleteBoardByID ( ctx context . Context , boardID int64 ) error {
ctx , committer , err := db . TxContext ( ctx )
2022-03-29 17:16:31 +03:00
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 {
2022-05-20 17:08:52 +03:00
board , err := GetBoard ( ctx , boardID )
2022-03-29 17:16:31 +03:00
if err != nil {
if IsErrProjectBoardNotExist ( err ) {
return nil
}
return err
}
2024-03-27 23:54:32 +03:00
if board . Default {
return fmt . Errorf ( "deleteBoardByID: cannot delete default board" )
}
2024-05-08 16:44:57 +03:00
// 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 {
2022-03-29 17:16:31 +03:00
return err
}
2022-05-20 17:08:52 +03:00
if _ , err := db . GetEngine ( ctx ) . ID ( board . ID ) . NoAutoCondition ( ) . Delete ( board ) ; err != nil {
2022-03-29 17:16:31 +03:00
return err
}
return nil
}
2022-05-20 17:08:52 +03:00
func deleteBoardByProjectID ( ctx context . Context , projectID int64 ) error {
_ , err := db . GetEngine ( ctx ) . Where ( "project_id=?" , projectID ) . Delete ( & Board { } )
2022-03-29 17:16:31 +03:00
return err
}
// GetBoard fetches the current board of a project
2022-05-20 17:08:52 +03:00
func GetBoard ( ctx context . Context , boardID int64 ) ( * Board , error ) {
2022-03-29 17:16:31 +03:00
board := new ( Board )
2022-05-20 17:08:52 +03:00
has , err := db . GetEngine ( ctx ) . ID ( boardID ) . Get ( board )
2022-03-29 17:16:31 +03:00
if err != nil {
return nil , err
} else if ! has {
return nil , ErrProjectBoardNotExist { BoardID : boardID }
}
return board , nil
}
// UpdateBoard updates a project board
2022-05-20 17:08:52 +03:00
func UpdateBoard ( ctx context . Context , board * Board ) error {
2022-03-29 17:16:31 +03:00
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" )
2022-05-20 17:08:52 +03:00
_ , err := db . GetEngine ( ctx ) . ID ( board . ID ) . Cols ( fieldToUpdate ... ) . Update ( board )
2022-03-29 17:16:31 +03:00
return err
}
// GetBoards fetches all boards related to a project
2023-04-09 17:07:23 +03:00
func ( p * Project ) GetBoards ( ctx context . Context ) ( BoardList , error ) {
2022-03-29 17:16:31 +03:00
boards := make ( [ ] * Board , 0 , 5 )
2024-05-08 16:44:57 +03:00
if err := db . GetEngine ( ctx ) . Where ( "project_id=?" , p . ID ) . OrderBy ( "sorting, id" ) . Find ( & boards ) ; err != nil {
2022-03-29 17:16:31 +03:00
return nil , err
}
2024-05-08 16:44:57 +03:00
return boards , nil
2022-03-29 17:16:31 +03:00
}
2024-05-08 16:44:57 +03:00
// GetDefaultBoard return default board and ensure only one exists
func ( p * Project ) GetDefaultBoard ( ctx context . Context ) ( * Board , error ) {
2024-03-28 19:14:30 +03:00
var board Board
has , err := db . GetEngine ( ctx ) .
Where ( "project_id=? AND `default` = ?" , p . ID , true ) .
Desc ( "id" ) . Get ( & board )
if err != nil {
2022-03-29 17:16:31 +03:00
return nil , err
}
2024-03-27 23:54:32 +03:00
2024-03-28 19:14:30 +03:00
if has {
2022-03-29 17:16:31 +03:00
return & board , nil
}
2024-03-28 19:14:30 +03:00
// create a default board if none is found
board = Board {
ProjectID : p . ID ,
Default : true ,
Title : "Uncategorized" ,
CreatorID : p . CreatorID ,
2024-03-27 23:54:32 +03:00
}
2024-03-28 19:14:30 +03:00
if _ , err := db . GetEngine ( ctx ) . Insert ( & board ) ; err != nil {
return nil , err
}
return & board , nil
2022-03-29 17:16:31 +03:00
}
// SetDefaultBoard represents a board for issues not assigned to one
2023-09-29 15:12:54 +03:00
func SetDefaultBoard ( ctx context . Context , projectID , boardID int64 ) error {
2024-03-28 19:14:30 +03:00
return db . WithTx ( ctx , func ( ctx context . Context ) error {
if _ , err := GetBoard ( ctx , boardID ) ; err != nil {
return err
}
2022-03-29 17:16:31 +03:00
2024-03-28 19:14:30 +03:00
if _ , err := db . GetEngine ( ctx ) . Where ( builder . Eq {
"project_id" : projectID ,
"`default`" : true ,
} ) . Cols ( "`default`" ) . Update ( & Board { Default : false } ) ; err != nil {
return err
}
2022-03-29 17:16:31 +03:00
2024-03-28 19:14:30 +03:00
_ , err := db . GetEngine ( ctx ) . ID ( boardID ) .
Where ( builder . Eq { "project_id" : projectID } ) .
Cols ( "`default`" ) . Update ( & Board { Default : true } )
return err
} )
2022-03-29 17:16:31 +03:00
}
// UpdateBoardSorting update project board sorting
2023-09-29 15:12:54 +03:00
func UpdateBoardSorting ( ctx context . Context , bs BoardList ) error {
2024-03-28 19:14:30 +03:00
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
}
2022-03-29 17:16:31 +03:00
}
2024-03-28 19:14:30 +03:00
return nil
} )
2022-03-29 17:16:31 +03:00
}
2024-05-08 16:44:57 +03:00
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
} )
}