diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8d3c19a033..a099fae2a9 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1564,6 +1564,7 @@ func Routes() *web.Route { Get(projects.ListProjectBoards). Post(bind(api.NewProjectBoardPayload{}), projects.CreateProjectBoard) m.Get("/boards/{boardId}/cards", projects.ListProjectBoardCards) + m.Post("/boards/{boardId}/cards/{cardId}/move", projects.MoveProjectBoardCard) }) m.Group("/boards", func() { diff --git a/routers/api/v1/projects/boards.go b/routers/api/v1/projects/boards.go index afbdfb3fc2..818a5c6d9d 100644 --- a/routers/api/v1/projects/boards.go +++ b/routers/api/v1/projects/boards.go @@ -6,6 +6,7 @@ package projects import ( "net/http" + "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" project_model "code.gitea.io/gitea/models/project" "code.gitea.io/gitea/modules/context" @@ -144,6 +145,81 @@ func ListProjectBoardCards(ctx *context.APIContext) { 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) { // swagger:operation POST /projects/{projectId}/boards board boardCreateProjectBoard // --- diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 0ce35da96a..e1f68413be 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -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": { "get": { "produces": [ diff --git a/tests/integration/api_project_board_test.go b/tests/integration/api_project_board_test.go index fb499e28f0..176689e10f 100644 --- a/tests/integration/api_project_board_test.go +++ b/tests/integration/api_project_board_test.go @@ -51,6 +51,42 @@ func TestAPIListProjectBoards(t *testing.T) { 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) { defer tests.PrepareTestEnv(t)()