539619be2f
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 24s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 55s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
API endpoints:
- GET/POST/DELETE /api/v1/orgs/{org}/custom-fields — org field definitions
- GET/PUT /api/v1/repos/{owner}/{repo}/metadata — repo-scoped field values
- GET/PUT /api/v1/repos/{owner}/{repo}/issues/{index}/custom-fields — issue values
All endpoints use field names as keys (not IDs) for ergonomic access.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
140 lines
3.8 KiB
Go
140 lines
3.8 KiB
Go
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package org
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
|
)
|
|
|
|
type apiCustomFieldDef struct {
|
|
ID int64 `json:"id"`
|
|
OwnerID int64 `json:"owner_id"`
|
|
Scope string `json:"scope"`
|
|
Name string `json:"name"`
|
|
FieldType string `json:"field_type"`
|
|
Description string `json:"description"`
|
|
Options any `json:"options"`
|
|
Required bool `json:"required"`
|
|
SortOrder int `json:"sort_order"`
|
|
IsActive bool `json:"is_active"`
|
|
}
|
|
|
|
func toAPIFieldDef(f *issues_model.CustomFieldDef) apiCustomFieldDef {
|
|
var opts any
|
|
if f.Options != "" {
|
|
var parsed []string
|
|
if json.Unmarshal([]byte(f.Options), &parsed) == nil {
|
|
opts = parsed
|
|
} else {
|
|
opts = f.Options
|
|
}
|
|
}
|
|
return apiCustomFieldDef{
|
|
ID: f.ID,
|
|
OwnerID: f.OwnerID,
|
|
Scope: string(f.Scope),
|
|
Name: f.Name,
|
|
FieldType: string(f.FieldType),
|
|
Description: f.Description,
|
|
Options: opts,
|
|
Required: f.Required,
|
|
SortOrder: f.SortOrder,
|
|
IsActive: f.IsActive,
|
|
}
|
|
}
|
|
|
|
// ListOrgCustomFields returns all custom field definitions for an org.
|
|
func ListOrgCustomFields(ctx *context.APIContext) {
|
|
fields, err := issues_model.GetAllCustomFieldsByOwner(ctx, ctx.Org.Organization.ID)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
result := make([]apiCustomFieldDef, 0, len(fields))
|
|
for _, f := range fields {
|
|
result = append(result, toAPIFieldDef(f))
|
|
}
|
|
ctx.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// CreateOrgCustomField creates a new custom field definition.
|
|
func CreateOrgCustomField(ctx *context.APIContext) {
|
|
var req struct {
|
|
Scope string `json:"scope" binding:"Required"`
|
|
Name string `json:"name" binding:"Required"`
|
|
FieldType string `json:"field_type" binding:"Required"`
|
|
Description string `json:"description"`
|
|
Options []string `json:"options"`
|
|
Required bool `json:"required"`
|
|
SortOrder int `json:"sort_order"`
|
|
}
|
|
if err := ctx.Req.ParseForm(); err != nil {
|
|
ctx.APIError(http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
|
|
ctx.APIError(http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
if req.Name == "" || req.Scope == "" {
|
|
ctx.APIError(http.StatusBadRequest, "name and scope are required")
|
|
return
|
|
}
|
|
|
|
scope := issues_model.CustomFieldScope(req.Scope)
|
|
if scope != issues_model.CustomFieldScopeIssue && scope != issues_model.CustomFieldScopeRepo {
|
|
ctx.APIError(http.StatusBadRequest, "scope must be 'issue' or 'repo'")
|
|
return
|
|
}
|
|
|
|
var optionsJSON string
|
|
if len(req.Options) > 0 {
|
|
data, _ := json.Marshal(req.Options)
|
|
optionsJSON = string(data)
|
|
}
|
|
|
|
field := &issues_model.CustomFieldDef{
|
|
OwnerID: ctx.Org.Organization.ID,
|
|
RepoID: 0,
|
|
Scope: scope,
|
|
Name: req.Name,
|
|
FieldType: issues_model.CustomFieldType(req.FieldType),
|
|
Description: req.Description,
|
|
Options: optionsJSON,
|
|
Required: req.Required,
|
|
SortOrder: req.SortOrder,
|
|
IsActive: true,
|
|
}
|
|
|
|
if err := issues_model.CreateCustomFieldDef(ctx, field); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusCreated, toAPIFieldDef(field))
|
|
}
|
|
|
|
// DeleteOrgCustomField deletes a custom field definition.
|
|
func DeleteOrgCustomField(ctx *context.APIContext) {
|
|
id := ctx.PathParamInt64("id")
|
|
field, err := issues_model.GetCustomFieldDefByID(ctx, id)
|
|
if err != nil {
|
|
ctx.APIErrorNotFound()
|
|
return
|
|
}
|
|
if field.OwnerID != ctx.Org.Organization.ID {
|
|
ctx.APIErrorNotFound()
|
|
return
|
|
}
|
|
if err := issues_model.DeleteCustomFieldDef(ctx, id); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|