Files
Jonathan Miller 71d52e432e
Deploy MokoGitea / deploy (push) Successful in 5m8s
feat: enforce required baseline issue statuses (#681)
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.
2026-06-21 11:29:49 -05:00

118 lines
3.6 KiB
Go

// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"net/http"
"strconv"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplOrgIssueStatuses templates.TplName = "org/settings/issue_statuses"
// SettingsIssueStatuses shows the org-level issue statuses management page.
func SettingsIssueStatuses(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings.issue_statuses")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsIssueStatuses"] = true
defs, err := issues_model.GetAllIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.ServerError("GetAllIssueStatusDefsByOrg", err)
return
}
ctx.Data["IssueStatuses"] = defs
ctx.HTML(http.StatusOK, tplOrgIssueStatuses)
}
// SettingsIssueStatusesCreatePost creates a new org-level issue status.
func SettingsIssueStatusesCreatePost(ctx *context.Context) {
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
def := &issues_model.IssueStatusDef{
OrgID: ctx.Org.Organization.ID,
Name: ctx.FormString("name"),
Color: ctx.FormString("color"),
Description: ctx.FormString("description"),
ClosesIssue: ctx.FormString("closes_issue") == "on",
SortOrder: sortOrder,
IsActive: true,
}
if def.Name == "" {
ctx.Flash.Error("Status name is required")
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
return
}
if err := issues_model.CreateIssueStatusDef(ctx, def); err != nil {
ctx.ServerError("CreateIssueStatusDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_status_created"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
}
// SettingsIssueStatusesEditPost updates an org-level issue status.
func SettingsIssueStatusesEditPost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
def, err := issues_model.GetIssueStatusDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetIssueStatusDefByID", err)
return
}
if def.OrgID != ctx.Org.Organization.ID {
ctx.NotFound(nil)
return
}
def.Name = ctx.FormString("name")
def.Color = ctx.FormString("color")
def.Description = ctx.FormString("description")
def.ClosesIssue = ctx.FormString("closes_issue") == "on"
def.IsActive = ctx.FormString("is_active") == "on"
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
def.SortOrder = sortOrder
if err := issues_model.UpdateIssueStatusDef(ctx, def); err != nil {
ctx.ServerError("UpdateIssueStatusDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_status_updated"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
}
// SettingsIssueStatusesDeletePost deletes an org-level issue status.
func SettingsIssueStatusesDeletePost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
def, err := issues_model.GetIssueStatusDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetIssueStatusDefByID", err)
return
}
if def.OrgID != ctx.Org.Organization.ID {
ctx.NotFound(nil)
return
}
if err := issues_model.DeleteIssueStatusDef(ctx, id); err != nil {
if issues_model.IsErrStatusRequired(err) {
ctx.Flash.Error("Cannot delete required status: " + def.Name)
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
return
}
ctx.ServerError("DeleteIssueStatusDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_status_deleted"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
}