release: v1.26.1-moko.06.03 #495
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user