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:
Jonathan Miller
2026-05-07 19:01:24 -05:00
parent f913d90ab6
commit e43359d566
4 changed files with 928 additions and 0 deletions
+102
View File
@@ -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"`
}
+23
View File
@@ -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))
+766
View File
@@ -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)
}
+37
View File
@@ -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
}