Add project board card move API
Some checks failed
cron-lock / action (push) Has been cancelled
cron-translations / crowdin-pull (push) Has been cancelled
cron-translations / crowdin-push (push) Has been cancelled
cron-licenses / cron-licenses (push) Has been cancelled

This commit is contained in:
Mikhail Kilin
2026-05-31 04:19:15 +03:00
parent ee3b2ac09d
commit f4963a22e2
4 changed files with 198 additions and 0 deletions

View File

@@ -1564,6 +1564,7 @@ func Routes() *web.Route {
Get(projects.ListProjectBoards). Get(projects.ListProjectBoards).
Post(bind(api.NewProjectBoardPayload{}), projects.CreateProjectBoard) Post(bind(api.NewProjectBoardPayload{}), projects.CreateProjectBoard)
m.Get("/boards/{boardId}/cards", projects.ListProjectBoardCards) m.Get("/boards/{boardId}/cards", projects.ListProjectBoardCards)
m.Post("/boards/{boardId}/cards/{cardId}/move", projects.MoveProjectBoardCard)
}) })
m.Group("/boards", func() { m.Group("/boards", func() {

View File

@@ -6,6 +6,7 @@ package projects
import ( import (
"net/http" "net/http"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project" project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
@@ -144,6 +145,81 @@ func ListProjectBoardCards(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, cards) ctx.JSON(http.StatusOK, cards)
} }
func MoveProjectBoardCard(ctx *context.APIContext) {
// swagger:operation POST /projects/{projectId}/boards/{boardId}/cards/{cardId}/move board boardMoveProjectBoardCard
// ---
// summary: Move project board card to another board
// produces:
// - application/json
// parameters:
// - name: projectId
// in: path
// description: id of the project
// type: string
// required: true
// - name: boardId
// in: path
// description: id of the target board
// type: string
// required: true
// - name: cardId
// in: path
// description: internal id of the issue card to move
// type: string
// required: true
// responses:
// "204":
// "description": "Project board card moved"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
projectID := ctx.ParamsInt64(":projectId")
project, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
ctx.Error(http.StatusNotFound, "MoveProjectBoardCard", err)
return
}
boardID := ctx.ParamsInt64(":boardId")
board, err := project_model.GetBoard(ctx, boardID)
if err != nil {
ctx.Error(http.StatusNotFound, "GetProjectBoard", err)
return
}
if board.ProjectID != project.ID {
ctx.NotFound("BoardNotInProject", nil)
return
}
cardID := ctx.ParamsInt64(":cardId")
has, err := db.GetEngine(ctx).
Where("project_id=? AND issue_id=?", project.ID, cardID).
Get(new(project_model.ProjectIssue))
if err != nil {
ctx.InternalServerError(err)
return
}
if !has {
ctx.NotFound("ProjectCardNotFound", nil)
return
}
var maxSorting int64
if _, err := db.GetEngine(ctx).
SQL("SELECT COALESCE(MAX(sorting), 0) FROM project_issue WHERE project_id = ? AND project_board_id = ?", project.ID, board.ID).
Get(&maxSorting); err != nil {
ctx.InternalServerError(err)
return
}
if err := project_model.MoveIssuesOnProjectBoard(ctx, board, map[int64]int64{maxSorting + 1: cardID}); err != nil {
ctx.InternalServerError(err)
return
}
ctx.Status(http.StatusNoContent)
}
func CreateProjectBoard(ctx *context.APIContext) { func CreateProjectBoard(ctx *context.APIContext) {
// swagger:operation POST /projects/{projectId}/boards board boardCreateProjectBoard // swagger:operation POST /projects/{projectId}/boards board boardCreateProjectBoard
// --- // ---

View File

@@ -3227,6 +3227,91 @@
] ]
} }
}, },
"/projects/{projectId}/boards/{boardId}/cards": {
"get": {
"produces": [
"application/json"
],
"tags": [
"board"
],
"summary": "Get project board cards",
"operationId": "boardGetProjectBoardCards",
"parameters": [
{
"type": "string",
"description": "id of the project",
"name": "projectId",
"in": "path",
"required": true
},
{
"type": "string",
"description": "id of the board",
"name": "boardId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Project board cards"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/projects/{projectId}/boards/{boardId}/cards/{cardId}/move": {
"post": {
"produces": [
"application/json"
],
"tags": [
"board"
],
"summary": "Move project board card to another board",
"operationId": "boardMoveProjectBoardCard",
"parameters": [
{
"type": "string",
"description": "id of the project",
"name": "projectId",
"in": "path",
"required": true
},
{
"type": "string",
"description": "id of the target board",
"name": "boardId",
"in": "path",
"required": true
},
{
"type": "string",
"description": "internal id of the issue card to move",
"name": "cardId",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "Project board card moved"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/issues/search": { "/repos/issues/search": {
"get": { "get": {
"produces": [ "produces": [

View File

@@ -51,6 +51,42 @@ func TestAPIListProjectBoards(t *testing.T) {
assert.Len(t, apiProjectBoards, 4) assert.Len(t, apiProjectBoards, 4)
} }
func TestAPIMoveProjectBoardCard(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue)
link, _ := url.Parse("/api/v1/projects/1/boards/2/cards/1/move")
req := NewRequest(t, "POST", link.String()).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
projectIssue := unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{
ProjectID: 1,
IssueID: 1,
})
assert.EqualValues(t, 2, projectIssue.ProjectBoardID)
}
func TestAPIMoveProjectBoardCardRejectsWrongProjectBoard(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue)
link, _ := url.Parse("/api/v1/projects/1/boards/4/cards/1/move")
req := NewRequest(t, "POST", link.String()).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIMoveProjectBoardCardRejectsCardOutsideProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue)
link, _ := url.Parse("/api/v1/projects/1/boards/2/cards/4/move")
req := NewRequest(t, "POST", link.String()).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIGetProjectBoard(t *testing.T) { func TestAPIGetProjectBoard(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()