feat(issues): custom status definitions with automated actions (#502) #503
@@ -76,6 +76,8 @@ type Issue struct {
|
||||
Assignee *user_model.User `xorm:"-"`
|
||||
isAssigneeLoaded bool `xorm:"-"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
StatusID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'status_id'"`
|
||||
Status *IssueStatusDef `xorm:"-"`
|
||||
IsRead bool `xorm:"-"`
|
||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||
PullRequest *PullRequest `xorm:"-"`
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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.
|
||||
func GetIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDef, error) {
|
||||
defs := make([]*IssueStatusDef, 0, 10)
|
||||
return defs, db.GetEngine(ctx).
|
||||
Where("org_id = ? AND is_active = ?", orgID, true).
|
||||
OrderBy("sort_order ASC, id ASC").
|
||||
Find(&defs)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -423,6 +423,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(343, "Add custom field tables for issue custom fields", v1_27.AddCustomFieldTables),
|
||||
newMigration(344, "Add domain_restriction to license_package table", v1_27.AddDomainRestrictionToLicensePackage),
|
||||
newMigration(345, "Migrate custom fields to org-level with scope", v1_27.MigrateCustomFieldsToOrgLevel),
|
||||
newMigration(346, "Add issue status definitions table", v1_27.AddIssueStatusDefTable),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddIssueStatusDefTable creates the issue_status_def table and adds
|
||||
// status_id to the issue table.
|
||||
func AddIssueStatusDefTable(x *xorm.Engine) error {
|
||||
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)"`
|
||||
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 int64 `xorm:"INDEX CREATED 'created_unix'"`
|
||||
UpdatedUnix int64 `xorm:"UPDATED 'updated_unix'"`
|
||||
}
|
||||
if err := x.Sync(new(IssueStatusDef)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add status_id column to issue table
|
||||
type Issue struct {
|
||||
StatusID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'status_id'"`
|
||||
}
|
||||
return x.Sync(new(Issue))
|
||||
}
|
||||
@@ -1582,6 +1582,7 @@
|
||||
"repo.issues.edit": "Edit",
|
||||
"repo.issues.cancel": "Cancel",
|
||||
"repo.issues.save": "Save",
|
||||
"repo.issues.status": "Status",
|
||||
"repo.issues.label_title": "Name",
|
||||
"repo.issues.label_description": "Description",
|
||||
"repo.issues.label_color": "Color",
|
||||
@@ -2917,6 +2918,21 @@
|
||||
"org.settings.custom_field_created": "Custom field created.",
|
||||
"org.settings.custom_field_updated": "Custom field updated.",
|
||||
"org.settings.custom_field_deleted": "Custom field deleted.",
|
||||
"org.settings.issue_statuses": "Issue Statuses",
|
||||
"org.settings.issue_statuses_desc": "Define custom issue statuses for all repositories in this organization. Statuses appear in the issue sidebar and can automatically close or reopen issues.",
|
||||
"org.settings.issue_statuses_empty": "No custom issue statuses defined yet.",
|
||||
"org.settings.issue_status_add": "Add Status",
|
||||
"org.settings.issue_status_name": "Status Name",
|
||||
"org.settings.issue_status_color": "Color",
|
||||
"org.settings.issue_status_description": "Description",
|
||||
"org.settings.issue_status_closes_issue": "Closes issue",
|
||||
"org.settings.issue_status_closes_issue_help": "When this status is selected, the issue will be automatically closed.",
|
||||
"org.settings.issue_status_closes": "Closes",
|
||||
"org.settings.issue_status_sort_order": "Sort Order",
|
||||
"org.settings.issue_status_inactive": "Inactive",
|
||||
"org.settings.issue_status_created": "Issue status created.",
|
||||
"org.settings.issue_status_updated": "Issue status updated.",
|
||||
"org.settings.issue_status_deleted": "Issue status deleted.",
|
||||
"org.settings.update_streams": "Update Server",
|
||||
"org.settings.licensing": "Update Server",
|
||||
"org.settings.licensing_desc": "Manage update feeds and optional license key gating across all repositories in this organization.",
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// 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 {
|
||||
ctx.ServerError("DeleteIssueStatusDef", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.issue_status_deleted"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
issue_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/issue"
|
||||
)
|
||||
|
||||
// UpdateIssueCustomStatus handles POST to set a custom status on an issue.
|
||||
// If the chosen status has ClosesIssue=true, the issue is automatically closed.
|
||||
// If the chosen status has ClosesIssue=false and the issue is closed, it is reopened.
|
||||
func UpdateIssueCustomStatus(ctx *context.Context) {
|
||||
issueID := ctx.PathParamInt64("id")
|
||||
statusID := ctx.FormInt64("status_id")
|
||||
|
||||
issue, err := issues_model.GetIssueByID(ctx, issueID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetIssueByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the status belongs to this repo's org (or is being cleared).
|
||||
if statusID > 0 {
|
||||
statusDef, err := issues_model.GetIssueStatusDefByID(ctx, statusID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetIssueStatusDefByID", err)
|
||||
return
|
||||
}
|
||||
if statusDef.OrgID != ctx.Repo.Repository.OwnerID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle automatic close/reopen based on the status definition.
|
||||
if statusDef.ClosesIssue && !issue.IsClosed {
|
||||
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||
log.Error("UpdateIssueCustomStatus: CloseIssue: %v", err)
|
||||
}
|
||||
} else if !statusDef.ClosesIssue && issue.IsClosed {
|
||||
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||
log.Error("UpdateIssueCustomStatus: ReopenIssue: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := issues_model.SetIssueStatusID(ctx, issueID, statusID); err != nil {
|
||||
ctx.ServerError("SetIssueStatusID", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
|
||||
}
|
||||
@@ -364,6 +364,14 @@ func ViewIssue(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["CustomFieldValues"] = customFieldValues
|
||||
ctx.Data["CustomFieldOptions"] = fieldOptions
|
||||
|
||||
// Load custom issue status definitions for the sidebar.
|
||||
issueStatusDefs, isErr := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
|
||||
if isErr != nil {
|
||||
log.Error("ViewIssue: GetIssueStatusDefsByOrg: %v", isErr)
|
||||
}
|
||||
ctx.Data["IssueStatusDefs"] = issueStatusDefs
|
||||
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
if err := issue.LoadAttributes(ctx); err != nil {
|
||||
|
||||
@@ -1067,6 +1067,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Post("/{id}/edit", org.SettingsCustomFieldsEditPost)
|
||||
m.Post("/{id}/delete", org.SettingsCustomFieldsDeletePost)
|
||||
})
|
||||
m.Group("/issue-statuses", func() {
|
||||
m.Get("", org.SettingsIssueStatuses)
|
||||
m.Post("", org.SettingsIssueStatusesCreatePost)
|
||||
m.Post("/{id}/edit", org.SettingsIssueStatusesEditPost)
|
||||
m.Post("/{id}/delete", org.SettingsIssueStatusesDeletePost)
|
||||
})
|
||||
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
|
||||
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
|
||||
}, reqSignIn)
|
||||
@@ -1399,6 +1405,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
|
||||
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
|
||||
m.Post("/{id}/custom-fields/{field_id}", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomField)
|
||||
m.Post("/{id}/custom-status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomStatus)
|
||||
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
|
||||
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
|
||||
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings issue-statuses")}}
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "org.settings.issue_statuses"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p class="text grey">{{ctx.Locale.Tr "org.settings.issue_statuses_desc"}}</p>
|
||||
|
||||
{{if .IssueStatuses}}
|
||||
<table class="ui compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "org.settings.issue_status_color"}}</th>
|
||||
<th>{{ctx.Locale.Tr "org.settings.issue_status_name"}}</th>
|
||||
<th>{{ctx.Locale.Tr "org.settings.issue_status_closes_issue"}}</th>
|
||||
<th>{{ctx.Locale.Tr "org.settings.issue_status_sort_order"}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .IssueStatuses}}
|
||||
<tr {{if not .IsActive}}class="tw-opacity-50"{{end}}>
|
||||
<td>
|
||||
{{if .Color}}
|
||||
<span class="tw-inline-block tw-w-4 tw-h-4 tw-rounded" style="background-color: {{.Color}}"></span>
|
||||
{{else}}
|
||||
<span class="text grey">-</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{.Name}}</strong>
|
||||
{{if not .IsActive}}<span class="ui mini grey label">{{ctx.Locale.Tr "org.settings.issue_status_inactive"}}</span>{{end}}
|
||||
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .ClosesIssue}}
|
||||
<span class="ui mini purple label">{{ctx.Locale.Tr "org.settings.issue_status_closes"}}</span>
|
||||
{{else}}
|
||||
<span class="text grey">-</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>{{.SortOrder}}</td>
|
||||
<td class="tw-text-right">
|
||||
<form method="post" action="{{$.OrgLink}}/settings/issue-statuses/{{.ID}}/delete" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="empty-placeholder">
|
||||
<p>{{ctx.Locale.Tr "org.settings.issue_statuses_empty"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<h5>{{ctx.Locale.Tr "org.settings.issue_status_add"}}</h5>
|
||||
<form class="ui form" method="post" action="{{.OrgLink}}/settings/issue-statuses">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="three fields">
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.issue_status_name"}}</label>
|
||||
<input name="name" required placeholder="e.g. In Progress, Won't Fix, Blocked">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.issue_status_color"}}</label>
|
||||
<input name="color" type="color" value="#0075ff">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.issue_status_sort_order"}}</label>
|
||||
<input name="sort_order" type="number" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.issue_status_description"}}</label>
|
||||
<input name="description" placeholder="Help text shown to users">
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox tw-mt-4">
|
||||
<input name="closes_issue" type="checkbox">
|
||||
<label>{{ctx.Locale.Tr "org.settings.issue_status_closes_issue"}}</label>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.issue_status_closes_issue_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "org.settings.issue_status_add"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{{template "org/settings/layout_footer" .}}
|
||||
@@ -31,6 +31,9 @@
|
||||
<a class="{{if .PageIsSettingsCustomFields}}active {{end}}item" href="{{.OrgLink}}/settings/custom-fields">
|
||||
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "org.settings.custom_fields"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsIssueStatuses}}active {{end}}item" href="{{.OrgLink}}/settings/issue-statuses">
|
||||
{{svg "octicon-tasklist"}} {{ctx.Locale.Tr "org.settings.issue_statuses"}}
|
||||
</a>
|
||||
{{if .EnableActions}}
|
||||
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
|
||||
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{{if .IssueStatusDefs}}
|
||||
<div class="divider"></div>
|
||||
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2">
|
||||
<span class="text grey tw-text-sm">{{ctx.Locale.Tr "repo.issues.status"}}</span>
|
||||
{{$canModify := .HasIssuesOrPullsWritePermission}}
|
||||
{{if $canModify}}
|
||||
<form method="post" action="{{.RepoLink}}/issues/{{.Issue.ID}}/custom-status" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<select name="status_id" class="ui compact mini dropdown tw-max-w-48" onchange="this.form.submit()">
|
||||
<option value="0">—</option>
|
||||
{{range .IssueStatusDefs}}
|
||||
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}
|
||||
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
|
||||
{{.Name}}{{if .ClosesIssue}} ⏻{{end}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</form>
|
||||
{{else}}
|
||||
{{$found := false}}
|
||||
{{range .IssueStatusDefs}}
|
||||
{{if eq .ID $.Issue.StatusID}}
|
||||
{{if .Color}}<span class="tw-inline-block tw-w-3 tw-h-3 tw-rounded" style="background-color: {{.Color}}"></span>{{end}}
|
||||
<span class="tw-text-sm">{{.Name}}</span>
|
||||
{{$found = true}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if not $found}}
|
||||
<span class="tw-text-sm text grey">—</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
{{template "repo/issue/sidebar/label_list" $.IssuePageMetaData}}
|
||||
|
||||
{{template "repo/issue/sidebar/issue_status" $}}
|
||||
|
||||
{{template "repo/issue/sidebar/custom_fields" $}}
|
||||
|
||||
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
|
||||
|
||||
Reference in New Issue
Block a user