release: v1.26.1-moko.06.06 #518

Merged
jmiller merged 4 commits from rc/v1.26.1-moko.06.06 into main 2026-06-06 17:08:09 +00:00
13 changed files with 443 additions and 0 deletions
+2
View File
@@ -78,6 +78,8 @@ type Issue struct {
IsClosed bool `xorm:"INDEX"`
StatusID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'status_id'"`
Status *IssueStatusDef `xorm:"-"`
PriorityID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'priority_id'"`
PriorityDef *IssuePriorityDef `xorm:"-"`
IsRead bool `xorm:"-"`
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
PullRequest *PullRequest `xorm:"-"`
+91
View File
@@ -0,0 +1,91 @@
// 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(IssuePriorityDef))
}
// IssuePriorityDef defines a custom issue priority at the org level.
type IssuePriorityDef 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"`
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
IsDefault bool `xorm:"NOT NULL DEFAULT false 'is_default'"`
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 (IssuePriorityDef) TableName() string {
return "issue_priority_def"
}
// GetIssuePriorityDefsByOrg returns active priority definitions for an org.
func GetIssuePriorityDefsByOrg(ctx context.Context, orgID int64) ([]*IssuePriorityDef, error) {
defs := make([]*IssuePriorityDef, 0, 10)
return defs, db.GetEngine(ctx).
Where("org_id = ? AND is_active = ?", orgID, true).
OrderBy("sort_order ASC, id ASC").
Find(&defs)
}
// GetAllIssuePriorityDefsByOrg returns all priority definitions (including inactive).
func GetAllIssuePriorityDefsByOrg(ctx context.Context, orgID int64) ([]*IssuePriorityDef, error) {
defs := make([]*IssuePriorityDef, 0, 10)
return defs, db.GetEngine(ctx).
Where("org_id = ?", orgID).
OrderBy("sort_order ASC, id ASC").
Find(&defs)
}
// GetIssuePriorityDefByID returns a single priority definition.
func GetIssuePriorityDefByID(ctx context.Context, id int64) (*IssuePriorityDef, error) {
def := new(IssuePriorityDef)
has, err := db.GetEngine(ctx).ID(id).Get(def)
if err != nil {
return nil, err
}
if !has {
return nil, db.ErrNotExist{Resource: "IssuePriorityDef", ID: id}
}
return def, nil
}
// CreateIssuePriorityDef creates a new priority definition.
func CreateIssuePriorityDef(ctx context.Context, def *IssuePriorityDef) error {
_, err := db.GetEngine(ctx).Insert(def)
return err
}
// UpdateIssuePriorityDef updates a priority definition.
func UpdateIssuePriorityDef(ctx context.Context, def *IssuePriorityDef) error {
_, err := db.GetEngine(ctx).ID(def.ID).AllCols().Update(def)
return err
}
// DeleteIssuePriorityDef deletes a priority definition and clears references on issues.
func DeleteIssuePriorityDef(ctx context.Context, id int64) error {
if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET priority_id = 0 WHERE priority_id = ?", id); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(id).Delete(new(IssuePriorityDef))
return err
}
// SetIssuePriorityID updates the priority_id on an issue.
func SetIssuePriorityID(ctx context.Context, issueID, priorityID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE issue SET priority_id = ? WHERE id = ?", priorityID, issueID)
return err
}
+1
View File
@@ -425,6 +425,7 @@ func prepareMigrationTasks() []*migration {
newMigration(345, "Migrate custom fields to org-level with scope", v1_27.MigrateCustomFieldsToOrgLevel),
newMigration(346, "Add issue status definitions table", v1_27.AddIssueStatusDefTable),
newMigration(347, "Add repo manifest table", v1_27.AddRepoManifestTable),
newMigration(348, "Add issue priority definitions table", v1_27.AddIssuePriorityDefTable),
}
return preparedMigrations
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"xorm.io/xorm"
)
// AddIssuePriorityDefTable creates the issue_priority_def table and adds
// priority_id to the issue table.
func AddIssuePriorityDefTable(x *xorm.Engine) error {
type IssuePriorityDef 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"`
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
IsDefault bool `xorm:"NOT NULL DEFAULT false 'is_default'"`
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(IssuePriorityDef)); err != nil {
return err
}
type Issue struct {
PriorityID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'priority_id'"`
}
return x.Sync(new(Issue))
}
+15
View File
@@ -1583,6 +1583,7 @@
"repo.issues.cancel": "Cancel",
"repo.issues.save": "Save",
"repo.issues.status": "Status",
"repo.issues.priority": "Priority",
"repo.issues.label_title": "Name",
"repo.issues.label_description": "Description",
"repo.issues.label_color": "Color",
@@ -2952,6 +2953,20 @@
"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.issue_priorities": "Issue Priorities",
"org.settings.issue_priorities_desc": "Define priority levels for all repositories in this organization. Priorities appear in the issue sidebar.",
"org.settings.issue_priorities_empty": "No custom issue priorities defined yet.",
"org.settings.issue_priority_add": "Add Priority",
"org.settings.issue_priority_name": "Priority Name",
"org.settings.issue_priority_color": "Color",
"org.settings.issue_priority_description": "Description",
"org.settings.issue_priority_default": "Default",
"org.settings.issue_priority_default_help": "Auto-assigned to new issues.",
"org.settings.issue_priority_sort_order": "Sort Order",
"org.settings.issue_priority_inactive": "Inactive",
"org.settings.issue_priority_created": "Issue priority created.",
"org.settings.issue_priority_updated": "Issue priority updated.",
"org.settings.issue_priority_deleted": "Issue priority 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.",
+112
View File
@@ -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 tplOrgIssuePriorities templates.TplName = "org/settings/issue_priorities"
// SettingsIssuePriorities shows the org-level issue priorities management page.
func SettingsIssuePriorities(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings.issue_priorities")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsIssuePriorities"] = true
defs, err := issues_model.GetAllIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.ServerError("GetAllIssuePriorityDefsByOrg", err)
return
}
ctx.Data["IssuePriorities"] = defs
ctx.HTML(http.StatusOK, tplOrgIssuePriorities)
}
// SettingsIssuePrioritiesCreatePost creates a new org-level issue priority.
func SettingsIssuePrioritiesCreatePost(ctx *context.Context) {
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
def := &issues_model.IssuePriorityDef{
OrgID: ctx.Org.Organization.ID,
Name: ctx.FormString("name"),
Color: ctx.FormString("color"),
Description: ctx.FormString("description"),
SortOrder: sortOrder,
IsDefault: ctx.FormString("is_default") == "on",
IsActive: true,
}
if def.Name == "" {
ctx.Flash.Error("Priority name is required")
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-priorities")
return
}
if err := issues_model.CreateIssuePriorityDef(ctx, def); err != nil {
ctx.ServerError("CreateIssuePriorityDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_priority_created"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-priorities")
}
// SettingsIssuePrioritiesEditPost updates an org-level issue priority.
func SettingsIssuePrioritiesEditPost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
def, err := issues_model.GetIssuePriorityDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetIssuePriorityDefByID", 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.IsDefault = ctx.FormString("is_default") == "on"
def.IsActive = ctx.FormString("is_active") == "on"
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
def.SortOrder = sortOrder
if err := issues_model.UpdateIssuePriorityDef(ctx, def); err != nil {
ctx.ServerError("UpdateIssuePriorityDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_priority_updated"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-priorities")
}
// SettingsIssuePrioritiesDeletePost deletes an org-level issue priority.
func SettingsIssuePrioritiesDeletePost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
def, err := issues_model.GetIssuePriorityDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetIssuePriorityDefByID", err)
return
}
if def.OrgID != ctx.Org.Organization.ID {
ctx.NotFound(nil)
return
}
if err := issues_model.DeleteIssuePriorityDef(ctx, id); err != nil {
ctx.ServerError("DeleteIssuePriorityDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_priority_deleted"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-priorities")
}
+44
View File
@@ -0,0 +1,44 @@
// 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/services/context"
)
// UpdateIssueCustomPriority handles POST to set a custom priority on an issue.
func UpdateIssueCustomPriority(ctx *context.Context) {
issueID := ctx.PathParamInt64("id")
priorityID := ctx.FormInt64("priority_id")
issue, err := issues_model.GetIssueByID(ctx, issueID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
// Validate the priority belongs to this repo's org.
if priorityID > 0 {
priorityDef, err := issues_model.GetIssuePriorityDefByID(ctx, priorityID)
if err != nil {
ctx.ServerError("GetIssuePriorityDefByID", err)
return
}
if priorityDef.OrgID != ctx.Repo.Repository.OwnerID {
ctx.NotFound(nil)
return
}
}
if err := issues_model.SetIssuePriorityID(ctx, issueID, priorityID); err != nil {
ctx.ServerError("SetIssuePriorityID", err)
return
}
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
}
+7
View File
@@ -372,6 +372,13 @@ func ViewIssue(ctx *context.Context) {
}
ctx.Data["IssueStatusDefs"] = issueStatusDefs
// Load custom issue priority definitions for the sidebar.
issuePriorityDefs, ipErr := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
if ipErr != nil {
log.Error("ViewIssue: GetIssuePriorityDefsByOrg: %v", ipErr)
}
ctx.Data["IssuePriorityDefs"] = issuePriorityDefs
upload.AddUploadContext(ctx, "comment")
if err := issue.LoadAttributes(ctx); err != nil {
+7
View File
@@ -1073,6 +1073,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/{id}/edit", org.SettingsIssueStatusesEditPost)
m.Post("/{id}/delete", org.SettingsIssueStatusesDeletePost)
})
m.Group("/issue-priorities", func() {
m.Get("", org.SettingsIssuePriorities)
m.Post("", org.SettingsIssuePrioritiesCreatePost)
m.Post("/{id}/edit", org.SettingsIssuePrioritiesEditPost)
m.Post("/{id}/delete", org.SettingsIssuePrioritiesDeletePost)
})
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
}, reqSignIn)
@@ -1407,6 +1413,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
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("/{id}/custom-priority", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomPriority)
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-priorities")}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "org.settings.issue_priorities"}}
</h4>
<div class="ui attached segment">
<p class="text grey">{{ctx.Locale.Tr "org.settings.issue_priorities_desc"}}</p>
{{if .IssuePriorities}}
<table class="ui compact table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "org.settings.issue_priority_color"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_priority_name"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_priority_default"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_priority_sort_order"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .IssuePriorities}}
<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_priority_inactive"}}</span>{{end}}
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
</td>
<td>
{{if .IsDefault}}
<span class="ui mini blue label">{{ctx.Locale.Tr "org.settings.issue_priority_default"}}</span>
{{else}}
<span class="text grey">-</span>
{{end}}
</td>
<td>{{.SortOrder}}</td>
<td class="tw-text-right">
<form method="post" action="{{$.OrgLink}}/settings/issue-priorities/{{.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_priorities_empty"}}</p>
</div>
{{end}}
<div class="divider"></div>
<h5>{{ctx.Locale.Tr "org.settings.issue_priority_add"}}</h5>
<form class="ui form" method="post" action="{{.OrgLink}}/settings/issue-priorities">
{{.CsrfTokenHtml}}
<div class="three fields">
<div class="required field">
<label>{{ctx.Locale.Tr "org.settings.issue_priority_name"}}</label>
<input name="name" required placeholder="e.g. Critical, High, Medium, Low">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.issue_priority_color"}}</label>
<input name="color" type="color" value="#f59e0b">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.issue_priority_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_priority_description"}}</label>
<input name="description" placeholder="Help text shown to users">
</div>
<div class="field">
<div class="ui checkbox tw-mt-4">
<input name="is_default" type="checkbox">
<label>{{ctx.Locale.Tr "org.settings.issue_priority_default"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "org.settings.issue_priority_default_help"}}</p>
</div>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "org.settings.issue_priority_add"}}</button>
</form>
</div>
{{template "org/settings/layout_footer" .}}
+3
View File
@@ -34,6 +34,9 @@
<a class="{{if .PageIsSettingsIssueStatuses}}active {{end}}item" href="{{.OrgLink}}/settings/issue-statuses">
{{svg "octicon-tasklist"}} {{ctx.Locale.Tr "org.settings.issue_statuses"}}
</a>
<a class="{{if .PageIsSettingsIssuePriorities}}active {{end}}item" href="{{.OrgLink}}/settings/issue-priorities">
{{svg "octicon-flame"}} {{ctx.Locale.Tr "org.settings.issue_priorities"}}
</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 .IssuePriorityDefs}}
<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.priority"}}</span>
{{$canModify := .HasIssuesOrPullsWritePermission}}
{{if $canModify}}
<form method="post" action="{{.RepoLink}}/issues/{{.Issue.ID}}/custom-priority" class="tw-inline">
{{$.CsrfTokenHtml}}
<select name="priority_id" class="ui compact mini dropdown tw-max-w-48" onchange="this.form.submit()">
<option value="0">-</option>
{{range .IssuePriorityDefs}}
<option value="{{.ID}}" {{if eq .ID $.Issue.PriorityID}}selected{{end}}
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
{{.Name}}
</option>
{{end}}
</select>
</form>
{{else}}
{{$found := false}}
{{range .IssuePriorityDefs}}
{{if eq .ID $.Issue.PriorityID}}
{{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}}
@@ -9,6 +9,8 @@
{{template "repo/issue/sidebar/issue_status" $}}
{{template "repo/issue/sidebar/issue_priority" $}}
{{template "repo/issue/sidebar/custom_fields" $}}
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}