diff --git a/CHANGELOG.md b/CHANGELOG.md index 8881870b8e..3352f10677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/modules/structs/issue.go b/modules/structs/issue.go index f108cf3d0a..516ed6235d 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -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 diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 6c85b2e4c2..b6c242b105 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -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) { diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index c123843341..f2f5bc1290 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -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) } diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index 8fc5afa035..0fa87b1bb6 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -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") + } + } +} diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index dfa2a8b34e..4803827c4c 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -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 diff --git a/services/updateserver/composer.go b/services/updateserver/composer.go index 4353daa6d1..0bdca68ac8 100644 --- a/services/updateserver/composer.go +++ b/services/updateserver/composer.go @@ -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) diff --git a/services/updateserver/drupal.go b/services/updateserver/drupal.go index 1c9495021c..8e5472d4d7 100644 --- a/services/updateserver/drupal.go +++ b/services/updateserver/drupal.go @@ -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) diff --git a/services/updateserver/joomla.go b/services/updateserver/joomla.go index 11139ddbc7..b64edf9fd1 100644 --- a/services/updateserver/joomla.go +++ b/services/updateserver/joomla.go @@ -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 element: packages use client_id=0 in #__extensions, diff --git a/services/updateserver/prestashop.go b/services/updateserver/prestashop.go index 81442f756e..8795500c71 100644 --- a/services/updateserver/prestashop.go +++ b/services/updateserver/prestashop.go @@ -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) diff --git a/services/updateserver/whmcs.go b/services/updateserver/whmcs.go index a3a95a50cb..da188162f9 100644 --- a/services/updateserver/whmcs.go +++ b/services/updateserver/whmcs.go @@ -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) diff --git a/services/updateserver/wordpress.go b/services/updateserver/wordpress.go index d17ef645d0..619ffa04ba 100644 --- a/services/updateserver/wordpress.go +++ b/services/updateserver/wordpress.go @@ -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. diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index f6ee548e15..1388fa950e 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -59,6 +59,31 @@ {{end}} {{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}} + {{if .CustomFieldDefs}} +
+
+ {{$values := .CustomFieldValues}} + {{$fieldOptions := .CustomFieldOptions}} + {{range .CustomFieldDefs}} + {{$currentVal := index $values .ID}} +
+ {{.Name}} + {{if ne .Options ""}} + {{$opts := index $fieldOptions .ID}} + + {{else}} + + {{end}} +
+ {{end}} +
+ {{end}} + {{if and .PageIsComparePull (not (eq .HeadRepo.FullName .BaseCompareRepo.FullName)) .CanWriteToHeadRepo}}