feat(api): add project board REST API for Gitea 1.25.5
Backport of project board API endpoints to the 1.25.5 release branch. Adds full CRUD for projects, columns, and issue cards. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Project represents a project
|
||||
// swagger:model
|
||||
type Project struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
OwnerID int64 `json:"owner_id,omitempty"`
|
||||
RepoID int64 `json:"repo_id,omitempty"`
|
||||
CreatorID int64 `json:"creator_id"`
|
||||
IsClosed bool `json:"is_closed"`
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created_at"`
|
||||
// swagger:strfmt date-time
|
||||
Updated time.Time `json:"updated_at"`
|
||||
// swagger:strfmt date-time
|
||||
Closed *time.Time `json:"closed_at,omitempty"`
|
||||
}
|
||||
|
||||
// CreateProjectOption options for creating a project
|
||||
// swagger:model
|
||||
type CreateProjectOption struct {
|
||||
// required: true
|
||||
Title string `json:"title" binding:"Required"`
|
||||
// Description of the project
|
||||
Description string `json:"description"`
|
||||
// BoardType: 0=none, 1=basic kanban, 2=bug triage
|
||||
BoardType uint8 `json:"board_type"`
|
||||
// CardType: 0=text only, 1=images and text
|
||||
CardType uint8 `json:"card_type"`
|
||||
}
|
||||
|
||||
// EditProjectOption options for editing a project
|
||||
// swagger:model
|
||||
type EditProjectOption struct {
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
||||
// ProjectColumn represents a project column (board)
|
||||
// swagger:model
|
||||
type ProjectColumn struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Sorting int8 `json:"sorting"`
|
||||
Color string `json:"color"`
|
||||
ProjectID int64 `json:"project_id"`
|
||||
Default bool `json:"default"`
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created_at"`
|
||||
// swagger:strfmt date-time
|
||||
Updated time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateProjectColumnOption options for creating a project column
|
||||
// swagger:model
|
||||
type CreateProjectColumnOption struct {
|
||||
// required: true
|
||||
Title string `json:"title" binding:"Required"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
// EditProjectColumnOption options for editing a project column
|
||||
// swagger:model
|
||||
type EditProjectColumnOption struct {
|
||||
Title *string `json:"title"`
|
||||
Color *string `json:"color"`
|
||||
Sorting *int8 `json:"sorting"`
|
||||
}
|
||||
|
||||
// ProjectColumnIssue represents an issue on a project column
|
||||
// swagger:model
|
||||
type ProjectColumnIssue struct {
|
||||
ID int64 `json:"id"`
|
||||
IssueID int64 `json:"issue_id"`
|
||||
ProjectID int64 `json:"project_id"`
|
||||
ColumnID int64 `json:"column_id"`
|
||||
Sorting int64 `json:"sorting"`
|
||||
}
|
||||
|
||||
// AddProjectColumnIssueOption options for adding an issue to a project column
|
||||
// swagger:model
|
||||
type AddProjectColumnIssueOption struct {
|
||||
// required: true
|
||||
IssueID int64 `json:"issue_id" binding:"Required"`
|
||||
}
|
||||
|
||||
// MoveProjectColumnIssueOption options for moving an issue between columns
|
||||
// swagger:model
|
||||
type MoveProjectColumnIssueOption struct {
|
||||
// required: true
|
||||
ColumnID int64 `json:"column_id" binding:"Required"`
|
||||
Sorting int64 `json:"sorting"`
|
||||
}
|
||||
@@ -1602,6 +1602,29 @@ func Routes() *web.Router {
|
||||
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
|
||||
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
|
||||
})
|
||||
m.Group("/projects", func() {
|
||||
m.Combo("").Get(repo.ListProjects).
|
||||
Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectOption{}), repo.CreateProject)
|
||||
m.Group("/{id}", func() {
|
||||
m.Combo("").Get(repo.GetProject).
|
||||
Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
|
||||
Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
|
||||
m.Post("/{action}", reqToken(), reqRepoWriter(unit.TypeProjects), repo.ChangeProjectStatus)
|
||||
m.Group("/columns", func() {
|
||||
m.Combo("").Get(repo.ListProjectColumns).
|
||||
Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
|
||||
m.Group("/{columnId}", func() {
|
||||
m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProjectColumn)
|
||||
m.Combo("/issues").Get(repo.ListProjectColumnIssues).
|
||||
Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.AddProjectColumnIssueOption{}), repo.AddIssueToColumn)
|
||||
})
|
||||
})
|
||||
m.Group("/issues/{issueId}", func() {
|
||||
m.Patch("/move", reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.MoveProjectColumnIssueOption{}), repo.MoveIssueOnColumn)
|
||||
m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.RemoveIssueFromProject)
|
||||
})
|
||||
})
|
||||
})
|
||||
}, repoAssignment(), checkTokenPublicOnly())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
|
||||
|
||||
|
||||
@@ -0,0 +1,766 @@
|
||||
// Copyright 2026 Moko Consulting. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
// ListProjects lists projects for a repository
|
||||
func ListProjects(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/projects project projectListProjects
|
||||
// ---
|
||||
// summary: List a repository's projects
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: state
|
||||
// in: query
|
||||
// description: state of the projects (open, closed, all)
|
||||
// type: string
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ProjectList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
state := ctx.FormString("state")
|
||||
|
||||
opts := project_model.SearchOptions{
|
||||
ListOptions: listOptions,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
}
|
||||
switch state {
|
||||
case "closed":
|
||||
opts.IsClosed = optional.Some(true)
|
||||
case "all":
|
||||
// no filter
|
||||
default:
|
||||
opts.IsClosed = optional.Some(false)
|
||||
}
|
||||
|
||||
projects, total, err := db.FindAndCount[project_model.Project](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIProjectList(projects))
|
||||
}
|
||||
|
||||
// GetProject gets a project by ID
|
||||
func GetProject(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/projects/{id} project projectGetProject
|
||||
// ---
|
||||
// summary: Get a project
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the project
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Project"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIProject(project))
|
||||
}
|
||||
|
||||
// CreateProject creates a new project for a repository
|
||||
func CreateProject(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/projects project projectCreateProject
|
||||
// ---
|
||||
// summary: Create a project
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateProjectOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Project"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateProjectOption)
|
||||
|
||||
project := &project_model.Project{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Title: form.Title,
|
||||
Description: form.Description,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
TemplateType: project_model.TemplateType(form.BoardType),
|
||||
CardType: project_model.CardType(form.CardType),
|
||||
Type: project_model.TypeRepository,
|
||||
}
|
||||
|
||||
if err := project_model.NewProject(ctx, project); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIProject(project))
|
||||
}
|
||||
|
||||
// EditProject edits a project
|
||||
func EditProject(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/projects/{id} project projectEditProject
|
||||
// ---
|
||||
// summary: Edit a project
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditProjectOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Project"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditProjectOption)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if form.Title != nil {
|
||||
project.Title = *form.Title
|
||||
}
|
||||
if form.Description != nil {
|
||||
project.Description = *form.Description
|
||||
}
|
||||
|
||||
if err := project_model.UpdateProject(ctx, project); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIProject(project))
|
||||
}
|
||||
|
||||
// DeleteProject deletes a project
|
||||
func DeleteProject(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/projects/{id} project projectDeleteProject
|
||||
// ---
|
||||
// summary: Delete a project
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err := project_model.DeleteProjectByID(ctx, project.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ChangeProjectStatus closes or reopens a project
|
||||
func ChangeProjectStatus(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/{action} project projectChangeStatus
|
||||
// ---
|
||||
// summary: Close or reopen a project
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: action
|
||||
// in: path
|
||||
// description: action (close or reopen)
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
action := ctx.PathParam("action")
|
||||
isClosed := action == "close"
|
||||
|
||||
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"), isClosed); err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListProjectColumns lists columns for a project
|
||||
func ListProjectColumns(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns project projectListColumns
|
||||
// ---
|
||||
// summary: List a project's columns
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: project id
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ProjectColumnList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
columns, err := project.GetColumns(ctx)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiColumns := make([]*api.ProjectColumn, len(columns))
|
||||
for i, col := range columns {
|
||||
apiColumns[i] = &api.ProjectColumn{
|
||||
ID: col.ID,
|
||||
Title: col.Title,
|
||||
Sorting: col.Sorting,
|
||||
Color: col.Color,
|
||||
ProjectID: col.ProjectID,
|
||||
Default: col.Default,
|
||||
Created: col.CreatedUnix.AsTime(),
|
||||
Updated: col.UpdatedUnix.AsTime(),
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, apiColumns)
|
||||
}
|
||||
|
||||
// CreateProjectColumn creates a new column in a project
|
||||
func CreateProjectColumn(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/columns project projectCreateColumn
|
||||
// ---
|
||||
// summary: Create a column in a project
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: project id
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateProjectColumnOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/ProjectColumn"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateProjectColumnOption)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
column := &project_model.Column{
|
||||
Title: form.Title,
|
||||
Color: form.Color,
|
||||
ProjectID: project.ID,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
}
|
||||
|
||||
if err := project_model.NewColumn(ctx, column); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, &api.ProjectColumn{
|
||||
ID: column.ID,
|
||||
Title: column.Title,
|
||||
Color: column.Color,
|
||||
ProjectID: column.ProjectID,
|
||||
Default: column.Default,
|
||||
Created: column.CreatedUnix.AsTime(),
|
||||
Updated: column.UpdatedUnix.AsTime(),
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteProjectColumn deletes a project column
|
||||
func DeleteProjectColumn(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/projects/{id}/columns/{columnId} project projectDeleteColumn
|
||||
// ---
|
||||
// summary: Delete a project column
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: columnId
|
||||
// in: path
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
columnID := ctx.PathParamInt64("columnId")
|
||||
projectID := ctx.PathParamInt64("id")
|
||||
|
||||
// Verify project belongs to repo
|
||||
if _, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, projectID); err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.DeleteColumnByID(ctx, columnID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListProjectColumnIssues lists issues in a project column
|
||||
func ListProjectColumnIssues(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns/{columnId}/issues project projectListColumnIssues
|
||||
// ---
|
||||
// summary: List issues in a project column
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: columnId
|
||||
// in: path
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ProjectColumnIssueList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
projectID := ctx.PathParamInt64("id")
|
||||
columnID := ctx.PathParamInt64("columnId")
|
||||
|
||||
// Verify project belongs to repo
|
||||
if _, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, projectID); err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
column, err := project_model.GetColumnByIDAndProjectID(ctx, columnID, projectID)
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectColumnNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
issues, err := column.GetIssues(ctx)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiIssues := make([]*api.ProjectColumnIssue, len(issues))
|
||||
for i, issue := range issues {
|
||||
apiIssues[i] = &api.ProjectColumnIssue{
|
||||
ID: issue.ID,
|
||||
IssueID: issue.IssueID,
|
||||
ProjectID: issue.ProjectID,
|
||||
ColumnID: issue.ProjectColumnID,
|
||||
Sorting: issue.Sorting,
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, apiIssues)
|
||||
}
|
||||
|
||||
// AddIssueToColumn adds an issue to a project column
|
||||
func AddIssueToColumn(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/columns/{columnId}/issues project projectAddIssueToColumn
|
||||
// ---
|
||||
// summary: Add an issue to a project column
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: columnId
|
||||
// in: path
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/AddProjectColumnIssueOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/ProjectColumnIssue"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.AddProjectColumnIssueOption)
|
||||
projectID := ctx.PathParamInt64("id")
|
||||
columnID := ctx.PathParamInt64("columnId")
|
||||
|
||||
// Verify project belongs to repo
|
||||
if _, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, projectID); err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get next sorting value
|
||||
sorting, err := project_model.GetColumnIssueNextSorting(ctx, projectID, columnID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
pi := &project_model.ProjectIssue{
|
||||
IssueID: form.IssueID,
|
||||
ProjectID: projectID,
|
||||
ProjectColumnID: columnID,
|
||||
Sorting: sorting,
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).Insert(pi); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, &api.ProjectColumnIssue{
|
||||
ID: pi.ID,
|
||||
IssueID: pi.IssueID,
|
||||
ProjectID: pi.ProjectID,
|
||||
ColumnID: pi.ProjectColumnID,
|
||||
Sorting: pi.Sorting,
|
||||
})
|
||||
}
|
||||
|
||||
// MoveIssueOnColumn moves an issue to a different column
|
||||
func MoveIssueOnColumn(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/projects/{id}/issues/{issueId}/move project projectMoveIssue
|
||||
// ---
|
||||
// summary: Move an issue to a different project column
|
||||
// consumes:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: project id
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: issueId
|
||||
// in: path
|
||||
// description: issue id
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/MoveProjectColumnIssueOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.MoveProjectColumnIssueOption)
|
||||
projectID := ctx.PathParamInt64("id")
|
||||
issueID := ctx.PathParamInt64("issueId")
|
||||
|
||||
// Verify project belongs to repo
|
||||
if _, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, projectID); err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Update the column and sorting
|
||||
_, err := db.GetEngine(ctx).Where("project_id = ? AND issue_id = ?", projectID, issueID).
|
||||
Update(&project_model.ProjectIssue{
|
||||
ProjectColumnID: form.ColumnID,
|
||||
Sorting: form.Sorting,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// RemoveIssueFromProject removes an issue from a project
|
||||
func RemoveIssueFromProject(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/projects/{id}/issues/{issueId} project projectRemoveIssue
|
||||
// ---
|
||||
// summary: Remove an issue from a project
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: issueId
|
||||
// in: path
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
|
||||
projectID := ctx.PathParamInt64("id")
|
||||
issueID := ctx.PathParamInt64("issueId")
|
||||
|
||||
_, err := db.GetEngine(ctx).Where("project_id = ? AND issue_id = ?", projectID, issueID).
|
||||
Delete(&project_model.ProjectIssue{})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package convert
|
||||
|
||||
import (
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
|
||||
// ToAPIProject converts a Project to API format
|
||||
func ToAPIProject(p *project_model.Project) *api.Project {
|
||||
apiProject := &api.Project{
|
||||
ID: p.ID,
|
||||
Title: p.Title,
|
||||
Description: p.Description,
|
||||
OwnerID: p.OwnerID,
|
||||
RepoID: p.RepoID,
|
||||
CreatorID: p.CreatorID,
|
||||
IsClosed: p.IsClosed,
|
||||
Created: p.CreatedUnix.AsTime(),
|
||||
Updated: p.UpdatedUnix.AsTime(),
|
||||
}
|
||||
if p.IsClosed && p.ClosedDateUnix > 0 {
|
||||
apiProject.Closed = p.ClosedDateUnix.AsTimePtr()
|
||||
}
|
||||
return apiProject
|
||||
}
|
||||
|
||||
// ToAPIProjectList converts a list of Projects to API format
|
||||
func ToAPIProjectList(projects []*project_model.Project) []*api.Project {
|
||||
result := make([]*api.Project, len(projects))
|
||||
for i := range projects {
|
||||
result[i] = ToAPIProject(projects[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user