release: v1.26.1-moko.06.03 #495

Merged
jmiller merged 6 commits from rc/v1.26.1-moko.06.03 into main 2026-06-05 04:04:36 +00:00
13 changed files with 303 additions and 108 deletions
+9
View File
@@ -46,6 +46,14 @@ All notable changes to MokoGitea are documented here. Versions follow the format
* feat(settings): icons on all settings navbars (repo, org, user, admin)
* feat(ui): styled 403 Access Denied page with inline login form
* feat(issues): custom fields with inline editing in issue sidebar
* feat(issues): pre-fill custom fields from issue template YAML frontmatter (#493)
* Templates specify `custom_fields:` map (field name → default value)
* New issue sidebar shows org-level fields with template defaults pre-selected
* API create issue accepts `custom_fields` map by name
* feat(updateserver): resolve extension metadata from org-level custom fields (#492)
* Cascading fallback: custom fields → config table → repo-derived defaults
* All six generators updated (Joomla, WordPress, Composer, Drupal, PrestaShop, WHMCS)
* Repos can be migrated to custom fields gradually
* feat(ui): two-in-one Update Server / Licenses tab
* No gating: shows "Update Server" tab with feed URLs only
* Gated: shows "Licenses" tab with full key management
@@ -73,6 +81,7 @@ All notable changes to MokoGitea are documented here. Versions follow the format
* fix(updateserver): prevent stream name tag from overriding asset-derived version
* fix(build): restore build/ directory after accidental deletion
* fix(licenses): master key banner removed, master keys sort first in table
* fix(issues): issue sidebar loads org-level fields instead of legacy repo-level fields
## [v1.26.1-moko.05] - 2026-05-31
+12 -9
View File
@@ -104,6 +104,8 @@ type CreateIssueOption struct {
// list of project ids
Projects []int64 `json:"projects"`
Closed bool `json:"closed"`
// custom field values keyed by field name
CustomFields map[string]string `json:"custom_fields,omitempty"`
}
// EditIssueOption options for editing an issue
@@ -190,15 +192,16 @@ const (
// IssueTemplate represents an issue template for a repository
// swagger:model
type IssueTemplate struct {
Name string `json:"name" yaml:"name"`
Title string `json:"title" yaml:"title"`
About string `json:"about" yaml:"about"` // Using "description" in a template file is compatible
Labels IssueTemplateStringSlice `json:"labels" yaml:"labels"`
Assignees IssueTemplateStringSlice `json:"assignees" yaml:"assignees"`
Ref string `json:"ref" yaml:"ref"`
Content string `json:"content" yaml:"-"`
Fields []*IssueFormField `json:"body" yaml:"body"`
FileName string `json:"file_name" yaml:"-"`
Name string `json:"name" yaml:"name"`
Title string `json:"title" yaml:"title"`
About string `json:"about" yaml:"about"` // Using "description" in a template file is compatible
Labels IssueTemplateStringSlice `json:"labels" yaml:"labels"`
Assignees IssueTemplateStringSlice `json:"assignees" yaml:"assignees"`
Ref string `json:"ref" yaml:"ref"`
Content string `json:"content" yaml:"-"`
Fields []*IssueFormField `json:"body" yaml:"body"`
FileName string `json:"file_name" yaml:"-"`
CustomFields map[string]string `json:"custom_fields,omitempty" yaml:"custom_fields"`
}
type IssueTemplateStringSlice []string
+23
View File
@@ -702,6 +702,29 @@ func CreateIssue(ctx *context.APIContext) {
return
}
// Save custom field values if provided (resolve field names to IDs).
if len(form.CustomFields) > 0 {
defs, defErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue)
if defErr != nil {
ctx.APIErrorInternal(defErr)
return
}
if len(defs) > 0 {
vals := make(map[int64]string)
for _, def := range defs {
if v, ok := form.CustomFields[def.Name]; ok {
vals[def.ID] = v
}
}
if len(vals) > 0 {
if setErr := issues_model.SetCustomFieldValues(ctx, issue.ID, vals); setErr != nil {
ctx.APIErrorInternal(setErr)
return
}
}
}
}
if form.Closed {
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
if issues_model.IsErrDependenciesLeft(err) {
+1 -1
View File
@@ -630,7 +630,7 @@ func (cpi *comparePageInfoType) prepareCreatePullRequestPage(ctx *context.Contex
if ctx.Written() {
return
}
_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, pageMetaData)
_, templateErrs, _ := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, pageMetaData)
if len(templateErrs) > 0 {
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
}
+65 -5
View File
@@ -4,6 +4,7 @@
package repo
import (
"encoding/json"
"errors"
"fmt"
"html/template"
@@ -36,10 +37,11 @@ import (
)
// Tries to load and set an issue template. The first return value indicates if a template was loaded.
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string, metaData *IssuePageMetaData) (bool, map[string]error) {
// The third return value contains the template's custom_fields map (field name → default value).
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string, metaData *IssuePageMetaData) (bool, map[string]error, map[string]string) {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
return false, nil
return false, nil, nil
}
templateCandidates := make([]string, 0, 1+len(possibleFiles))
@@ -84,9 +86,9 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
}
metaData.AssigneesData.SelectedAssigneeIDs = strings.Join(selectedAssigneeIDStrings, ",")
return true, templateErrs
return true, templateErrs, template.CustomFields
}
return false, templateErrs
return false, templateErrs, nil
}
// NewIssue render creating issue page
@@ -128,7 +130,7 @@ func NewIssue(ctx *context.Context) {
ctx.Data["Tags"] = tags
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData)
templateLoaded, errs, templateCustomFields := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData)
maps.Copy(ret.TemplateErrors, errs)
if ctx.Written() {
return
@@ -140,6 +142,35 @@ func NewIssue(ctx *context.Context) {
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypeIssues)
// Load org-level issue-scoped custom fields for the new issue sidebar.
customFieldDefs, cfErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue)
if cfErr != nil {
log.Error("NewIssue: GetCustomFieldsByOwner: %v", cfErr)
}
ctx.Data["CustomFieldDefs"] = customFieldDefs
customFieldValues := make(map[int64]string)
fieldOptions := make(map[int64][]string)
if len(customFieldDefs) > 0 {
// Resolve template custom_fields (name → value) to field IDs.
if len(templateCustomFields) > 0 {
for _, def := range customFieldDefs {
if val, ok := templateCustomFields[def.Name]; ok {
customFieldValues[def.ID] = val
}
}
}
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
if !issueConfig.BlankIssuesEnabled && hasTemplates && !templateLoaded {
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if blank issues are disabled, just redirect to the "issues/choose" page with these parameters.
ctx.Redirect(fmt.Sprintf("%s/issues/new/choose?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
@@ -377,6 +408,9 @@ func NewIssuePost(ctx *context.Context) {
return
}
// Save custom field values submitted from the new issue form.
saveCustomFieldsFromForm(ctx, repo.OwnerID, issue.ID)
log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
if ctx.FormString("redirect_after_creation") == "project" && len(projectIDs) > 0 {
// When issue is in multiple projects, redirect to first project from form order.
@@ -392,3 +426,29 @@ func NewIssuePost(ctx *context.Context) {
}
ctx.JSONRedirect(issue.Link())
}
// saveCustomFieldsFromForm reads custom field values from the form
// (submitted as "custom-field-{fieldID}") and persists them for the issue.
func saveCustomFieldsFromForm(ctx *context.Context, ownerID, issueID int64) {
defs, err := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeIssue)
if err != nil {
log.Error("saveCustomFieldsFromForm: GetCustomFieldsByOwner: %v", err)
return
}
if len(defs) == 0 {
return
}
vals := make(map[int64]string)
for _, def := range defs {
v := ctx.Req.FormValue(fmt.Sprintf("custom-field-%d", def.ID))
if v != "" {
vals[def.ID] = v
}
}
if len(vals) > 0 {
if err := issues_model.SetCustomFieldValues(ctx, issueID, vals); err != nil {
log.Error("saveCustomFieldsFromForm: %v", err)
ctx.Flash.Error("Failed to save custom field values")
}
}
}
+10 -3
View File
@@ -339,13 +339,20 @@ 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)
// Load custom fields for the issue sidebar (org-level issue-scoped fields).
customFieldDefs, cfErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue)
if cfErr != nil {
log.Error("ViewIssue: GetCustomFieldsByOwner: %v", cfErr)
}
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)
var cvErr error
customFieldValues, cvErr = issues_model.GetCustomFieldValuesMap(ctx, issue.ID)
if cvErr != nil {
log.Error("ViewIssue: GetCustomFieldValuesMap: %v", cvErr)
}
for _, f := range customFieldDefs {
if f.Options != "" {
var opts []string
+7 -10
View File
@@ -68,13 +68,15 @@ func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, lice
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg)
// Composer package name: vendor/package
// Composer package name: vendor/package (override with resolved extension name if set)
packageName := fmt.Sprintf("%s/%s", strings.ToLower(repo.Owner.Name), strings.ToLower(repo.Name))
if cfg != nil && cfg.ExtensionName != "" {
packageName = cfg.ExtensionName
if meta.Element != strings.ToLower(repo.Name) {
packageName = meta.Element
}
description := meta.Description
maintainer := repo.Owner.Name
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
if cfg != nil && cfg.Maintainer != "" {
@@ -84,14 +86,9 @@ func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, lice
maintainerURL = cfg.MaintainerURL
}
description := ""
if cfg != nil && cfg.Description != "" {
description = cfg.Description
}
phpMin := ""
if cfg != nil && cfg.PHPMinimum != "" {
phpMin = ">=" + cfg.PHPMinimum
if meta.PHPMinimum != "" {
phpMin = ">=" + meta.PHPMinimum
}
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
+3 -10
View File
@@ -66,16 +66,9 @@ func GenerateDrupalXML(ctx context.Context, repo *repo_model.Repository, allowed
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
shortName := strings.ToLower(repo.Name)
title := repo.Name
if cfg != nil {
if cfg.ExtensionName != "" {
shortName = cfg.ExtensionName
}
if cfg.DisplayName != "" {
title = cfg.DisplayName
}
}
meta := resolveExtensionMetadata(ctx, repo, cfg)
shortName := meta.Element
title := meta.DisplayName
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
+128 -29
View File
@@ -13,8 +13,10 @@ import (
"time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
)
@@ -160,9 +162,121 @@ func NormalizeChannel(ch string) string {
}
}
// extensionMetadata holds resolved metadata for feed generation.
// Fields are resolved with priority: custom field → config table → default.
type extensionMetadata struct {
Element string
DisplayName string
ExtType string
TargetVersion string
PHPMinimum string
Description string
SupportURL string
DownloadGating string
KeyPrefix string
}
// resolveExtensionMetadata loads extension metadata with cascading fallback:
// org-level repo-scoped custom fields → update_stream_config → repo-derived defaults.
func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository, cfg *licenses.UpdateStreamConfig) extensionMetadata {
m := extensionMetadata{
Element: strings.ToLower(repo.Name),
DisplayName: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
ExtType: "component",
TargetVersion: "(5|6)\\..*",
}
// Apply config table values.
if cfg != nil {
if cfg.ExtensionName != "" {
m.Element = cfg.ExtensionName
}
if cfg.DisplayName != "" {
m.DisplayName = cfg.DisplayName
}
if cfg.ExtensionType != "" {
m.ExtType = cfg.ExtensionType
}
if cfg.TargetVersion != "" {
m.TargetVersion = cfg.TargetVersion
}
if cfg.PHPMinimum != "" {
m.PHPMinimum = cfg.PHPMinimum
}
if cfg.Description != "" {
m.Description = cfg.Description
}
if cfg.SupportURL != "" {
m.SupportURL = cfg.SupportURL
}
if cfg.DownloadGating != "" {
m.DownloadGating = cfg.DownloadGating
}
if cfg.KeyPrefix != "" {
m.KeyPrefix = cfg.KeyPrefix
}
}
// Override with custom field values (highest priority).
fields, err := issues_model.GetCustomFieldsByOwner(ctx, repo.OwnerID, issues_model.CustomFieldScopeRepo)
if err != nil {
log.Error("resolveExtensionMetadata: GetCustomFieldsByOwner for repo %d: %v", repo.ID, err)
return m
}
if len(fields) == 0 {
return m
}
values, err := issues_model.GetCustomFieldValuesMap(ctx, repo.ID)
if err != nil {
log.Error("resolveExtensionMetadata: GetCustomFieldValuesMap for repo %d: %v", repo.ID, err)
return m
}
if len(values) == 0 {
return m
}
// Build name → value map from field definitions + values.
named := make(map[string]string, len(fields))
for _, f := range fields {
if v, ok := values[f.ID]; ok && v != "" {
named[f.Name] = v
}
}
if v := named["Extension Name"]; v != "" {
m.Element = v
}
if v := named["Display Name"]; v != "" {
m.DisplayName = v
}
if v := named["Extension Type"]; v != "" {
m.ExtType = v
}
if v := named["Target Version"]; v != "" {
m.TargetVersion = v
}
if v := named["PHP Minimum"]; v != "" {
m.PHPMinimum = v
}
if v := named["Support URL"]; v != "" {
m.SupportURL = v
}
if v := named["Description"]; v != "" {
m.Description = v
}
if v := named["Download Gating"]; v != "" {
m.DownloadGating = v
}
if v := named["Key Prefix"]; v != "" {
m.KeyPrefix = v
}
return m
}
// GenerateJoomlaXML builds a Joomla-compatible updates.xml from repository releases.
// It returns the raw XML bytes. Extension metadata is read from the update stream config;
// falls back to repo name/owner when not configured.
// It returns the raw XML bytes. Extension metadata is resolved from custom fields first,
// then the update stream config, then repo-derived defaults.
// allowedChannels optionally restricts output to specific channels (nil = all).
func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, requireKey, stripDownloads bool, allowedChannels ...string) ([]byte, error) {
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
@@ -185,21 +299,18 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
}
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
// Load extension metadata from config (falls back to repo-derived values).
// Load extension metadata with cascading fallback:
// custom fields → config table → repo-derived defaults.
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg)
element := meta.Element
displayName := meta.DisplayName
extType := meta.ExtType
targetVersion := meta.TargetVersion
phpMinimum := meta.PHPMinimum
feedDescription := meta.Description
element := strings.ToLower(repo.Name)
if cfg != nil && cfg.ExtensionName != "" {
element = cfg.ExtensionName
}
displayName := fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name)
if cfg != nil && cfg.DisplayName != "" {
displayName = cfg.DisplayName
}
extType := "component"
if cfg != nil && cfg.ExtensionType != "" {
extType = cfg.ExtensionType
}
// Maintainer and URL always come from the org profile.
maintainer := repo.Owner.FullName
if maintainer == "" {
@@ -209,18 +320,6 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
if maintainerURL == "" {
maintainerURL = fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
}
targetVersion := "(5|6)\\..*"
if cfg != nil && cfg.TargetVersion != "" {
targetVersion = cfg.TargetVersion
}
phpMinimum := ""
if cfg != nil && cfg.PHPMinimum != "" {
phpMinimum = cfg.PHPMinimum
}
feedDescription := ""
if cfg != nil && cfg.Description != "" {
feedDescription = cfg.Description
}
// Resolve effective streams (repo override → org default → Joomla default).
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
@@ -319,8 +418,8 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
// Info URL: use support_url (product page), fall back to releases page.
infoURL := fmt.Sprintf("%s/releases", repoLink)
if cfg != nil && cfg.SupportURL != "" {
infoURL = cfg.SupportURL
if meta.SupportURL != "" {
infoURL = meta.SupportURL
}
// Joomla <client> element: packages use client_id=0 in #__extensions,
+6 -16
View File
@@ -55,23 +55,13 @@ func GeneratePrestaShopXML(ctx context.Context, repo *repo_model.Repository, all
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
moduleName := strings.ToLower(repo.Name)
displayName := repo.Name
meta := resolveExtensionMetadata(ctx, repo, cfg)
moduleName := meta.Element
displayName := meta.DisplayName
description := meta.Description
maintainer := repo.Owner.Name
description := ""
if cfg != nil {
if cfg.ExtensionName != "" {
moduleName = cfg.ExtensionName
}
if cfg.DisplayName != "" {
displayName = cfg.DisplayName
}
if cfg.Maintainer != "" {
maintainer = cfg.Maintainer
}
if cfg.Description != "" {
description = cfg.Description
}
if cfg != nil && cfg.Maintainer != "" {
maintainer = cfg.Maintainer
}
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
+3 -8
View File
@@ -50,23 +50,18 @@ func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, license
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
displayName := repo.Name
meta := resolveExtensionMetadata(ctx, repo, cfg)
displayName := meta.DisplayName
description := meta.Description
maintainer := repo.Owner.Name
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
description := ""
if cfg != nil {
if cfg.DisplayName != "" {
displayName = cfg.DisplayName
}
if cfg.Maintainer != "" {
maintainer = cfg.Maintainer
}
if cfg.MaintainerURL != "" {
maintainerURL = cfg.MaintainerURL
}
if cfg.Description != "" {
description = cfg.Description
}
}
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
+11 -17
View File
@@ -57,36 +57,30 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic
baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
// Load extension metadata.
// Load extension metadata with cascading fallback:
// custom fields → config table → repo-derived defaults.
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg)
slug := strings.ToLower(repo.Name)
displayName := fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name)
slug := meta.Element
displayName := meta.DisplayName
requiresPHP := meta.PHPMinimum
homepage := repoLink
if meta.SupportURL != "" {
homepage = meta.SupportURL
}
maintainer := repo.Owner.Name
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
homepage := repoLink
requiresPHP := ""
if cfg != nil {
if cfg.ExtensionName != "" {
slug = cfg.ExtensionName
}
if cfg.DisplayName != "" {
displayName = cfg.DisplayName
}
if cfg.Maintainer != "" {
maintainer = cfg.Maintainer
}
if cfg.MaintainerURL != "" {
maintainerURL = cfg.MaintainerURL
}
if cfg.SupportURL != "" {
homepage = cfg.SupportURL
} else if cfg.InfoURL != "" {
if homepage == repoLink && cfg.InfoURL != "" {
homepage = cfg.InfoURL
}
if cfg.PHPMinimum != "" {
requiresPHP = cfg.PHPMinimum
}
}
// Resolve streams and find the latest stable release.
+25
View File
@@ -59,6 +59,31 @@
{{end}}
{{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
{{if .CustomFieldDefs}}
<div class="divider"></div>
<div class="tw-flex tw-flex-col tw-gap-2">
{{$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 ne .Options ""}}
{{$opts := index $fieldOptions .ID}}
<select name="custom-field-{{.ID}}" class="ui compact mini dropdown tw-max-w-48">
<option value="">—</option>
{{range $opts}}
<option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
{{end}}
</select>
{{else}}
<input name="custom-field-{{.ID}}" type="text" class="tw-max-w-48 tw-text-sm" value="{{$currentVal}}" placeholder="—">
{{end}}
</div>
{{end}}
</div>
{{end}}
{{if and .PageIsComparePull (not (eq .HeadRepo.FullName .BaseCompareRepo.FullName)) .CanWriteToHeadRepo}}
<div class="divider"></div>
<div class="ui checkbox">