improvements
This commit is contained in:
@@ -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"`
|
||||||
|
// Card type: 0=text_only, 1=images_and_text
|
||||||
CardType *int `json:"card_type,omitempty"`
|
CardType *int `json:"card_type,omitempty"`
|
||||||
IsClosed *bool `json:"is_closed,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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,21 +69,10 @@ 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,
|
|
||||||
PageSize: limit,
|
|
||||||
},
|
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
IsClosed: isClosed,
|
IsClosed: isClosed,
|
||||||
Type: project_model.TypeRepository,
|
Type: project_model.TypeRepository,
|
||||||
@@ -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.ChangeProjectStatus(ctx, project, *form.IsClosed); err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := project_model.UpdateProject(ctx, project); err != nil {
|
if err := project_model.UpdateProject(ctx, project); err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
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)
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
|||||||
105
templates/swagger/v1_json.tmpl
generated
105
templates/swagger/v1_json.tmpl
generated
@@ -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",
|
||||||
|
|||||||
@@ -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)()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user