feat(issues): custom fields in issue sidebar #473

Merged
jmiller merged 1 commits from feat/custom-fields-sidebar into dev 2026-06-04 18:49:47 +00:00
6 changed files with 90 additions and 0 deletions
+1
View File
@@ -1412,6 +1412,7 @@
"repo.issues.new.open_projects": "Open Projects",
"repo.issues.new.closed_projects": "Closed Projects",
"repo.issues.new.no_items": "No items",
"repo.issues.custom_fields": "Custom Fields",
"repo.issues.new.milestone": "Milestone",
"repo.issues.new.no_milestone": "No Milestone",
"repo.issues.new.clear_milestone": "Clear milestone",
+33
View File
@@ -0,0 +1,33 @@
// 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"
)
// UpdateIssueCustomField handles POST to set a custom field value on an issue.
func UpdateIssueCustomField(ctx *context.Context) {
issueID := ctx.PathParamInt64("id")
fieldID := ctx.PathParamInt64("field_id")
value := ctx.FormString("value")
// Look up issue to get the index for redirect.
issue, err := issues_model.GetIssueByID(ctx, issueID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
if err := issues_model.SetCustomFieldValue(ctx, issueID, fieldID, value); err != nil {
ctx.ServerError("SetCustomFieldValue", err)
return
}
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
}
+20
View File
@@ -4,6 +4,7 @@
package repo
import (
"encoding/json"
"fmt"
"math/big"
"net/http"
@@ -337,6 +338,25 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanRead(unit.TypeProjects)
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
// Load custom fields for the issue sidebar.
customFieldDefs, _ := issues_model.GetCustomFieldsByRepo(ctx, ctx.Repo.Repository.ID)
ctx.Data["CustomFieldDefs"] = customFieldDefs
customFieldValues := make(map[int64]string)
fieldOptions := make(map[int64][]string)
if len(customFieldDefs) > 0 {
customFieldValues, _ = issues_model.GetCustomFieldValuesMap(ctx, issue.ID)
for _, f := range customFieldDefs {
if f.Options != "" {
var opts []string
if err := json.Unmarshal([]byte(f.Options), &opts); err == nil {
fieldOptions[f.ID] = opts
}
}
}
}
ctx.Data["CustomFieldValues"] = customFieldValues
ctx.Data["CustomFieldOptions"] = fieldOptions
upload.AddUploadContext(ctx, "comment")
if err := issue.LoadAttributes(ctx); err != nil {
+1
View File
@@ -1397,6 +1397,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/projects/column", reqRepoIssuesOrPullsWriter, reqRepoProjectsWriter, repo.UpdateIssueProjectColumn)
m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
m.Post("/{id}/custom-fields/{field_id}", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomField)
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
@@ -0,0 +1,33 @@
{{if .CustomFieldDefs}}
<div class="divider"></div>
<div class="tw-flex tw-flex-col tw-gap-2">
{{$issueID := .Issue.ID}}
{{$repoLink := .RepoLink}}
{{$canModify := .HasIssuesOrPullsWritePermission}}
{{$values := .CustomFieldValues}}
{{$fieldOptions := .CustomFieldOptions}}
{{range .CustomFieldDefs}}
{{$currentVal := index $values .ID}}
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2">
<span class="text grey tw-text-sm" {{if .Description}}title="{{.Description}}"{{end}}>{{.Name}}</span>
{{if and $canModify (eq .Options "")}}
{{/* Non-dropdown: just display the value */}}
<span class="tw-text-sm">{{if $currentVal}}{{$currentVal}}{{else}}<span class="text grey">—</span>{{end}}</span>
{{else if $canModify}}
<form method="post" action="{{$repoLink}}/issues/{{$issueID}}/custom-fields/{{.ID}}" class="tw-inline">
{{$.CsrfTokenHtml}}
<select name="value" class="ui compact mini dropdown tw-max-w-48" onchange="this.form.submit()">
<option value="">—</option>
{{$opts := index $fieldOptions .ID}}
{{range $opts}}
<option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
{{end}}
</select>
</form>
{{else}}
<span class="tw-text-sm">{{if $currentVal}}{{$currentVal}}{{else}}<span class="text grey">—</span>{{end}}</span>
{{end}}
</div>
{{end}}
</div>
{{end}}
@@ -7,6 +7,8 @@
{{template "repo/issue/sidebar/label_list" $.IssuePageMetaData}}
{{template "repo/issue/sidebar/custom_fields" $}}
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
{{if .IsProjectsEnabled}}
{{template "repo/issue/sidebar/project_list" $.IssuePageMetaData}}