feat(issues): custom fields in issue sidebar #473
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}}
|
||||
|
||||
Reference in New Issue
Block a user