feat(settings): consolidate manifest + custom fields into metadata page
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 33s
Deploy MokoGitea / deploy (push) Failing after 3m31s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled

- Merge project identity (manifest), update server config, and custom
  fields into single /settings/metadata page
- Three sections: Project Identity, Update Server, Custom Fields
- Old /settings/manifest and /settings/licensing redirect to /metadata
- Single nav link replaces two separate entries
This commit is contained in:
Jonathan Miller
2026-06-09 23:24:35 -05:00
parent 704d9d10be
commit e1ca5cdfc4
5 changed files with 374 additions and 59 deletions
+2 -2
View File
@@ -9,10 +9,10 @@ import (
// LicensingSettings redirects to the manifest page where licensing is now consolidated.
func LicensingSettings(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/manifest")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
}
// LicensingSettingsPost redirects POST to the manifest page.
func LicensingSettingsPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/manifest")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
}
+119 -5
View File
@@ -9,20 +9,48 @@ import (
"net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplSettingsMetadata templates.TplName = "repo/settings/metadata"
// Metadata displays the repo metadata page (repo-scoped custom field values).
// Metadata displays the consolidated metadata page:
// project identity (manifest), update server config, and repo-scoped custom fields.
func Metadata(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.metadata")
ctx.Data["Title"] = "Metadata"
ctx.Data["PageIsSettingsMetadata"] = true
ownerID := ctx.Repo.Repository.OwnerID
repoID := ctx.Repo.Repository.ID
ownerID := ctx.Repo.Repository.OwnerID
// Load manifest (project identity).
manifest, err := repo_model.GetRepoManifest(ctx, repoID)
if err != nil {
ctx.ServerError("GetRepoManifest", err)
return
}
if manifest == nil {
manifest = tryMigrateManifestXML(ctx)
}
if manifest == nil {
manifest = &repo_model.RepoManifest{
RepoID: repoID,
Name: ctx.Repo.Repository.Name,
Org: ctx.Repo.Repository.OwnerName,
Description: ctx.Repo.Repository.Description,
}
}
ctx.Data["Manifest"] = manifest
// Load update server config.
repoCfg, _ := updateserver_model.GetRepoConfig(ctx, repoID)
ctx.Data["RepoUpdateConfig"] = repoCfg
// Load repo-scoped custom fields.
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
ctx.Data["CustomFieldDefs"] = fields
@@ -45,8 +73,55 @@ func Metadata(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplSettingsMetadata)
}
// MetadataPost saves repo-scoped custom field values.
// MetadataPost routes to the correct sub-handler based on the action param.
func MetadataPost(ctx *context.Context) {
switch ctx.FormString("action") {
case "manifest":
saveManifest(ctx)
case "updateserver":
saveLicensingSettings(ctx)
case "customfields":
saveCustomFields(ctx)
default:
saveManifest(ctx)
}
}
func saveManifest(ctx *context.Context) {
manifest := &repo_model.RepoManifest{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"),
Org: ctx.FormString("org"),
Description: ctx.Repo.Repository.Description,
Version: ctx.FormString("version"),
LicenseSPDX: ctx.FormString("license_spdx"),
LicenseName: ctx.FormString("license_name"),
VersionPrefix: ctx.FormString("version_prefix"),
ElementName: ctx.FormString("element_name"),
Platform: ctx.FormString("platform"),
StandardsVersion: ctx.FormString("standards_version"),
StandardsSource: ctx.FormString("standards_source"),
DisplayName: ctx.FormString("display_name"),
Maintainer: ctx.FormString("maintainer"),
MaintainerURL: ctx.FormString("maintainer_url"),
InfoURL: ctx.FormString("info_url"),
TargetVersion: ctx.FormString("target_version"),
PHPMinimum: ctx.FormString("php_minimum"),
Language: ctx.FormString("language"),
PackageType: ctx.FormString("package_type"),
EntryPoint: ctx.FormString("entry_point"),
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
ctx.ServerError("CreateOrUpdateRepoManifest", err)
return
}
ctx.Flash.Success("Project identity saved")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
}
func saveCustomFields(ctx *context.Context) {
repoID := ctx.Repo.Repository.ID
ownerID := ctx.Repo.Repository.OwnerID
@@ -59,6 +134,45 @@ func MetadataPost(ctx *context.Context) {
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.metadata_saved"))
ctx.Flash.Success("Custom fields saved")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
}
// saveLicensingSettings handles the update server form.
func saveLicensingSettings(ctx *context.Context) {
repo := ctx.Repo.Repository
updatePlatform := ctx.FormString("update_platform")
if updatePlatform == "" {
updatePlatform = "joomla"
}
enabled := ctx.FormString("enable_licensing") == "on"
if !enabled {
if err := updateserver_model.DeleteRepoConfig(ctx, repo.ID); err != nil {
log.Error("DeleteRepoConfig: %v", err)
}
} else {
updateCfg := &updateserver_model.UpdateStreamConfig{
OwnerID: repo.OwnerID,
RepoID: repo.ID,
Platform: updatePlatform,
LicensingEnabled: true,
RequireKey: ctx.FormString("require_update_key") == "on",
DownloadGating: ctx.FormString("download_gating"),
FeedVisibility: ctx.FormString("feed_visibility"),
SupportURL: ctx.FormString("support_url"),
StreamMode: "joomla",
}
if err := updateserver_model.SaveConfig(ctx, updateCfg); err != nil {
log.Error("SaveConfig: %v", err)
ctx.ServerError("SaveConfig", err)
return
}
}
ctx.Flash.Success("Update server settings saved")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
}
+2 -2
View File
@@ -1229,9 +1229,9 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("", func() {
m.Combo("/advanced").Get(repo_setting.AdvancedSettings).Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost)
}, repo_setting.SettingsCtxData)
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost)
m.Combo("/manifest").Get(repo_setting.ManifestSettings).Post(repo_setting.ManifestSettingsPost)
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
m.Combo("/manifest").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect to metadata
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect to metadata
m.Group("/security", func() {
m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost)
m.Post("/scan", repo_setting.SecurityScanNow)
+250 -46
View File
@@ -1,49 +1,253 @@
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings metadata")}}
<div class="user-main-content twelve wide column">
<h4 class="ui top attached header">
{{svg "octicon-list-unordered" 16}} {{ctx.Locale.Tr "repo.settings.metadata"}}
</h4>
<div class="ui attached segment">
{{if .CustomFieldDefs}}
<form class="ui form" method="post" action="{{.RepoLink}}/settings/metadata">
{{.CsrfTokenHtml}}
{{$values := .CustomFieldValues}}
{{$options := .CustomFieldOptions}}
{{range .CustomFieldDefs}}
{{$currentVal := index $values .ID}}
<div class="field">
<label>{{.Name}}{{if .Description}} <small class="text grey">({{.Description}})</small>{{end}}</label>
{{if .Options}}
{{$opts := index $options .ID}}
<select name="field_{{.ID}}" class="ui dropdown">
<option value="">—</option>
{{range $opts}}
<option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
{{end}}
</select>
{{else if eq (printf "%s" .FieldType) "checkbox"}}
<div class="ui checkbox">
<input type="checkbox" name="field_{{.ID}}" value="true" {{if eq $currentVal "true"}}checked{{end}}>
<label></label>
</div>
{{else if eq (printf "%s" .FieldType) "number"}}
<input type="number" name="field_{{.ID}}" value="{{$currentVal}}">
{{else if eq (printf "%s" .FieldType) "url"}}
<input type="url" name="field_{{.ID}}" value="{{$currentVal}}" placeholder="https://...">
{{else if eq (printf "%s" .FieldType) "date"}}
<input type="date" name="field_{{.ID}}" value="{{$currentVal}}">
{{else}}
<input type="text" name="field_{{.ID}}" value="{{$currentVal}}">
{{end}}
</div>
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings metadata")}}
<h4 class="ui top attached header">
{{svg "octicon-file-code" 16}} Project Identity
</h4>
<div class="ui attached segment">
<form class="ui form" method="post" action="{{.RepoLink}}/settings/metadata?action=manifest">
{{.CsrfTokenHtml}}
<div class="two fields">
<div class="field">
{{if eq .Manifest.Platform "joomla"}}
<label>Element Name</label>
<input name="name" value="{{.Manifest.Name}}" placeholder="e.g. mokowaas">
{{else}}
<label>Project Name</label>
<input name="name" value="{{.Manifest.Name}}" placeholder="Project name">
{{end}}
<div class="field tw-mt-4">
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "save"}}</button>
</div>
</form>
{{else}}
<p class="text grey">{{ctx.Locale.Tr "repo.settings.metadata_empty"}}</p>
{{end}}
</div>
<div class="field">
<label>Organization</label>
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
</div>
</div>
</div>
<div class="four fields">
<div class="field">
<label>Version</label>
<input name="version" value="{{.Manifest.Version}}" placeholder="e.g. 06.00.00">
</div>
<div class="field">
<label>Version Prefix</label>
<input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko.">
</div>
<div class="field">
<label>License (SPDX)</label>
<input name="license_spdx" value="{{.Manifest.LicenseSPDX}}" placeholder="e.g. GPL-3.0-or-later">
</div>
<div class="field">
<label>License Name</label>
<input name="license_name" value="{{.Manifest.LicenseName}}" placeholder="e.g. GNU General Public License v3">
</div>
</div>
<h5 class="ui dividing header">Governance</h5>
<div class="three fields">
<div class="field">
<label>Platform</label>
<select name="platform" class="ui dropdown">
<option value="">—</option>
{{$platform := .Manifest.Platform}}
{{range $val := StringUtils.Split "joomla,wordpress,dolibarr,go,mcp,platform,generic" ","}}
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
<div class="field">
<label>Standards Version</label>
<input name="standards_version" value="{{.Manifest.StandardsVersion}}" placeholder="e.g. 05.00.00">
</div>
<div class="field">
<label>Standards Source</label>
<input name="standards_source" value="{{.Manifest.StandardsSource}}" placeholder="URL to standards repo">
</div>
</div>
{{if or (eq .Manifest.Platform "joomla") (eq .Manifest.Platform "wordpress") (eq .Manifest.Platform "dolibarr")}}
<h5 class="ui dividing header">Distribution</h5>
<div class="two fields">
<div class="field">
<label>Display Name</label>
<input name="display_name" value="{{.Manifest.DisplayName}}" placeholder="e.g. Package - MokoWaaS">
</div>
<div class="field">
<label>Info URL</label>
<input name="info_url" value="{{.Manifest.InfoURL}}" placeholder="https://mokoconsulting.tech/product/...">
</div>
</div>
<div class="two fields">
<div class="field">
<label>Maintainer</label>
<input name="maintainer" value="{{.Manifest.Maintainer}}" placeholder="Moko Consulting">
</div>
<div class="field">
<label>Maintainer URL</label>
<input name="maintainer_url" value="{{.Manifest.MaintainerURL}}" placeholder="https://mokoconsulting.tech">
</div>
</div>
{{if or (eq .Manifest.Platform "joomla") (eq .Manifest.Platform "wordpress")}}
<div class="two fields">
<div class="field">
<label>Target Platform Version</label>
<input name="target_version" value="{{.Manifest.TargetVersion}}" placeholder="e.g. (5|6)\..*">
</div>
<div class="field">
<label>PHP Minimum</label>
<input name="php_minimum" value="{{.Manifest.PHPMinimum}}" placeholder="e.g. 8.1">
</div>
</div>
{{end}}
{{end}}
<h5 class="ui dividing header">Build</h5>
<div class="three fields">
<div class="field">
<label>Language</label>
<select name="language" class="ui dropdown">
<option value="">—</option>
{{$lang := .Manifest.Language}}
{{range $val := StringUtils.Split "Go,PHP,TypeScript,JavaScript,Python,Ruby,Java,C#,Rust,Shell,SQL,CSS,HTML" ","}}
<option value="{{$val}}" {{if eq $val $lang}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
{{if eq .Manifest.Platform "joomla"}}
<div class="field">
<label>Package Type</label>
<select name="package_type" class="ui dropdown">
<option value="">—</option>
{{$pkgType := .Manifest.PackageType}}
{{range $val := StringUtils.Split "component,module,plugin,package,template,library,file" ","}}
<option value="{{$val}}" {{if eq $val $pkgType}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
<div class="field">
<label>Element Name (full)</label>
<input name="element_name" value="{{.Manifest.ElementName}}" placeholder="{{.Manifest.AutoElementName}}">
</div>
{{end}}
<div class="field">
<label>Entry Point</label>
<input name="entry_point" value="{{.Manifest.EntryPoint}}" placeholder="e.g. ./ or src/index.ts">
</div>
</div>
<button class="ui primary button" type="submit">Save Project Identity</button>
</form>
</div>
{{if .LicensingEnabled}}
<h4 class="ui top attached header">
{{svg "octicon-broadcast" 16}} Update Server
</h4>
<div class="ui attached segment">
<form class="ui form" method="post" action="{{.RepoLink}}/settings/metadata?action=updateserver">
{{.CsrfTokenHtml}}
<div class="inline field">
<div class="ui checkbox">
<input name="enable_licensing" type="checkbox" {{if and .RepoUpdateConfig .RepoUpdateConfig.LicensingEnabled}}checked{{end}}>
<label><strong>Enable Update Server</strong></label>
</div>
<p class="help">Serve update feeds from releases and show the Licenses tab for optional key management.</p>
</div>
<div class="ui divider"></div>
<div class="inline field">
<label>Feed Platform</label>
<select name="update_platform" class="ui dropdown">
<option value="joomla" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.Platform "joomla") (eq .RepoUpdateConfig.Platform "")}}selected{{end}}>Joomla (updates.xml)</option>
<option value="dolibarr" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "dolibarr")}}selected{{end}}>Dolibarr (JSON)</option>
<option value="wordpress" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "wordpress")}}selected{{end}}>WordPress (JSON)</option>
<option value="composer" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "composer")}}selected{{end}}>Composer (packages.json)</option>
<option value="prestashop" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "prestashop")}}selected{{end}}>PrestaShop</option>
<option value="drupal" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "drupal")}}selected{{end}}>Drupal</option>
<option value="both" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "both")}}selected{{end}}>Both (Joomla + Dolibarr)</option>
</select>
</div>
<div class="inline field">
<label>Feed Visibility</label>
<select name="feed_visibility" class="ui dropdown">
<option value="public" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.FeedVisibility "") (eq .RepoUpdateConfig.FeedVisibility "public")}}selected{{end}}>Public (show versions and downloads)</option>
<option value="no-download" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.FeedVisibility "no-download")}}selected{{end}}>No downloads (show versions only)</option>
<option value="hidden" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.FeedVisibility "hidden")}}selected{{end}}>Hidden (require license key)</option>
</select>
</div>
<div class="inline field">
<label>Download Gating</label>
<select name="download_gating" class="ui dropdown">
<option value="none" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.DownloadGating "") (eq .RepoUpdateConfig.DownloadGating "none")}}selected{{end}}>None</option>
<option value="prerelease" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "prerelease")}}selected{{end}}>Pre-release only</option>
<option value="all" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "all")}}selected{{end}}>All releases</option>
</select>
</div>
{{if and .RepoUpdateConfig (ne .RepoUpdateConfig.Platform "joomla") (ne .RepoUpdateConfig.Platform "both") (ne .RepoUpdateConfig.Platform "")}}
<div class="inline field">
<div class="ui checkbox">
<input name="require_update_key" type="checkbox" {{if .RepoUpdateConfig.RequireKey}}checked{{end}}>
<label>Require license key for update feed</label>
</div>
</div>
{{end}}
<div class="inline field">
<label>Support URL</label>
<input name="support_url" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.SupportURL}}{{end}}" placeholder="https://mokoconsulting.tech/support">
</div>
<div class="field">
<button class="ui primary button">Save Update Server</button>
</div>
</form>
</div>
{{end}}
{{if .CustomFieldDefs}}
<h4 class="ui top attached header">
{{svg "octicon-list-unordered" 16}} Custom Fields
</h4>
<div class="ui attached segment">
<form class="ui form" method="post" action="{{.RepoLink}}/settings/metadata?action=customfields">
{{.CsrfTokenHtml}}
{{$values := .CustomFieldValues}}
{{$options := .CustomFieldOptions}}
{{range .CustomFieldDefs}}
{{$currentVal := index $values .ID}}
<div class="field">
<label>{{.Name}}{{if .Required}} <span class="tw-text-red">*</span>{{end}}{{if .Description}} <small class="text grey">({{.Description}})</small>{{end}}</label>
{{if .Options}}
{{$opts := index $options .ID}}
<select name="field_{{.ID}}" class="ui dropdown" {{if .Required}}required{{end}}>
<option value="">—</option>
{{range $opts}}
<option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
{{end}}
</select>
{{else if eq (printf "%s" .FieldType) "checkbox"}}
<div class="ui checkbox">
<input type="checkbox" name="field_{{.ID}}" value="true" {{if eq $currentVal "true"}}checked{{end}}>
<label></label>
</div>
{{else if eq (printf "%s" .FieldType) "number"}}
<input type="number" name="field_{{.ID}}" value="{{$currentVal}}" {{if .Required}}required{{end}}>
{{else if eq (printf "%s" .FieldType) "url"}}
<input type="url" name="field_{{.ID}}" value="{{$currentVal}}" placeholder="https://..." {{if .Required}}required{{end}}>
{{else if eq (printf "%s" .FieldType) "date"}}
<input type="date" name="field_{{.ID}}" value="{{$currentVal}}" {{if .Required}}required{{end}}>
{{else}}
<input type="text" name="field_{{.ID}}" value="{{$currentVal}}" {{if .Required}}required{{end}}>
{{end}}
</div>
{{end}}
<div class="field tw-mt-4">
<button class="ui primary button" type="submit">Save Custom Fields</button>
</div>
</form>
</div>
{{end}}
{{template "repo/settings/layout_footer" .}}
+1 -4
View File
@@ -7,11 +7,8 @@
<a class="{{if .PageIsSettingsAdvanced}}active {{end}}item" href="{{.RepoLink}}/settings/advanced">
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings.advanced_settings"}}
</a>
<a class="{{if .PageIsSettingsManifest}}active {{end}}item" href="{{.RepoLink}}/settings/manifest">
{{svg "octicon-file-code"}} {{ctx.Locale.Tr "repo.settings.manifest"}}
</a>
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}}
{{svg "octicon-file-code"}} Metadata
</a>
<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{.RepoLink}}/settings/security">
{{svg "octicon-shield"}} {{ctx.Locale.Tr "repo.settings.security"}}