6bd9548b2a
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
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 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
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 23s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m4s
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
- CustomFieldDef now has owner_id (org) and scope (issue/repo) - Issue sidebar loads fields by org owner_id, not repo_id - Org Settings > Custom Fields page for managing field definitions - Repo Settings > Metadata page for filling in repo-scoped values - Migration v345 adds owner_id, scope, entity_id, entity_type columns - Per-repo custom field management replaced by org-level - Replaces .mokogitea/manifest.xml with database-backed metadata Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
217 lines
9.3 KiB
Go
217 lines
9.3 KiB
Go
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package issues
|
|
|
|
import (
|
|
"context"
|
|
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
|
)
|
|
|
|
func init() {
|
|
db.RegisterModel(new(CustomFieldDef))
|
|
db.RegisterModel(new(CustomFieldValue))
|
|
}
|
|
|
|
// CustomFieldType represents the data type of a custom field.
|
|
type CustomFieldType string
|
|
|
|
const (
|
|
CustomFieldTypeText CustomFieldType = "text"
|
|
CustomFieldTypeNumber CustomFieldType = "number"
|
|
CustomFieldTypeDate CustomFieldType = "date"
|
|
CustomFieldTypeDropdown CustomFieldType = "dropdown"
|
|
CustomFieldTypeCheckbox CustomFieldType = "checkbox"
|
|
CustomFieldTypeURL CustomFieldType = "url"
|
|
)
|
|
|
|
// CustomFieldScope determines where the field appears.
|
|
type CustomFieldScope string
|
|
|
|
const (
|
|
CustomFieldScopeIssue CustomFieldScope = "issue" // appears in issue sidebar
|
|
CustomFieldScopeRepo CustomFieldScope = "repo" // appears in repo settings metadata
|
|
)
|
|
|
|
// CustomFieldDef defines a custom field at the org level.
|
|
// owner_id = org ID, scope = issue or repo.
|
|
// repo_id is kept for backward compat but 0 for org-level definitions.
|
|
type CustomFieldDef struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
OwnerID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'owner_id'"` // org that owns this field
|
|
RepoID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'repo_id'"` // 0 = org-level (inherited by all repos)
|
|
Scope CustomFieldScope `xorm:"VARCHAR(10) NOT NULL DEFAULT 'issue' 'scope'"`
|
|
Name string `xorm:"NOT NULL"`
|
|
FieldType CustomFieldType `xorm:"VARCHAR(20) NOT NULL 'field_type'"`
|
|
Description string `xorm:"TEXT"`
|
|
Options string `xorm:"TEXT"` // JSON array for dropdown options
|
|
Required bool `xorm:"NOT NULL DEFAULT false"`
|
|
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
|
|
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
|
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
|
|
}
|
|
|
|
func (CustomFieldDef) TableName() string {
|
|
return "custom_field_def"
|
|
}
|
|
|
|
// CustomFieldValue stores a custom field value for an entity (issue or repo).
|
|
type CustomFieldValue struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
EntityID int64 `xorm:"INDEX NOT NULL 'entity_id'"` // issue ID or repo ID
|
|
EntityType string `xorm:"VARCHAR(10) NOT NULL DEFAULT 'issue' 'entity_type'"` // "issue" or "repo"
|
|
FieldID int64 `xorm:"INDEX NOT NULL 'field_id'"`
|
|
Value string `xorm:"TEXT"`
|
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
|
|
}
|
|
|
|
func (CustomFieldValue) TableName() string {
|
|
return "custom_field_value"
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Queries for org-level field definitions
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// GetCustomFieldsByOwner returns all active field definitions for an org with a given scope.
|
|
func GetCustomFieldsByOwner(ctx context.Context, ownerID int64, scope CustomFieldScope) ([]*CustomFieldDef, error) {
|
|
fields := make([]*CustomFieldDef, 0, 10)
|
|
return fields, db.GetEngine(ctx).
|
|
Where("owner_id = ? AND scope = ? AND is_active = ?", ownerID, scope, true).
|
|
OrderBy("sort_order ASC, id ASC").
|
|
Find(&fields)
|
|
}
|
|
|
|
// GetAllCustomFieldsByOwner returns all field definitions for an org (including inactive).
|
|
func GetAllCustomFieldsByOwner(ctx context.Context, ownerID int64) ([]*CustomFieldDef, error) {
|
|
fields := make([]*CustomFieldDef, 0, 10)
|
|
return fields, db.GetEngine(ctx).
|
|
Where("owner_id = ?", ownerID).
|
|
OrderBy("scope ASC, sort_order ASC, id ASC").
|
|
Find(&fields)
|
|
}
|
|
|
|
// GetCustomFieldsByOwnerAndScope returns all fields for an org filtered by scope.
|
|
func GetCustomFieldsByOwnerAndScope(ctx context.Context, ownerID int64, scope CustomFieldScope) ([]*CustomFieldDef, error) {
|
|
fields := make([]*CustomFieldDef, 0, 10)
|
|
return fields, db.GetEngine(ctx).
|
|
Where("owner_id = ? AND scope = ?", ownerID, scope).
|
|
OrderBy("sort_order ASC, id ASC").
|
|
Find(&fields)
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Backward-compatible queries (load by repo's owner)
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// GetCustomFieldsByRepo returns active issue-scoped fields for a repo's org.
|
|
// This is the main query used by the issue sidebar.
|
|
func GetCustomFieldsByRepo(ctx context.Context, repoID int64) ([]*CustomFieldDef, error) {
|
|
// First try org-level fields (owner_id != 0, repo_id = 0)
|
|
// Fall back to legacy repo-level fields (repo_id = repoID)
|
|
fields := make([]*CustomFieldDef, 0, 10)
|
|
return fields, db.GetEngine(ctx).
|
|
Where("((owner_id != 0 AND repo_id = 0) OR repo_id = ?) AND scope = ? AND is_active = ?",
|
|
repoID, CustomFieldScopeIssue, true).
|
|
OrderBy("sort_order ASC, id ASC").
|
|
Find(&fields)
|
|
}
|
|
|
|
// GetAllCustomFieldsByRepo returns all field definitions for a repo (for settings page).
|
|
func GetAllCustomFieldsByRepo(ctx context.Context, repoID int64) ([]*CustomFieldDef, error) {
|
|
fields := make([]*CustomFieldDef, 0, 10)
|
|
return fields, db.GetEngine(ctx).
|
|
Where("repo_id = ?", repoID).
|
|
OrderBy("sort_order ASC, id ASC").
|
|
Find(&fields)
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Field definition CRUD
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// GetCustomFieldDefByID returns a single field definition.
|
|
func GetCustomFieldDefByID(ctx context.Context, id int64) (*CustomFieldDef, error) {
|
|
field := new(CustomFieldDef)
|
|
has, err := db.GetEngine(ctx).ID(id).Get(field)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, db.ErrNotExist{Resource: "CustomFieldDef", ID: id}
|
|
}
|
|
return field, nil
|
|
}
|
|
|
|
// CreateCustomFieldDef creates a new custom field definition.
|
|
func CreateCustomFieldDef(ctx context.Context, field *CustomFieldDef) error {
|
|
_, err := db.GetEngine(ctx).Insert(field)
|
|
return err
|
|
}
|
|
|
|
// UpdateCustomFieldDef updates a custom field definition.
|
|
func UpdateCustomFieldDef(ctx context.Context, field *CustomFieldDef) error {
|
|
_, err := db.GetEngine(ctx).ID(field.ID).AllCols().Update(field)
|
|
return err
|
|
}
|
|
|
|
// DeleteCustomFieldDef deletes a field definition and all its values.
|
|
func DeleteCustomFieldDef(ctx context.Context, id int64) error {
|
|
if _, err := db.GetEngine(ctx).Where("field_id = ?", id).Delete(new(CustomFieldValue)); err != nil {
|
|
return err
|
|
}
|
|
_, err := db.GetEngine(ctx).ID(id).Delete(new(CustomFieldDef))
|
|
return err
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Field values — generic entity-based (works for issues and repos)
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// GetCustomFieldValuesMap returns field_id -> value for an entity.
|
|
func GetCustomFieldValuesMap(ctx context.Context, entityID int64) (map[int64]string, error) {
|
|
values := make([]*CustomFieldValue, 0, 10)
|
|
if err := db.GetEngine(ctx).Where("entity_id = ?", entityID).Find(&values); err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(map[int64]string, len(values))
|
|
for _, v := range values {
|
|
result[v.FieldID] = v.Value
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// SetCustomFieldValue creates or updates a single custom field value.
|
|
func SetCustomFieldValue(ctx context.Context, entityID, fieldID int64, value string) error {
|
|
existing := new(CustomFieldValue)
|
|
has, err := db.GetEngine(ctx).Where("entity_id = ? AND field_id = ?", entityID, fieldID).Get(existing)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if has {
|
|
existing.Value = value
|
|
_, err = db.GetEngine(ctx).ID(existing.ID).Cols("value").Update(existing)
|
|
return err
|
|
}
|
|
_, err = db.GetEngine(ctx).Insert(&CustomFieldValue{
|
|
EntityID: entityID,
|
|
FieldID: fieldID,
|
|
Value: value,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// SetCustomFieldValues sets multiple custom field values for an entity.
|
|
func SetCustomFieldValues(ctx context.Context, entityID int64, values map[int64]string) error {
|
|
for fieldID, value := range values {
|
|
if err := SetCustomFieldValue(ctx, entityID, fieldID, value); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|