This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
project_model "gitea.dev/models/project"
|
||||
"gitea.dev/modules/optional"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
project_service "gitea.dev/services/projects"
|
||||
)
|
||||
|
||||
func getRepoProject(ctx *context.APIContext) *project_model.Project {
|
||||
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return project
|
||||
}
|
||||
|
||||
func getProjectColumn(ctx *context.APIContext, projectID int64) *project_model.Column {
|
||||
column, err := project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("column_id"), projectID)
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectColumnNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return column
|
||||
}
|
||||
|
||||
// ListProjects list a repository's projects
|
||||
func ListProjects(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/projects project repoListProjects
|
||||
// ---
|
||||
// summary: List a repository's projects
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: repo
|
||||
// in: path
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: state
|
||||
// in: query
|
||||
// type: string
|
||||
// enum: [open, closed, all]
|
||||
// default: open
|
||||
// - name: page
|
||||
// in: query
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ProjectList"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "500":
|
||||
// "$ref": "#/responses/internalServerError"
|
||||
|
||||
state := ctx.FormString("state")
|
||||
isClosed := optional.Some(false)
|
||||
switch state {
|
||||
case "", "open":
|
||||
case "closed":
|
||||
isClosed = optional.Some(true)
|
||||
case "all":
|
||||
isClosed = optional.None[bool]()
|
||||
default:
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "state must be one of open, closed, or all")
|
||||
return
|
||||
}
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: listOptions,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsClosed: isClosed,
|
||||
OrderBy: db.SearchOrderByNewest,
|
||||
Type: project_model.TypeRepository,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.SetLinkHeader(count, listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIProjectList(projects))
|
||||
}
|
||||
|
||||
// ListProjectColumns list a repository project's columns
|
||||
func ListProjectColumns(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns project repoListProjectColumns
|
||||
// ---
|
||||
// summary: List a repository project's columns
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: repo
|
||||
// in: path
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: id
|
||||
// in: path
|
||||
// required: true
|
||||
// type: integer
|
||||
// format: int64
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ProjectColumnList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "500":
|
||||
// "$ref": "#/responses/internalServerError"
|
||||
|
||||
project := getRepoProject(ctx)
|
||||
if project == nil {
|
||||
return
|
||||
}
|
||||
columns, err := project.GetColumns(ctx)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.SetTotalCountHeader(int64(len(columns)))
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIProjectColumnList(columns))
|
||||
}
|
||||
|
||||
// ListProjectColumnIssues list issues in a repository project column
|
||||
func ListProjectColumnIssues(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues project repoListProjectColumnIssues
|
||||
// ---
|
||||
// summary: List issues in a repository project column
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: repo
|
||||
// in: path
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: id
|
||||
// in: path
|
||||
// required: true
|
||||
// type: integer
|
||||
// format: int64
|
||||
// - name: column_id
|
||||
// in: path
|
||||
// required: true
|
||||
// type: integer
|
||||
// format: int64
|
||||
// - name: page
|
||||
// in: query
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/IssueList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "500":
|
||||
// "$ref": "#/responses/internalServerError"
|
||||
|
||||
project := getRepoProject(ctx)
|
||||
if project == nil {
|
||||
return
|
||||
}
|
||||
column := getProjectColumn(ctx, project.ID)
|
||||
if column == nil {
|
||||
return
|
||||
}
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
issueOptions := &issues_model.IssuesOptions{
|
||||
Paginator: &listOptions,
|
||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
ProjectIDs: []int64{project.ID},
|
||||
ProjectColumnID: column.ID,
|
||||
ProjectColumnOrUnassigned: column.Default,
|
||||
SortType: "project-column-sorting",
|
||||
Doer: ctx.Doer,
|
||||
}
|
||||
total, err := issues_model.CountIssues(ctx, issueOptions)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
issues, err := issues_model.Issues(ctx, issueOptions)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.SetLinkHeader(total, listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
|
||||
}
|
||||
|
||||
// MoveProjectIssue moves an issue to another repository project column
|
||||
func MoveProjectIssue(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/issues/{issue_id}/move project repoMoveProjectIssue
|
||||
// ---
|
||||
// summary: Move an issue to another repository project column
|
||||
// consumes:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: repo
|
||||
// in: path
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: id
|
||||
// in: path
|
||||
// required: true
|
||||
// type: integer
|
||||
// format: int64
|
||||
// - name: issue_id
|
||||
// in: path
|
||||
// required: true
|
||||
// type: integer
|
||||
// format: int64
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/MoveProjectIssueOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// description: issue moved
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "423":
|
||||
// "$ref": "#/responses/locked"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "500":
|
||||
// "$ref": "#/responses/internalServerError"
|
||||
|
||||
project := getRepoProject(ctx)
|
||||
if project == nil {
|
||||
return
|
||||
}
|
||||
if project.IsClosed {
|
||||
ctx.APIError(http.StatusForbidden, "project is closed")
|
||||
return
|
||||
}
|
||||
form := web.GetForm(ctx).(*api.MoveProjectIssueOption)
|
||||
column, err := project_model.GetColumnByIDAndProjectID(ctx, form.ColumnID, project.ID)
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectColumnNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
issueID := ctx.PathParamInt64("issue_id")
|
||||
if _, err := issues_model.GetIssueByRepoID(ctx, ctx.Repo.Repository.ID, issueID); err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := project_service.MoveIssueOnProjectColumn(ctx, ctx.Doer, column, issueID, form.Sorting); err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
Reference in New Issue
Block a user