improvements

This commit is contained in:
Lunny Xiao
2026-03-21 15:30:24 -07:00
committed by beardev-in
parent a1d1274101
commit d17a2b099a
5 changed files with 240 additions and 76 deletions

View File

@@ -47,8 +47,8 @@ type CreateProjectOption struct {
type EditProjectOption struct { type EditProjectOption struct {
Title *string `json:"title,omitempty"` Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
CardType *int `json:"card_type,omitempty"` // Card type: 0=text_only, 1=images_and_text
IsClosed *bool `json:"is_closed,omitempty"` CardType *int `json:"card_type,omitempty"`
} }
// ProjectColumn represents a project column (board) // ProjectColumn represents a project column (board)
@@ -84,9 +84,11 @@ type EditProjectColumnOption struct {
Sorting *int `json:"sorting,omitempty"` Sorting *int `json:"sorting,omitempty"`
} }
// AddIssueToProjectColumnOption represents options for adding issues to a project
// AddIssueToProjectColumnOption represents options for adding an issue to a project column
// swagger:model // swagger:model
type AddIssueToProjectColumnOption struct { type AddIssueToProjectColumnOption struct {
// required: true // required: true
IssueIDs []int64 `json:"issue_ids" binding:"Required"` IssueIDs []int64 `json:"issue_ids" binding:"Required"`
} }

View File

@@ -1582,6 +1582,8 @@ func Routes() *web.Router {
m.Combo("").Get(repo.GetProject). m.Combo("").Get(repo.GetProject).
Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject). Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject) Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
m.Post("/close", reqToken(), reqRepoWriter(unit.TypeProjects), repo.CloseProject)
m.Post("/reopen", reqToken(), reqRepoWriter(unit.TypeProjects), repo.ReopenProject)
m.Combo("/columns").Get(repo.ListProjectColumns). m.Combo("/columns").Get(repo.ListProjectColumns).
Post(reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived, bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn) Post(reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived, bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
}) })

View File

@@ -10,7 +10,6 @@ import (
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/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
@@ -70,24 +69,13 @@ func ListProjects(ctx *context.APIContext) {
isClosed = optional.Some(false) isClosed = optional.Some(false)
} }
page := ctx.FormInt("page") listOptions := utils.GetListOptions(ctx)
if page <= 0 {
page = 1
}
limit := ctx.FormInt("limit")
if limit <= 0 {
limit = setting.UI.IssuePagingNum
}
projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
ListOptions: db.ListOptions{ ListOptions: listOptions,
Page: page, RepoID: ctx.Repo.Repository.ID,
PageSize: limit, IsClosed: isClosed,
}, Type: project_model.TypeRepository,
RepoID: ctx.Repo.Repository.ID,
IsClosed: isClosed,
Type: project_model.TypeRepository,
}) })
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
@@ -101,7 +89,7 @@ func ListProjects(ctx *context.APIContext) {
apiProjects := convert.ToProjectList(ctx, projects) apiProjects := convert.ToProjectList(ctx, projects)
ctx.SetLinkHeader(count, limit) ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count) ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiProjects) ctx.JSON(http.StatusOK, apiProjects)
} }
@@ -270,16 +258,99 @@ func EditProject(ctx *context.APIContext) {
if form.CardType != nil { if form.CardType != nil {
project.CardType = project_model.CardType(*form.CardType) project.CardType = project_model.CardType(*form.CardType)
} }
if form.IsClosed != nil { if err := project_model.UpdateProject(ctx, project); err != nil {
if err := project_model.ChangeProjectStatus(ctx, project, *form.IsClosed); err != nil { ctx.APIErrorInternal(err)
return
}
if err := project_service.LoadIssueNumbersForProjects(ctx, []*project_model.Project{project}, ctx.Doer); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToProject(ctx, project))
}
// CloseProject closes a project
func CloseProject(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/close repository repoCloseProject
// ---
// summary: Close 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"
changeProjectStatus(ctx, true)
}
// ReopenProject reopens a project
func ReopenProject(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/reopen repository repoReopenProject
// ---
// summary: Reopen 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"
changeProjectStatus(ctx, false)
}
func changeProjectStatus(ctx *context.APIContext, isClosed bool) {
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) ctx.APIErrorInternal(err)
return
}
} else {
if err := project_model.UpdateProject(ctx, project); err != nil {
ctx.APIErrorInternal(err)
return
} }
return
}
if err := project_model.ChangeProjectStatus(ctx, project, isClosed); err != nil {
ctx.APIErrorInternal(err)
return
} }
if err := project_service.LoadIssueNumbersForProjects(ctx, []*project_model.Project{project}, ctx.Doer); err != nil { if err := project_service.LoadIssueNumbersForProjects(ctx, []*project_model.Project{project}, ctx.Doer); err != nil {
@@ -536,7 +607,12 @@ func EditProjectColumn(ctx *context.APIContext) {
column.Color = *form.Color column.Color = *form.Color
} }
if form.Sorting != nil { if form.Sorting != nil {
column.Sorting = int8(*form.Sorting) sorting := int8(*form.Sorting)
if int(sorting) != *form.Sorting {
ctx.APIError(http.StatusUnprocessableEntity, "sorting out of range")
return
}
column.Sorting = sorting
} }
if err := project_model.UpdateColumn(ctx, column); err != nil { if err := project_model.UpdateColumn(ctx, column); err != nil {
@@ -633,14 +709,7 @@ func AddIssueToProjectColumn(ctx *context.APIContext) {
// - name: body // - name: body
// in: body // in: body
// schema: // schema:
// type: object // "$ref": "#/definitions/AddIssueToProjectColumnOption"
// required:
// - issue_id
// properties:
// issue_id:
// type: integer
// format: int64
// description: ID of the issue to add
// responses: // responses:
// "201": // "201":
// "$ref": "#/responses/empty" // "$ref": "#/responses/empty"

View File

@@ -13768,17 +13768,7 @@
"name": "body", "name": "body",
"in": "body", "in": "body",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/AddIssueToProjectColumnOption"
"required": [
"issue_id"
],
"properties": {
"issue_id": {
"description": "ID of the issue to add",
"type": "integer",
"format": "int64"
}
}
} }
} }
], ],
@@ -13936,6 +13926,50 @@
} }
} }
}, },
"/repos/{owner}/{repo}/projects/{id}/close": {
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Close a project",
"operationId": "repoCloseProject",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"format": "int64",
"description": "id of the project",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/Project"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/projects/{id}/columns": { "/repos/{owner}/{repo}/projects/{id}/columns": {
"get": { "get": {
"produces": [ "produces": [
@@ -14047,6 +14081,50 @@
} }
} }
}, },
"/repos/{owner}/{repo}/projects/{id}/reopen": {
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Reopen a project",
"operationId": "repoReopenProject",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"format": "int64",
"description": "id of the project",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/Project"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/pulls": { "/repos/{owner}/{repo}/pulls": {
"get": { "get": {
"produces": [ "produces": [
@@ -25230,11 +25308,6 @@
"type": "string", "type": "string",
"x-go-name": "Description" "x-go-name": "Description"
}, },
"is_closed": {
"description": "Whether the project is closed",
"type": "boolean",
"x-go-name": "IsClosed"
},
"title": { "title": {
"description": "Project title", "description": "Project title",
"type": "string", "type": "string",

View File

@@ -188,27 +188,6 @@ func TestAPIUpdateProject(t *testing.T) {
assert.Equal(t, newTitle, updatedProject.Title) assert.Equal(t, newTitle, updatedProject.Title)
assert.Equal(t, newDesc, updatedProject.Description) assert.Equal(t, newDesc, updatedProject.Description)
// Test closing project
isClosed := true
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID), &api.EditProjectOption{
IsClosed: &isClosed,
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &updatedProject)
assert.True(t, updatedProject.IsClosed)
assert.NotNil(t, updatedProject.ClosedDate)
// Test reopening project
isClosed = false
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID), &api.EditProjectOption{
IsClosed: &isClosed,
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &updatedProject)
assert.False(t, updatedProject.IsClosed)
// Test updating non-existent project // Test updating non-existent project
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/99999", owner.Name, repo.Name), &api.EditProjectOption{ req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/99999", owner.Name, repo.Name), &api.EditProjectOption{
Title: &newTitle, Title: &newTitle,
@@ -216,6 +195,45 @@ func TestAPIUpdateProject(t *testing.T) {
MakeRequest(t, req, http.StatusNotFound) MakeRequest(t, req, http.StatusNotFound)
} }
func TestAPIChangeProjectStatus(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
project := &project_model.Project{
Title: "Project to Close",
Description: "Project to close and reopen",
RepoID: repo.ID,
Type: project_model.TypeRepository,
CreatorID: owner.ID,
TemplateType: project_model.TemplateTypeNone,
}
err := project_model.NewProject(t.Context(), project)
assert.NoError(t, err)
defer func() {
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
}()
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeWriteIssue)
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/projects/%d/close", owner.Name, repo.Name, project.ID).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var updatedProject api.Project
DecodeJSON(t, resp, &updatedProject)
assert.True(t, updatedProject.IsClosed)
assert.NotNil(t, updatedProject.ClosedDate)
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/projects/%d/reopen", owner.Name, repo.Name, project.ID).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &updatedProject)
assert.False(t, updatedProject.IsClosed)
}
func TestAPIDeleteProject(t *testing.T) { func TestAPIDeleteProject(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()