71d52e432e
Deploy MokoGitea / deploy (push) Successful in 5m8s
Add IsRequired field to IssueStatusDef. Open and Closed statuses are seeded as required and cannot be deleted. Delete attempts return an error flash in the web UI and ErrStatusRequired in the model layer. API response now includes is_required field.
159 lines
6.7 KiB
Go
159 lines
6.7 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'"`
|
|
IsRequired bool `xorm:"NOT NULL DEFAULT false 'is_required'"` // cannot be deleted
|
|
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.
|
|
// Open and Closed are required (is_required=true) and cannot be deleted.
|
|
func seedDefaultIssueStatuses(ctx context.Context, orgID int64) error {
|
|
defaults := []*IssueStatusDef{
|
|
{OrgID: orgID, Name: "Open", Color: "#2563eb", Description: "New or active issue", ClosesIssue: false, IsRequired: true, SortOrder: 0, IsActive: true},
|
|
{OrgID: orgID, Name: "In Progress", Color: "#7c3aed", Description: "Work is actively being done", SortOrder: 1, IsActive: true},
|
|
{OrgID: orgID, Name: "Waiting", Color: "#f59e0b", Description: "Blocked or waiting for input", SortOrder: 2, IsActive: true},
|
|
{OrgID: orgID, Name: "In Review", Color: "#0891b2", Description: "PR submitted, awaiting review", SortOrder: 3, IsActive: true},
|
|
{OrgID: orgID, Name: "Closed", Color: "#16a34a", Description: "Completed or resolved", ClosesIssue: true, IsRequired: true, SortOrder: 4, IsActive: true},
|
|
{OrgID: orgID, Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to address", ClosesIssue: true, SortOrder: 5, 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
|
|
}
|
|
|
|
// ErrStatusRequired is returned when trying to delete a required status.
|
|
type ErrStatusRequired struct {
|
|
ID int64
|
|
Name string
|
|
}
|
|
|
|
func (e ErrStatusRequired) Error() string {
|
|
return "status is required and cannot be deleted"
|
|
}
|
|
|
|
// IsErrStatusRequired checks if an error is ErrStatusRequired.
|
|
func IsErrStatusRequired(err error) bool {
|
|
_, ok := err.(ErrStatusRequired)
|
|
return ok
|
|
}
|
|
|
|
// DeleteIssueStatusDef deletes a status definition and clears references on issues.
|
|
// Returns ErrStatusRequired if the status is marked as required.
|
|
func DeleteIssueStatusDef(ctx context.Context, id int64) error {
|
|
def, err := GetIssueStatusDefByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if def.IsRequired {
|
|
return ErrStatusRequired{ID: def.ID, Name: def.Name}
|
|
}
|
|
// 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
|
|
}
|