22586b7a06
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 50s
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
Status and priority are first-class fields, not custom fields. They must always show in the sidebar without requiring manual setup. When an org has no definitions, the standard presets are auto-created on first access.
133 lines
5.9 KiB
Go
133 lines
5.9 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(IssueStatusDef))
|
|
}
|
|
|
|
// IssueStatusDef defines a custom issue status at the org level.
|
|
type IssueStatusDef struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'org_id'"`
|
|
Name string `xorm:"NOT NULL"`
|
|
Color string `xorm:"VARCHAR(7)"` // hex color, e.g. "#e11d48"
|
|
Description string `xorm:"TEXT"`
|
|
ClosesIssue bool `xorm:"NOT NULL DEFAULT false 'closes_issue'"`
|
|
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 (IssueStatusDef) TableName() string {
|
|
return "issue_status_def"
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Queries
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// GetIssueStatusDefsByOrg returns active status definitions for an org.
|
|
// If none exist, seeds the org with default statuses automatically.
|
|
func GetIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDef, error) {
|
|
defs := make([]*IssueStatusDef, 0, 10)
|
|
if err := db.GetEngine(ctx).
|
|
Where("org_id = ? AND is_active = ?", orgID, true).
|
|
OrderBy("sort_order ASC, id ASC").
|
|
Find(&defs); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(defs) == 0 && orgID > 0 {
|
|
if err := seedDefaultIssueStatuses(ctx, orgID); err != nil {
|
|
return defs, nil // non-fatal
|
|
}
|
|
return GetIssueStatusDefsByOrg(ctx, orgID)
|
|
}
|
|
return defs, nil
|
|
}
|
|
|
|
// seedDefaultIssueStatuses creates the standard status presets for an org.
|
|
func seedDefaultIssueStatuses(ctx context.Context, orgID int64) error {
|
|
defaults := []*IssueStatusDef{
|
|
{OrgID: orgID, Name: "In Progress", Color: "#2563eb", Description: "Work is actively being done", SortOrder: 1, IsActive: true},
|
|
{OrgID: orgID, Name: "Needs Info", Color: "#f59e0b", Description: "Waiting for more information", SortOrder: 2, IsActive: true},
|
|
{OrgID: orgID, Name: "Blocked", Color: "#dc2626", Description: "Cannot proceed due to dependency", SortOrder: 3, IsActive: true},
|
|
{OrgID: orgID, Name: "Resolved", Color: "#16a34a", Description: "Fix implemented and verified", ClosesIssue: true, SortOrder: 4, IsActive: true},
|
|
{OrgID: orgID, Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to address", ClosesIssue: true, SortOrder: 5, IsActive: true},
|
|
{OrgID: orgID, Name: "Duplicate", Color: "#8b5cf6", Description: "Already tracked elsewhere", ClosesIssue: true, SortOrder: 6, IsActive: true},
|
|
}
|
|
for _, d := range defaults {
|
|
if _, err := db.GetEngine(ctx).Insert(d); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAllIssueStatusDefsByOrg returns all status definitions (including inactive).
|
|
func GetAllIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDef, error) {
|
|
defs := make([]*IssueStatusDef, 0, 10)
|
|
return defs, db.GetEngine(ctx).
|
|
Where("org_id = ?", orgID).
|
|
OrderBy("sort_order ASC, id ASC").
|
|
Find(&defs)
|
|
}
|
|
|
|
// GetIssueStatusDefByID returns a single status definition.
|
|
func GetIssueStatusDefByID(ctx context.Context, id int64) (*IssueStatusDef, error) {
|
|
def := new(IssueStatusDef)
|
|
has, err := db.GetEngine(ctx).ID(id).Get(def)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, db.ErrNotExist{Resource: "IssueStatusDef", ID: id}
|
|
}
|
|
return def, nil
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// CRUD
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// CreateIssueStatusDef creates a new status definition.
|
|
func CreateIssueStatusDef(ctx context.Context, def *IssueStatusDef) error {
|
|
_, err := db.GetEngine(ctx).Insert(def)
|
|
return err
|
|
}
|
|
|
|
// UpdateIssueStatusDef updates a status definition.
|
|
func UpdateIssueStatusDef(ctx context.Context, def *IssueStatusDef) error {
|
|
_, err := db.GetEngine(ctx).ID(def.ID).AllCols().Update(def)
|
|
return err
|
|
}
|
|
|
|
// DeleteIssueStatusDef deletes a status definition and clears references on issues.
|
|
func DeleteIssueStatusDef(ctx context.Context, id int64) error {
|
|
// Clear status_id on all issues that reference this definition
|
|
if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = 0 WHERE status_id = ?", id); err != nil {
|
|
return err
|
|
}
|
|
_, err := db.GetEngine(ctx).ID(id).Delete(new(IssueStatusDef))
|
|
return err
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Issue status helpers
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// SetIssueStatusID updates the status_id on an issue.
|
|
func SetIssueStatusID(ctx context.Context, issueID, statusID int64) error {
|
|
_, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = ? WHERE id = ?", statusID, issueID)
|
|
return err
|
|
}
|