Address remaining review feedback

- Use ParseIssueFilterStateIsClosed for ListProjects state parsing
- Add SortTypeProjectColumnSorting const, replace magic string
- Use GetIssueByRepoID and dedupe Add/Remove issue handlers
- Migrate Column.Sorting from int8 to int (drops 127-column limit, allows
  the API to expose a normal int without truncation)
- Introduce project_service.UpdateProject with optional.Option fields,
  use it from the API EditProject handler

Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
This commit is contained in:
silverwind
2026-04-27 12:08:29 +02:00
committed by beardev-in
parent 795c6bc944
commit 45832f4d68
12 changed files with 229 additions and 105 deletions

View File

@@ -13,6 +13,7 @@ import (
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/routers/common"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
project_service "code.gitea.io/gitea/services/projects"
@@ -97,18 +98,7 @@ func ListProjects(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
state := ctx.FormTrim("state")
var isClosed optional.Option[bool]
switch state {
case "closed":
isClosed = optional.Some(true)
case "open":
isClosed = optional.Some(false)
case "all":
isClosed = optional.None[bool]()
default:
isClosed = optional.Some(false)
}
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormTrim("state"))
listOptions := utils.GetListOptions(ctx)
@@ -275,30 +265,21 @@ func EditProject(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditProjectOption)
if form.Title != nil {
project.Title = *form.Title
}
if form.Description != nil {
project.Description = *form.Description
opts := project_service.UpdateProjectOptions{
Title: optional.FromPtr(form.Title),
Description: optional.FromPtr(form.Description),
}
if form.CardType != nil {
project.CardType = project_model.CardType(*form.CardType)
opts.CardType = optional.Some(project_model.CardType(*form.CardType))
}
if err := project_model.UpdateProject(ctx, project); err != nil {
if form.State != nil {
opts.IsClosed = optional.Some(*form.State == string(api.StateClosed))
}
if err := project_service.UpdateProject(ctx, project, opts); err != nil {
ctx.APIErrorInternal(err)
return
}
if form.State != nil {
isClosed := *form.State == string(api.StateClosed)
if isClosed != project.IsClosed {
if err := project_model.ChangeProjectStatus(ctx, project, isClosed); err != nil {
ctx.APIErrorInternal(err)
return
}
}
}
if err := project_service.LoadIssueNumbersForProject(ctx, project, ctx.Doer); err != nil {
ctx.APIErrorInternal(err)
return
@@ -527,12 +508,7 @@ func EditProjectColumn(ctx *context.APIContext) {
column.Color = *form.Color
}
if form.Sorting != nil {
sorting := int8(*form.Sorting)
if int(sorting) != *form.Sorting {
ctx.APIError(http.StatusUnprocessableEntity, "sorting out of range")
return
}
column.Sorting = sorting
column.Sorting = *form.Sorting
}
if err := project_model.UpdateColumn(ctx, column); err != nil {
@@ -645,7 +621,7 @@ func ListProjectColumnIssues(ctx *context.APIContext) {
RepoIDs: []int64{ctx.Repo.Repository.ID},
ProjectID: column.ProjectID,
ProjectColumnID: column.ID,
SortType: "project-column-sorting",
SortType: issues_model.SortTypeProjectColumnSorting,
}
count, err := issues_model.CountIssues(ctx, issuesOpts)
@@ -713,32 +689,7 @@ func AddIssueToProjectColumn(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
column := getRepoProjectColumn(ctx)
if ctx.Written() {
return
}
issue, err := issues_model.GetIssueByID(ctx, ctx.PathParamInt64("issue_id"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, "issue not found")
} else {
ctx.APIErrorInternal(err)
}
return
}
if issue.RepoID != ctx.Repo.Repository.ID {
ctx.APIError(http.StatusUnprocessableEntity, "issue does not belong to this repository")
return
}
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, column.ProjectID, column.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusCreated)
assignIssueToProjectColumn(ctx, true)
}
// RemoveIssueFromProjectColumn remove an issue from a project column
@@ -789,31 +740,39 @@ func RemoveIssueFromProjectColumn(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
assignIssueToProjectColumn(ctx, false)
}
// assignIssueToProjectColumn assigns an issue to a project column when add is true,
// or removes the issue from any project assignment when add is false.
func assignIssueToProjectColumn(ctx *context.APIContext, add bool) {
column := getRepoProjectColumn(ctx)
if ctx.Written() {
return
}
issue, err := issues_model.GetIssueByID(ctx, ctx.PathParamInt64("issue_id"))
issue, err := issues_model.GetIssueByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("issue_id"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, "issue not found")
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if issue.RepoID != ctx.Repo.Repository.ID {
ctx.APIError(http.StatusUnprocessableEntity, "issue does not belong to this repository")
return
projectID := int64(0)
if add {
projectID = column.ProjectID
}
// 0 means remove
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, 0, column.ID); err != nil {
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, column.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
if add {
ctx.Status(http.StatusCreated)
} else {
ctx.Status(http.StatusNoContent)
}
}