diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 3682191be5..d4a0b38d91 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -115,6 +115,20 @@ type EditIssueOption struct { RemoveDeadline *bool `json:"unset_due_date"` } +// MoveIssuesOption options for moving issues +type MovedIssuesOption struct { + Issues []struct { + IssueID int64 `json:"issueID"` + Sorting int64 `json:"sorting"` + } `json:"issues"` +} + +// UpdateIssuesOption options for updating issues +type UpdateIssuesOption struct { + ProjectID int64 `json:"project_id"` + Issues []int64 `json:"issues"` +} + // EditDeadlineOption options for creating a deadline type EditDeadlineOption struct { // required:true diff --git a/modules/structs/project.go b/modules/structs/project.go new file mode 100644 index 0000000000..02ebd2fe52 --- /dev/null +++ b/modules/structs/project.go @@ -0,0 +1,46 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +// Project represents a project +type Project struct { + ID int64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + TemplateType uint8 `json:"template_type"` + CardType uint8 `json:"card_type"` + OwnerID int64 `json:"owner_id"` + RepoID int64 `json:"repo_id"` + CreatorID int64 `json:"creator_id"` + IsClosed bool `json:"is_closed"` + Type uint8 `json:"type"` + + CreatedUnix int64 `json:"created_unix"` + UpdatedUnix int64 `json:"updated_unix"` + ClosedDateUnix int64 `json:"closed_date_unix"` +} + +// CreateProjectOption options for creating a project +type CreateProjectOption struct { + // required:true + Title string `json:"title" binding:"Required;MaxSize(100)"` + Content string `json:"content"` + TemplateType uint8 `json:"template_type"` + CardType uint8 `json:"card_type"` +} + +// EditProjectOption options for editing a project +type EditProjectOption struct { + Title string `json:"title" binding:"MaxSize(100)"` + Content string `json:"content"` + CardType uint8 `json:"card_type"` +} + +// MoveColumnsOption options for moving columns +type MovedColumnsOption struct { + Columns []struct { + ColumnID int64 `json:"columnID"` + Sorting int64 `json:"sorting"` + } `json:"columns"` +} diff --git a/modules/structs/project_column.go b/modules/structs/project_column.go new file mode 100644 index 0000000000..b57d23bd91 --- /dev/null +++ b/modules/structs/project_column.go @@ -0,0 +1,26 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +// Column represents a project column +type Column struct { + ID int64 `json:"id"` + Title string `json:"title"` + Color string `json:"color"` +} + +// EditProjectColumnOption options for editing a project column +type EditProjectColumnOption struct { + Title string `json:"title" binding:"MaxSize(100)"` + Sorting int8 `json:"sorting"` + Color string `json:"color" binding:"MaxSize(7)"` +} + +// CreateProjectColumnOption options for creating a project column +type CreateProjectColumnOption struct { + // required:true + Title string `json:"title" binding:"Required;MaxSize(100)"` + Sorting int8 `json:"sorting"` + Color string `json:"color" binding:"MaxSize(7)"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index be67ec1695..1db3e18f7c 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -76,6 +76,7 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" + project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -89,6 +90,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/notify" "code.gitea.io/gitea/routers/api/v1/org" "code.gitea.io/gitea/routers/api/v1/packages" + "code.gitea.io/gitea/routers/api/v1/project" "code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/settings" "code.gitea.io/gitea/routers/api/v1/user" @@ -134,6 +136,114 @@ func sudo() func(ctx *context.APIContext) { } } +func projectIDAssignmentAPI() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if ctx.PathParam(":project_id") == "" { + return + } + + projectAssignment(ctx, ctx.PathParamInt64(":project_id")) + } +} + +func projectAssignment(ctx *context.APIContext, projectID int64) { + var ( + owner *user_model.User + err error + ) + + project, err := project_model.GetProjectByID(ctx, projectID) + if err != nil { + ctx.Error(http.StatusNotFound, "GetProjectByID", err) + return + } + + if project.Type == project_model.TypeIndividual || project.Type == project_model.TypeOrganization { + if err := project.LoadOwner(ctx); err != nil { + ctx.Error(http.StatusNotFound, "LoadOwner", err) + return + } + + if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(project.Owner.Name) { + owner = ctx.Doer + } else { + owner = project.Owner + } + + if project.Type == project_model.TypeOrganization { + ctx.Org.Organization = (*organization.Organization)(owner) + } + } else { + if err := project.LoadRepo(ctx); err != nil { + ctx.Error(http.StatusNotFound, "LoadRepo", err) + } + + repo := project.Repo + + if err := repo.LoadOwner(ctx); err != nil { + ctx.Error(http.StatusNotFound, "LoadOwner", err) + return + } + + ctx.Repo.Repository = repo + owner = repo.Owner + + if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID { + taskID := ctx.Data["ActionsTaskID"].(int64) + task, err := actions_model.GetTaskByID(ctx, taskID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "actions_model.GetTaskByID", err) + return + } + if task.RepoID != repo.ID { + ctx.NotFound() + return + } + + if task.IsForkPullRequest { + ctx.Repo.Permission.AccessMode = perm.AccessModeRead + } else { + ctx.Repo.Permission.AccessMode = perm.AccessModeWrite + } + + if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadUnits", err) + return + } + ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode) + } else { + ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + return + } + } + + if !ctx.Repo.Permission.HasAnyUnitAccess() { + ctx.NotFound() + return + } + } + ctx.ContextUser = owner +} + +func columnAssignment() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if ctx.PathParam("column_id") == "" { + return + } + + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":column_id")) + + if err != nil { + ctx.Error(http.StatusNotFound, "GetColumn", err) + } + + projectAssignment(ctx, column.ProjectID) + + } +} + func repoAssignment() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { userName := ctx.PathParam("username") @@ -164,9 +274,14 @@ func repoAssignment() func(ctx *context.APIContext) { return } } + ctx.Repo.Owner = owner ctx.ContextUser = owner + if owner.IsOrganization() { + ctx.Org.Organization = (*organization.Organization)(owner) + } + // Get repository. repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName) if err != nil { @@ -368,6 +483,7 @@ func reqSelfOrAdmin() func(ctx *context.APIContext) { ctx.Error(http.StatusForbidden, "reqSelfOrAdmin", "doer should be the site admin or be same as the contextUser") return } + } } @@ -384,6 +500,9 @@ func reqAdmin() func(ctx *context.APIContext) { // reqRepoWriter user should have a permission to write to a repo, or be a site admin func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { + if ctx.Repo.Repository == nil { + return + } if !ctx.IsUserRepoWriter(unitTypes) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { ctx.Error(http.StatusForbidden, "reqRepoWriter", "user should have a permission to write to a repo") return @@ -403,6 +522,10 @@ func reqRepoBranchWriter(ctx *context.APIContext) { // reqRepoReader user should have specific read permission or be a repo admin or a site admin func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { + if ctx.Repo.Repository == nil { + return + } + if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin") return @@ -539,6 +662,15 @@ func reqWebhooksEnabled() func(ctx *context.APIContext) { } } +func reqProjectOwner() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if ctx.Repo.Repository == nil && ctx.ContextUser.IsIndividual() && ctx.ContextUser != ctx.Doer { + ctx.Error(http.StatusForbidden, "", "must be the project owner") + return + } + } +} + func orgAssignment(args ...bool) func(ctx *context.APIContext) { var ( assignOrg bool @@ -555,22 +687,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { var err error if assignOrg { - ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.PathParam(":org")) - if err != nil { - if organization.IsErrOrgNotExist(err) { - redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam(":org")) - if err == nil { - context.RedirectToUser(ctx.Base, ctx.PathParam(":org"), redirectUserID) - } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.NotFound("GetOrgByName", err) - } else { - ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err) - } - } else { - ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) - } - return - } + getOrganizationByParams(ctx) ctx.ContextUser = ctx.Org.Organization.AsUser() } @@ -588,6 +705,50 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { } } +func getOrganizationByParams(ctx *context.APIContext) { + orgName := ctx.PathParam(":org") + + var err error + + ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName) + if err != nil { + if organization.IsErrOrgNotExist(err) { + redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam(":org")) + if err == nil { + context.RedirectToUser(ctx.Base, ctx.PathParam(":org"), redirectUserID) + } else if user_model.IsErrUserRedirectNotExist(err) { + ctx.NotFound("GetOrgByName", err) + } else { + ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err) + } + } else { + ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) + } + return + } +} + +func mustEnableRepoProjects(ctx *context.APIContext) { + if unit.TypeProjects.UnitGlobalDisabled() { + ctx.NotFound("EnableRepoProjects", nil) + return + } + + if ctx.Repo.Repository != nil { + projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects) + if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) { + ctx.NotFound("MustEnableRepoProjects", nil) + return + } + } +} + +func getAuthenticatedUser(ctx *context.APIContext) { + if ctx.IsSigned { + ctx.ContextUser = ctx.Doer + } +} + func mustEnableIssues(ctx *context.APIContext) { if !ctx.Repo.CanRead(unit.TypeIssues) { if log.IsTrace() { @@ -668,6 +829,10 @@ func mustEnableWiki(ctx *context.APIContext) { } func mustNotBeArchived(ctx *context.APIContext) { + if ctx.Repo.Repository == nil { + return + } + if ctx.Repo.Repository.IsArchived { ctx.Error(http.StatusLocked, "RepoArchived", fmt.Errorf("%s is archived", ctx.Repo.Repository.LogString())) return @@ -811,6 +976,28 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) { } } +func reqUnitAccess(unitType unit.Type, accessMode perm.AccessMode, ignoreGlobal bool) func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + // only check global disabled units when ignoreGlobal is false + if !ignoreGlobal && unitType.UnitGlobalDisabled() { + ctx.NotFound("Repo unit is is disabled: "+unitType.LogString(), nil) + return + } + + if ctx.ContextUser == nil { + ctx.NotFound("ContextUser is nil", nil) + return + } + + if ctx.ContextUser.IsOrganization() { + if ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unitType) < accessMode { + ctx.NotFound("ContextUser is org but doer has no access to unit", nil) + return + } + } + } +} + // Routes registers all v1 APIs routes to web application. func Routes() *web.Router { m := web.NewRouter() @@ -942,6 +1129,52 @@ func Routes() *web.Router { }, context.UserAssignmentAPI(), individualPermsChecker) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser)) + // Projects + m.Group("/orgs/{org}/projects", func() { + m.Get("", reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true), org.GetProjects) + m.Post("", reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), bind(api.CreateProjectOption{}), org.CreateProject) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) + + m.Group("/projects", func() { + m.Group("/{project_id}", func() { + m.Get("", project.GetProject) + m.Get("/columns", project.GetProjectColumns) + + m.Group("", func() { + m.Patch("", bind(api.EditProjectOption{}), project.EditProject) + m.Delete("", project.DeleteProject) + m.Post("/{action:open|close}", project.ChangeProjectStatus) + m.Group("/columns", func() { + m.Post("", bind(api.CreateProjectColumnOption{}), project.AddColumnToProject) + m.Patch("/move", project.MoveColumns) + m.Patch("/{column_id}/move", project.MoveIssues) + }) + }, reqRepoWriter(unit.TypeProjects), mustNotBeArchived, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), reqProjectOwner()) + }) + + m.Group("/columns/{column_id}", func() { + m.Get("", project.GetProjectColumn) + + m.Group("", func() { + m.Patch("", bind(api.EditProjectColumnOption{}), project.EditProjectColumn) + m.Delete("", project.DeleteProjectColumn) + m.Put("/default", project.SetDefaultProjectColumn) + }, reqRepoWriter(unit.TypeProjects), mustNotBeArchived, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), reqProjectOwner()) + }) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), reqToken(), projectIDAssignmentAPI(), columnAssignment(), individualPermsChecker, reqRepoReader(unit.TypeProjects), mustEnableRepoProjects, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true)) + + m.Group("/repos/{username}/{reponame}/projects", func() { + m.Get("", repo.GetProjects) + m.Group("", func() { + m.Post("", bind(api.CreateProjectOption{}), repo.CreateProject) + m.Put("/{type:issues|pulls}", reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.UpdateIssueProject) + }, reqRepoWriter(unit.TypeProjects), mustNotBeArchived) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqToken(), repoAssignment(), individualPermsChecker, reqRepoReader(unit.TypeProjects), mustEnableRepoProjects) + + m.Post("/user/projects", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), getAuthenticatedUser, reqSelfOrAdmin(), bind(api.CreateProjectOption{}), user.CreateProject) + + m.Get("/users/{username}/projects", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), context.UserAssignmentAPI(), individualPermsChecker, user.GetProjects) + // Users (requires user scope) m.Group("/users", func() { m.Group("/{username}", func() { @@ -1476,6 +1709,7 @@ func Routes() *web.Router { m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create) m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization)) m.Group("/orgs/{org}", func() { + m.Combo("").Get(org.Get). Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). Delete(reqToken(), reqOrgOwnership(), org.Delete) diff --git a/routers/api/v1/org/project.go b/routers/api/v1/org/project.go new file mode 100644 index 0000000000..1dbf429bc6 --- /dev/null +++ b/routers/api/v1/org/project.go @@ -0,0 +1,127 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package org + +import ( + "net/http" + "strings" + + "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" +) + +// CreateProject creates a new project for organization +func CreateProject(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/projects project orgCreateProject + // --- + // summary: Create a new project + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: organization name that the project belongs to + // required: true + // type: string + // - name: body + // in: body + // description: Project data + // required: true + // schema: + // "$ref": "#/definitions/CreateProjectOption" + // responses: + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "412": + // "$ref": "#/responses/error" + // "422": + // "$ref": "#/responses/validationError" + // "423": + // "$ref": "#/responses/repoArchivedError" + + form := web.GetForm(ctx).(*api.CreateProjectOption) + + project := &project_model.Project{ + Title: form.Title, + Description: form.Content, + CreatorID: ctx.Doer.ID, + TemplateType: project_model.TemplateType(form.TemplateType), + CardType: project_model.CardType(form.CardType), + Type: project_model.TypeOrganization, + OwnerID: ctx.ContextUser.ID, + } + + if err := project_model.NewProject(ctx, project); err != nil { + ctx.Error(http.StatusInternalServerError, "NewProject", err) + return + } + + ctx.JSON(http.StatusCreated, convert.ToProject(ctx, project)) +} + +// GetProjects returns a list of projects that belong to an organization +func GetProjects(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/projects project orgGetProjects + // --- + // summary: Get a list of projects + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: organization name that the project belongs to + // required: true + // type: string + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + listOptions := utils.GetListOptions(ctx) + sortType := ctx.FormTrim("sort") + + isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" + + searchOptions := project_model.SearchOptions{ + ListOptions: listOptions, + IsClosed: optional.Some(isShowClosed), + OrderBy: project_model.GetSearchOrderByBySortType(sortType), + OwnerID: ctx.ContextUser.ID, + Type: project_model.TypeOrganization, + } + + projects, maxResults, err := db.FindAndCount[project_model.Project](ctx, &searchOptions) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "db.FindAndCount[project_model.Project]", err) + return + } + + ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetTotalCountHeader(maxResults) + ctx.JSON(http.StatusOK, convert.ToProjects(ctx, projects)) +} diff --git a/routers/api/v1/project/project.go b/routers/api/v1/project/project.go new file mode 100644 index 0000000000..47d2d01945 --- /dev/null +++ b/routers/api/v1/project/project.go @@ -0,0 +1,208 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +import ( + "net/http" + + issues_model "code.gitea.io/gitea/models/issues" + project_model "code.gitea.io/gitea/models/project" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +// GetProject returns a project +func GetProject(ctx *context.APIContext) { + // swagger:operation GET /projects/{project_id} project projectGetProject + // --- + // summary: Get a project + // produces: + // - application/json + // parameters: + // - name: project_id + // in: path + // description: project ID + // required: true + // type: integer + // responses: + // "200": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":project_id")) + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + + columns, err := project.GetColumns(ctx) + if err != nil { + ctx.ServerError("GetProjectColumns", err) + return + } + + issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns) + if err != nil { + ctx.ServerError("LoadIssuesOfColumns", err) + return + } + + issues := issues_model.IssueList{} + + for _, column := range columns { + if empty := issuesMap[column.ID]; len(empty) == 0 { + continue + } + issues = append(issues, issuesMap[column.ID]...) + } + + ctx.JSON(http.StatusOK, map[string]any{ + "project": convert.ToProject(ctx, project), + "columns": convert.ToColumns(ctx, columns), + "issues": convert.ToAPIIssueList(ctx, ctx.Doer, issues), + }) +} + +// EditProject edits a project +func EditProject(ctx *context.APIContext) { + // swagger:operation PATCH /projects/{project_id} project projectEditProject + // --- + // summary: Edit a project + // produces: + // - application/json + // parameters: + // - name: project_id + // in: path + // description: project ID + // required: true + // type: integer + // responses: + // "200": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "412": + // "$ref": "#/responses/error" + // "423": + // "$ref": "#/responses/repoArchivedError" + + form := web.GetForm(ctx).(*api.EditProjectOption) + projectID := ctx.PathParamInt64(":project_id") + + project, err := project_model.GetProjectByID(ctx, projectID) + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + + project.Title = form.Title + project.Description = form.Content + project.CardType = project_model.CardType(form.CardType) + + if err = project_model.UpdateProject(ctx, project); err != nil { + ctx.ServerError("UpdateProjects", err) + return + } + + ctx.JSON(http.StatusOK, convert.ToProject(ctx, project)) +} + +// DeleteProject deletes a project +func DeleteProject(ctx *context.APIContext) { + // swagger:operation DELETE /projects/{project_id} project projectDeleteProject + // --- + // summary: Delete a project + // description: Deletes a specific project for a given user and repository. + // parameters: + // - name: project_id + // in: path + // description: project ID + // required: true + // type: integer + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + err := project_model.DeleteProjectByID(ctx, ctx.PathParamInt64(":project_id")) + + if err != nil { + ctx.ServerError("DeleteProjectByID", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// ChangeProjectStatus updates the status of a project between "open" and "close" +func ChangeProjectStatus(ctx *context.APIContext) { + // swagger:operation PATCH /projects/{project_id}/{action} project projectProjectChangeProjectStatus + // --- + // summary: Change the status of a project + // produces: + // - application/json + // parameters: + // - name: project_id + // in: path + // description: project ID + // required: true + // type: integer + // - name: action + // in: path + // description: action to perform (open or close) + // required: true + // type: string + // enum: + // - open + // - close + // responses: + // "200": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + var toClose bool + switch ctx.PathParam(":action") { + case "open": + toClose = false + case "close": + toClose = true + default: + ctx.NotFound("ChangeProjectStatus", nil) + return + } + id := ctx.PathParamInt64(":project_id") + + if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil { + ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) + return + } + + project, err := project_model.GetProjectByID(ctx, id) + + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + + ctx.JSON(http.StatusOK, convert.ToProject(ctx, project)) +} diff --git a/routers/api/v1/project/project_column.go b/routers/api/v1/project/project_column.go new file mode 100644 index 0000000000..cb247bbdbf --- /dev/null +++ b/routers/api/v1/project/project_column.go @@ -0,0 +1,425 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +import ( + "encoding/json" + "errors" + "net/http" + + issues_model "code.gitea.io/gitea/models/issues" + project_model "code.gitea.io/gitea/models/project" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +// GetProjectColumn returns a project column +func GetProjectColumn(ctx *context.APIContext) { + // swagger:operation GET /projects/columns/{column_id} project projectGetProjectColumn + // --- + // summary: Get a project column + // produces: + // - application/json + // parameters: + // - name: column_id + // in: path + // description: column ID + // required: true + // type: integer + // responses: + // "200": + // "$ref": "#/responses/Column" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":column_id")) + + if err != nil { + ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) + return + } + + ctx.JSON(http.StatusOK, convert.ToColumn(ctx, column)) +} + +// GetProjectColumns returns a list of project columns +func GetProjectColumns(ctx *context.APIContext) { + // swagger:operation GET /projects/{project_id}/columns project projectGetProjectColumns + // --- + // summary: Get a list of project columns + // produces: + // - application/json + // parameters: + // - name: project_id + // in: path + // description: project ID + // required: true + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ColumnList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":project_id")) + + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + + columns, err := project.GetColumns(ctx) + + if err != nil { + ctx.ServerError("GetColumnsByProjectID", err) + return + } + + ctx.JSON(http.StatusOK, convert.ToColumns(ctx, columns)) +} + +// AddColumnToProject adds a new column to a project +func AddColumnToProject(ctx *context.APIContext) { + // swagger:operation POST /projects/{project_id}/columns project projectAddColumnToProject + // --- + // summary: Add a column to a project + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: project_id + // in: path + // description: project ID + // required: true + // type: integer + // - name: body + // in: body + // description: column data + // required: true + // schema: + // "$ref": "#/definitions/CreateProjectColumnOption" + // responses: + // "201": + // "$ref": "#/responses/Column" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "412": + // "$ref": "#/responses/error" + // "422": + // "$ref": "#/responses/validationError" + // "423": + // "$ref": "#/responses/repoArchivedError" + + var project *project_model.Project + + projectID := ctx.PathParamInt64(":project_id") + + project, err := project_model.GetProjectByID(ctx, projectID) + + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + + form := web.GetForm(ctx).(*api.CreateProjectColumnOption) + column := &project_model.Column{ + ProjectID: project.ID, + Title: form.Title, + Sorting: form.Sorting, + Color: form.Color, + CreatorID: ctx.Doer.ID, + } + if err := project_model.NewColumn(ctx, column); err != nil { + ctx.ServerError("NewProjectColumn", err) + return + } + + ctx.JSON(http.StatusCreated, convert.ToColumn(ctx, column)) +} + +// EditProjectColumn edits a project column +func EditProjectColumn(ctx *context.APIContext) { + // swagger:operation PATCH /projects/columns/{column_id} project projectEditProjectColumn + // --- + // summary: Edit a project column + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: column_id + // in: path + // description: column ID + // required: true + // type: integer + // - name: body + // in: body + // description: column data + // required: true + // schema: + // "$ref": "#/definitions/EditProjectColumnOption" + // responses: + // "200": + // "$ref": "#/responses/Column" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "412": + // "$ref": "#/responses/error" + // "422": + // "$ref": "#/responses/validationError" + // "423": + // "$ref": "#/responses/repoArchivedError" + + form := web.GetForm(ctx).(*api.EditProjectColumnOption) + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":column_id")) + + if err != nil { + ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) + return + } + + if form.Title != "" { + column.Title = form.Title + } + column.Color = form.Color + if form.Sorting != 0 { + column.Sorting = form.Sorting + } + + if err := project_model.UpdateColumn(ctx, column); err != nil { + ctx.ServerError("UpdateProjectColumn", err) + return + } + + ctx.JSON(http.StatusOK, convert.ToColumn(ctx, column)) +} + +// DeleteProjectColumn deletes a project column +func DeleteProjectColumn(ctx *context.APIContext) { + // swagger:operation DELETE /projects/columns/{column_id} project projectDeleteProjectColumn + // --- + // summary: Delete a project column + // parameters: + // - name: column_id + // in: path + // description: column ID + // required: true + // type: integer + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64(":column_id")); err != nil { + ctx.ServerError("DeleteProjectColumnByID", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// SetDefaultProjectColumn set default column for issues/pulls +func SetDefaultProjectColumn(ctx *context.APIContext) { + // swagger:operation PUT /projects/columns/{column_id}/default project projectSetDefaultProjectColumn + // --- + // summary: Set default column for issues/pulls + // parameters: + // - name: column_id + // in: path + // description: column ID + // required: true + // type: integer + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":column_id")) + if err != nil { + ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) + return + } + + if err := project_model.SetDefaultColumn(ctx, column.ProjectID, column.ID); err != nil { + ctx.ServerError("SetDefaultColumn", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// MoveColumns moves or keeps columns in a project and sorts them inside that project +func MoveColumns(ctx *context.APIContext) { + // swagger:operation PATCH /projects/{project_id}/columns/move project projectMoveColumns + // --- + // summary: Move columns in a project + // consumes: + // - application/json + // parameters: + // - name: project_id + // in: path + // description: project ID + // required: true + // type: integer + // - name: body + // in: body + // description: columns data + // required: true + // schema: + // "$ref": "#/definitions/MovedColumnsOption" + // responses: + // "200": + // "$ref": "#/responses/ColumnList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":project_id")) + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + + form := &api.MovedColumnsOption{} + if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { + ctx.ServerError("DecodeMovedColumnsForm", err) + return + } + + sortedColumnIDs := make(map[int64]int64) + for _, column := range form.Columns { + sortedColumnIDs[column.Sorting] = column.ColumnID + } + + if err = project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil { + ctx.ServerError("MoveColumnsOnProject", err) + return + } + + columns, err := project.GetColumns(ctx) + + if err != nil { + ctx.ServerError("GetColumns", err) + return + } + + ctx.JSON(http.StatusOK, convert.ToColumns(ctx, columns)) +} + +// MoveIssues moves or keeps issues in a column and sorts them inside that column +func MoveIssues(ctx *context.APIContext) { + // swagger:operation PATCH /projects/{project_id}/columns/{column_id}/move project projectMoveIssues + // --- + // summary: Move issues in a column + // consumes: + // - application/json + // parameters: + // - name: project_id + // in: path + // description: project ID + // required: true + // type: integer + // - name: column_id + // in: path + // description: column ID + // required: true + // type: integer + // - name: body + // in: body + // description: issues data + // required: true + // schema: + // "$ref": "#/definitions/MovedIssuesOption" + // responses: + // "200": + // "$ref": "#/responses/IssueList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":project_id")) + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":column_id")) + if err != nil { + ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) + return + } + + form := &api.MovedIssuesOption{} + if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { + ctx.ServerError("DecodeMovedIssuesForm", err) + return + } + + issueIDs := make([]int64, 0, len(form.Issues)) + sortedIssueIDs := make(map[int64]int64) + for _, issue := range form.Issues { + issueIDs = append(issueIDs, issue.IssueID) + sortedIssueIDs[issue.Sorting] = issue.IssueID + } + movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) + if err != nil { + ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err) + return + } + + if len(movedIssues) != len(form.Issues) { + ctx.ServerError("some issues do not exist", errors.New("some issues do not exist")) + return + } + + if _, err = movedIssues.LoadRepositories(ctx); err != nil { + ctx.ServerError("LoadRepositories", err) + return + } + + for _, issue := range movedIssues { + if issue.RepoID != project.RepoID && issue.Repo.OwnerID != project.OwnerID { + ctx.ServerError("Some issue's repoID is not equal to project's repoID", errors.New("some issue's repoID is not equal to project's repoID")) + return + } + } + + if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil { + ctx.ServerError("MoveIssuesOnProjectColumn", err) + return + } + + ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, movedIssues)) +} diff --git a/routers/api/v1/repo/project.go b/routers/api/v1/repo/project.go new file mode 100644 index 0000000000..2d42640eae --- /dev/null +++ b/routers/api/v1/repo/project.go @@ -0,0 +1,255 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "encoding/json" + "errors" + "net/http" + "strings" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/optional" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "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" +) + +// GetProjects returns a list of projects for a given user and repository. +func GetProjects(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{reponame}/projects project repoGetProjects + // --- + // summary: Get a list of projects + // description: Returns a list of projects for a given user and repository. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the project + // required: true + // type: string + // - name: reponame + // in: path + // description: repository name. + // required: true + // type: string + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + listOptions := utils.GetListOptions(ctx) + sortType := ctx.FormTrim("sort") + + isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" + + searchOptions := project_model.SearchOptions{ + ListOptions: listOptions, + IsClosed: optional.Some(isShowClosed), + OrderBy: project_model.GetSearchOrderByBySortType(sortType), + RepoID: ctx.Repo.Repository.ID, + Type: project_model.TypeRepository, + } + + projects, maxResults, err := db.FindAndCount[project_model.Project](ctx, &searchOptions) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "db.FindAndCount[project_model.Project]", err) + return + } + + ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetTotalCountHeader(maxResults) + ctx.JSON(http.StatusOK, convert.ToProjects(ctx, projects)) +} + +// CreateProject creates a new project +func CreateProject(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{reponame}/projects project repoCreateProject + // --- + // summary: Create a new project + // description: Creates a new project for a given user and repository. + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the project + // required: true + // type: string + // - name: reponame + // in: path + // description: repository name. + // required: true + // type: string + // - name: body + // in: body + // description: Project data + // required: true + // schema: + // "$ref": "#/definitions/CreateProjectOption" + // responses: + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "412": + // "$ref": "#/responses/error" + // "422": + // "$ref": "#/responses/validationError" + // "423": + // "$ref": "#/responses/repoArchivedError" + + form := web.GetForm(ctx).(*api.CreateProjectOption) + + project := &project_model.Project{ + Title: form.Title, + Description: form.Content, + CreatorID: ctx.Doer.ID, + TemplateType: project_model.TemplateType(form.TemplateType), + CardType: project_model.CardType(form.CardType), + Type: project_model.TypeRepository, + RepoID: ctx.Repo.Repository.ID, + } + + if err := project_model.NewProject(ctx, project); err != nil { + ctx.Error(http.StatusInternalServerError, "NewProject", err) + return + } + + ctx.JSON(http.StatusCreated, convert.ToProject(ctx, project)) +} + +// UpdateIssueProject change an issue's project to another project in a repository +func UpdateIssueProject(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{reponame}/projects/{type} project repoUpdateIssueProject + // --- + // summary: Change an issue's project + // consumes: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the project + // required: true + // type: string + // - name: reponame + // in: path + // description: repository name. + // required: true + // type: string + // - name: type + // in: path + // description: issue type (issues or pulls) + // required: true + // type: string + // enum: + // - issues + // - pulls + // - name: body + // in: body + // description: issues data + // required: true + // schema: + // "$ref": "#/definitions/UpdateIssuesOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + form := &api.UpdateIssuesOption{} + + if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { + ctx.ServerError("DecodeMovedIssuesForm", err) + return + } + + issues := getActionIssues(ctx, form.Issues) + if ctx.Written() { + return + } + + if err := issues.LoadProjects(ctx); err != nil { + ctx.ServerError("LoadProjects", err) + return + } + if _, err := issues.LoadRepositories(ctx); err != nil { + ctx.ServerError("LoadProjects", err) + return + } + + projectID := form.ProjectID + for _, issue := range issues { + if issue.Project != nil && issue.Project.ID == projectID { + continue + } + if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil { + if errors.Is(err, util.ErrPermissionDenied) { + continue + } + ctx.ServerError("IssueAssignOrRemoveProject", err) + return + } + } + + ctx.Status(http.StatusNoContent) +} + +func getActionIssues(ctx *context.APIContext, issuesIDs []int64) issues_model.IssueList { + + if len(issuesIDs) == 0 { + return nil + } + + issues, err := issues_model.GetIssuesByIDs(ctx, issuesIDs) + if err != nil { + ctx.ServerError("GetIssuesByIDs", err) + return nil + } + + issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues) + prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests) + for _, issue := range issues { + if issue.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect")) + return nil + } + if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled { + ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil) + return nil + } + if err = issue.LoadAttributes(ctx); err != nil { + ctx.ServerError("LoadAttributes", err) + return nil + } + } + return issues +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 1de58632d5..472aea0e16 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -205,4 +205,22 @@ type swaggerParameterBodies struct { // in:body UpdateVariableOption api.UpdateVariableOption + + // in:body + CreateProjectOption api.CreateProjectOption + + // in:body + CreateProjectColumnOption api.CreateProjectColumnOption + + // in:body + EditProjectColumnOption api.EditProjectColumnOption + + // in:body + MovedColumnsOption api.MovedColumnsOption + + // in:body + MovedIssuesOption api.MovedIssuesOption + + // in:body + UpdateIssuesOption api.UpdateIssuesOption } diff --git a/routers/api/v1/swagger/project.go b/routers/api/v1/swagger/project.go new file mode 100644 index 0000000000..c21bb532d8 --- /dev/null +++ b/routers/api/v1/swagger/project.go @@ -0,0 +1,36 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package swagger + +import ( + api "code.gitea.io/gitea/modules/structs" +) + +// Project +// swagger:response Project +type swaggerResponseProject struct { + // in:body + Body api.Project `json:"body"` +} + +// ProjectList +// swagger:response ProjectList +type swaggerResponseProjectList struct { + // in:body + Body []api.Project `json:"body"` +} + +// Column +// swagger:response Column +type swaggerResponseColumn struct { + // in:body + Body api.Column `json:"body"` +} + +// ColumnList +// swagger:response ColumnList +type swaggerResponseColumnList struct { + // in:body + Body []api.Column `json:"body"` +} diff --git a/routers/api/v1/user/project.go b/routers/api/v1/user/project.go new file mode 100644 index 0000000000..f71607e26e --- /dev/null +++ b/routers/api/v1/user/project.go @@ -0,0 +1,122 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "net/http" + "strings" + + "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" +) + +// CreateProject creates a new project for a user +func CreateProject(ctx *context.APIContext) { + // swagger:operation POST /user/projects project userCreateProject + // --- + // summary: Create a new project for user + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // description: Project data + // required: true + // schema: + // "$ref": "#/definitions/CreateProjectOption" + // responses: + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "412": + // "$ref": "#/responses/error" + // "422": + // "$ref": "#/responses/validationError" + // "423": + // "$ref": "#/responses/repoArchivedError" + + form := web.GetForm(ctx).(*api.CreateProjectOption) + + project := &project_model.Project{ + Title: form.Title, + Description: form.Content, + CreatorID: ctx.Doer.ID, + TemplateType: project_model.TemplateType(form.TemplateType), + CardType: project_model.CardType(form.CardType), + Type: project_model.TypeIndividual, + OwnerID: ctx.ContextUser.ID, + } + + if err := project_model.NewProject(ctx, project); err != nil { + ctx.Error(http.StatusInternalServerError, "NewProject", err) + return + } + + ctx.JSON(http.StatusCreated, convert.ToProject(ctx, project)) +} + +// GetProjects returns a list of projects that belong to a user +func GetProjects(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/projects project userGetProjects + // --- + // summary: Get a list of projects + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: owner of the project + // required: true + // type: string + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "423": + // "$ref": "#/responses/repoArchivedError" + + listOptions := utils.GetListOptions(ctx) + sortType := ctx.FormTrim("sort") + + isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" + + searchOptions := project_model.SearchOptions{ + ListOptions: listOptions, + IsClosed: optional.Some(isShowClosed), + OrderBy: project_model.GetSearchOrderByBySortType(sortType), + OwnerID: ctx.ContextUser.ID, + Type: project_model.TypeIndividual, + } + + projects, maxResults, err := db.FindAndCount[project_model.Project](ctx, &searchOptions) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "db.FindAndCount[project_model.Project]", err) + return + } + + ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetTotalCountHeader(maxResults) + ctx.JSON(http.StatusOK, convert.ToProjects(ctx, projects)) +} diff --git a/services/convert/project.go b/services/convert/project.go new file mode 100644 index 0000000000..8f0daa1460 --- /dev/null +++ b/services/convert/project.go @@ -0,0 +1,39 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + + project_model "code.gitea.io/gitea/models/project" + api "code.gitea.io/gitea/modules/structs" +) + +// ToProject converts a models.Project to api.Project +func ToProject(ctx context.Context, project *project_model.Project) *api.Project { + return &api.Project{ + ID: project.ID, + Title: project.Title, + Description: project.Description, + TemplateType: uint8(project.TemplateType), + CardType: uint8(project.CardType), + OwnerID: project.OwnerID, + RepoID: project.RepoID, + CreatorID: project.CreatorID, + IsClosed: project.IsClosed, + Type: uint8(project.Type), + CreatedUnix: int64(project.CreatedUnix), + UpdatedUnix: int64(project.UpdatedUnix), + ClosedDateUnix: int64(project.ClosedDateUnix), + } +} + +// ToProjects converts a slice of models.Project to a slice of api.Project +func ToProjects(ctx context.Context, projects []*project_model.Project) []*api.Project { + result := make([]*api.Project, len(projects)) + for i, project := range projects { + result[i] = ToProject(ctx, project) + } + return result +} diff --git a/services/convert/project_column.go b/services/convert/project_column.go new file mode 100644 index 0000000000..bcd9632c01 --- /dev/null +++ b/services/convert/project_column.go @@ -0,0 +1,36 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + + column_model "code.gitea.io/gitea/models/project" + api "code.gitea.io/gitea/modules/structs" +) + +// ToProject converts a models.Project to api.Project +func ToColumn(ctx context.Context, column *column_model.Column) *api.Column { + if column == nil { + return nil + } + + return &api.Column{ + ID: column.ID, + Title: column.Title, + Color: column.Color, + } +} + +func ToColumns(ctx context.Context, columns column_model.ColumnList) []*api.Column { + if columns == nil { + return nil + } + + var apiColumns []*api.Column + for _, column := range columns { + apiColumns = append(apiColumns, ToColumn(ctx, column)) + } + return apiColumns +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 52d3754737..b83f19b9ee 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2831,6 +2831,101 @@ } } }, + "/orgs/{org}/projects": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Get a list of projects", + "operationId": "orgGetProjects", + "parameters": [ + { + "type": "string", + "description": "organization name that the project belongs to", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Create a new project", + "operationId": "orgCreateProject", + "parameters": [ + { + "type": "string", + "description": "organization name that the project belongs to", + "name": "org", + "in": "path", + "required": true + }, + { + "description": "Project data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateProjectOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "412": { + "$ref": "#/responses/error" + }, + "422": { + "$ref": "#/responses/validationError" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, "/orgs/{org}/public_members": { "get": { "produces": [ @@ -3432,6 +3527,483 @@ } } }, + "/projects/columns/{column_id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Get a project column", + "operationId": "projectGetProjectColumn", + "parameters": [ + { + "type": "integer", + "description": "column ID", + "name": "column_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Column" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + }, + "delete": { + "tags": [ + "project" + ], + "summary": "Delete a project column", + "operationId": "projectDeleteProjectColumn", + "parameters": [ + { + "type": "integer", + "description": "column ID", + "name": "column_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Edit a project column", + "operationId": "projectEditProjectColumn", + "parameters": [ + { + "type": "integer", + "description": "column ID", + "name": "column_id", + "in": "path", + "required": true + }, + { + "description": "column data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/EditProjectColumnOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/Column" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "412": { + "$ref": "#/responses/error" + }, + "422": { + "$ref": "#/responses/validationError" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, + "/projects/columns/{column_id}/default": { + "put": { + "tags": [ + "project" + ], + "summary": "Set default column for issues/pulls", + "operationId": "projectSetDefaultProjectColumn", + "parameters": [ + { + "type": "integer", + "description": "column ID", + "name": "column_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, + "/projects/{project_id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Get a project", + "operationId": "projectGetProject", + "parameters": [ + { + "type": "integer", + "description": "project ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + }, + "delete": { + "description": "Deletes a specific project for a given user and repository.", + "tags": [ + "project" + ], + "summary": "Delete a project", + "operationId": "projectDeleteProject", + "parameters": [ + { + "type": "integer", + "description": "project ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + }, + "patch": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Edit a project", + "operationId": "projectEditProject", + "parameters": [ + { + "type": "integer", + "description": "project ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "412": { + "$ref": "#/responses/error" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, + "/projects/{project_id}/columns": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Get a list of project columns", + "operationId": "projectGetProjectColumns", + "parameters": [ + { + "type": "integer", + "description": "project ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ColumnList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Add a column to a project", + "operationId": "projectAddColumnToProject", + "parameters": [ + { + "type": "integer", + "description": "project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "column data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateProjectColumnOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Column" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "412": { + "$ref": "#/responses/error" + }, + "422": { + "$ref": "#/responses/validationError" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, + "/projects/{project_id}/columns/move": { + "patch": { + "consumes": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Move columns in a project", + "operationId": "projectMoveColumns", + "parameters": [ + { + "type": "integer", + "description": "project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "columns data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MovedColumnsOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/ColumnList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, + "/projects/{project_id}/columns/{column_id}/move": { + "patch": { + "consumes": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Move issues in a column", + "operationId": "projectMoveIssues", + "parameters": [ + { + "type": "integer", + "description": "project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "column ID", + "name": "column_id", + "in": "path", + "required": true + }, + { + "description": "issues data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MovedIssuesOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/IssueList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, + "/projects/{project_id}/{action}": { + "patch": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Change the status of a project", + "operationId": "projectProjectChangeProjectStatus", + "parameters": [ + { + "type": "integer", + "description": "project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "enum": [ + "open", + "close" + ], + "type": "string", + "description": "action to perform (open or close)", + "name": "action", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, "/repos/issues/search": { "get": { "produces": [ @@ -3722,6 +4294,179 @@ } } }, + "/repos/{owner}/{reponame}/projects": { + "get": { + "description": "Returns a list of projects for a given user and repository.", + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Get a list of projects", + "operationId": "repoGetProjects", + "parameters": [ + { + "type": "string", + "description": "owner of the project", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "repository name.", + "name": "reponame", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + }, + "post": { + "description": "Creates a new project for a given user and repository.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Create a new project", + "operationId": "repoCreateProject", + "parameters": [ + { + "type": "string", + "description": "owner of the project", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "repository name.", + "name": "reponame", + "in": "path", + "required": true + }, + { + "description": "Project data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateProjectOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "412": { + "$ref": "#/responses/error" + }, + "422": { + "$ref": "#/responses/validationError" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, + "/repos/{owner}/{reponame}/projects/{type}": { + "put": { + "consumes": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Change an issue's project", + "operationId": "repoUpdateIssueProject", + "parameters": [ + { + "type": "string", + "description": "owner of the project", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "repository name.", + "name": "reponame", + "in": "path", + "required": true + }, + { + "enum": [ + "issues", + "pulls" + ], + "type": "string", + "description": "issue type (issues or pulls)", + "name": "type", + "in": "path", + "required": true + }, + { + "description": "issues data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateIssuesOption" + } + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, "/repos/{owner}/{repo}": { "get": { "produces": [ @@ -17121,6 +17866,49 @@ } } }, + "/user/projects": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Create a new project for user", + "operationId": "userCreateProject", + "parameters": [ + { + "description": "Project data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateProjectOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "412": { + "$ref": "#/responses/error" + }, + "422": { + "$ref": "#/responses/validationError" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, "/user/repos": { "get": { "produces": [ @@ -17953,6 +18741,53 @@ } } }, + "/users/{username}/projects": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Get a list of projects", + "operationId": "userGetProjects", + "parameters": [ + { + "type": "string", + "description": "owner of the project", + "name": "username", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, "/users/{username}/repos": { "get": { "produces": [ @@ -18976,6 +19811,26 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "Column": { + "description": "Column represents a project column", + "type": "object", + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "CombinedStatus": { "description": "CombinedStatus holds the combined state of several statuses for a single commit", "type": "object", @@ -19960,6 +20815,57 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "CreateProjectColumnOption": { + "description": "CreateProjectColumnOption options for creating a project column", + "type": "object", + "required": [ + "title" + ], + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "sorting": { + "type": "integer", + "format": "int8", + "x-go-name": "Sorting" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "CreateProjectOption": { + "description": "CreateProjectOption options for creating a project", + "type": "object", + "required": [ + "title" + ], + "properties": { + "card_type": { + "type": "integer", + "format": "uint8", + "x-go-name": "CardType" + }, + "content": { + "type": "string", + "x-go-name": "Content" + }, + "template_type": { + "type": "integer", + "format": "uint8", + "x-go-name": "TemplateType" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "CreatePullRequestOption": { "description": "CreatePullRequestOption options when creating a pull request", "type": "object", @@ -20954,6 +21860,26 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "EditProjectColumnOption": { + "description": "EditProjectColumnOption options for editing a project column", + "type": "object", + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "sorting": { + "type": "integer", + "format": "int8", + "x-go-name": "Sorting" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "EditPullRequestOption": { "description": "EditPullRequestOption options when modify pull request", "type": "object", @@ -22752,6 +23678,58 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "MovedColumnsOption": { + "description": "MoveColumnsOption options for moving columns", + "type": "object", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "columnID": { + "type": "integer", + "format": "int64", + "x-go-name": "ColumnID" + }, + "sorting": { + "type": "integer", + "format": "int64", + "x-go-name": "Sorting" + } + } + }, + "x-go-name": "Columns" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "MovedIssuesOption": { + "description": "MoveIssuesOption options for moving issues", + "type": "object", + "properties": { + "issues": { + "type": "array", + "items": { + "type": "object", + "properties": { + "issueID": { + "type": "integer", + "format": "int64", + "x-go-name": "IssueID" + }, + "sorting": { + "type": "integer", + "format": "int64", + "x-go-name": "Sorting" + } + } + }, + "x-go-name": "Issues" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "NewIssuePinsAllowed": { "description": "NewIssuePinsAllowed represents an API response that says if new Issue Pins are allowed", "type": "object", @@ -23337,6 +24315,75 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "Project": { + "description": "Project represents a project", + "type": "object", + "properties": { + "card_type": { + "type": "integer", + "format": "uint8", + "x-go-name": "CardType" + }, + "closed_date_unix": { + "type": "integer", + "format": "int64", + "x-go-name": "ClosedDateUnix" + }, + "created_unix": { + "type": "integer", + "format": "int64", + "x-go-name": "CreatedUnix" + }, + "creator_id": { + "type": "integer", + "format": "int64", + "x-go-name": "CreatorID" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "is_closed": { + "type": "boolean", + "x-go-name": "IsClosed" + }, + "owner_id": { + "type": "integer", + "format": "int64", + "x-go-name": "OwnerID" + }, + "repo_id": { + "type": "integer", + "format": "int64", + "x-go-name": "RepoID" + }, + "template_type": { + "type": "integer", + "format": "uint8", + "x-go-name": "TemplateType" + }, + "title": { + "type": "string", + "x-go-name": "Title" + }, + "type": { + "type": "integer", + "format": "uint8", + "x-go-name": "Type" + }, + "updated_unix": { + "type": "integer", + "format": "int64", + "x-go-name": "UpdatedUnix" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "PublicKey": { "description": "PublicKey publickey is a user key to push code to repository", "type": "object", @@ -24818,6 +25865,26 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UpdateIssuesOption": { + "description": "UpdateIssuesOption options for updating issues", + "type": "object", + "properties": { + "issues": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + }, + "x-go-name": "Issues" + }, + "project_id": { + "type": "integer", + "format": "int64", + "x-go-name": "ProjectID" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "UpdateRepoAvatarOption": { "description": "UpdateRepoAvatarUserOption options when updating the repo avatar", "type": "object", @@ -25370,6 +26437,21 @@ } } }, + "Column": { + "description": "Column", + "schema": { + "$ref": "#/definitions/Column" + } + }, + "ColumnList": { + "description": "ColumnList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Column" + } + } + }, "CombinedStatus": { "description": "CombinedStatus", "schema": { @@ -25829,6 +26911,21 @@ } } }, + "Project": { + "description": "Project", + "schema": { + "$ref": "#/definitions/Project" + } + }, + "ProjectList": { + "description": "ProjectList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Project" + } + } + }, "PublicKey": { "description": "PublicKey", "schema": { @@ -26254,7 +27351,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/UpdateVariableOption" + "$ref": "#/definitions/UpdateIssuesOption" } }, "redirect": {