From 6bd9548b2a298b9835e21a6dd027eb2b445e15b4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 19:11:22 -0500 Subject: [PATCH] feat(custom-fields): move to org-level definitions with issue and repo scopes - CustomFieldDef now has owner_id (org) and scope (issue/repo) - Issue sidebar loads fields by org owner_id, not repo_id - Org Settings > Custom Fields page for managing field definitions - Repo Settings > Metadata page for filling in repo-scoped values - Migration v345 adds owner_id, scope, entity_id, entity_type columns - Per-repo custom field management replaced by org-level - Replaces .mokogitea/manifest.xml with database-backed metadata Co-Authored-By: Claude Opus 4.6 (1M context) --- models/issues/custom_field.go | 100 ++++++++++++++---- models/migrations/migrations.go | 1 + models/migrations/v1_27/v345.go | 34 ++++++ options/locale/locale_en-US.json | 16 +++ routers/web/org/custom_fields.go | 120 ++++++++++++++++++++++ routers/web/repo/setting/metadata.go | 64 ++++++++++++ routers/web/web.go | 13 +-- templates/org/settings/custom_fields.tmpl | 85 +++++++++++++++ templates/org/settings/navbar.tmpl | 3 + templates/repo/settings/metadata.tmpl | 49 +++++++++ templates/repo/settings/navbar.tmpl | 4 +- 11 files changed, 461 insertions(+), 28 deletions(-) create mode 100644 models/migrations/v1_27/v345.go create mode 100644 routers/web/org/custom_fields.go create mode 100644 routers/web/repo/setting/metadata.go create mode 100644 templates/org/settings/custom_fields.tmpl create mode 100644 templates/repo/settings/metadata.tmpl diff --git a/models/issues/custom_field.go b/models/issues/custom_field.go index 2d56b94034..d4aeeb1979 100644 --- a/models/issues/custom_field.go +++ b/models/issues/custom_field.go @@ -27,14 +27,26 @@ const ( CustomFieldTypeURL CustomFieldType = "url" ) -// CustomFieldDef defines a custom field available for issues in a repository. +// CustomFieldScope determines where the field appears. +type CustomFieldScope string + +const ( + CustomFieldScopeIssue CustomFieldScope = "issue" // appears in issue sidebar + CustomFieldScopeRepo CustomFieldScope = "repo" // appears in repo settings metadata +) + +// CustomFieldDef defines a custom field at the org level. +// owner_id = org ID, scope = issue or repo. +// repo_id is kept for backward compat but 0 for org-level definitions. type CustomFieldDef struct { ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX NOT NULL 'repo_id'"` + OwnerID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'owner_id'"` // org that owns this field + RepoID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'repo_id'"` // 0 = org-level (inherited by all repos) + Scope CustomFieldScope `xorm:"VARCHAR(10) NOT NULL DEFAULT 'issue' 'scope'"` Name string `xorm:"NOT NULL"` FieldType CustomFieldType `xorm:"VARCHAR(20) NOT NULL 'field_type'"` Description string `xorm:"TEXT"` - Options string `xorm:"TEXT"` // JSON array for dropdown options + Options string `xorm:"TEXT"` // JSON array for dropdown options Required bool `xorm:"NOT NULL DEFAULT false"` SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"` IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"` @@ -46,10 +58,11 @@ func (CustomFieldDef) TableName() string { return "custom_field_def" } -// CustomFieldValue stores a custom field value for a specific issue. +// CustomFieldValue stores a custom field value for an entity (issue or repo). type CustomFieldValue struct { ID int64 `xorm:"pk autoincr"` - IssueID int64 `xorm:"INDEX NOT NULL 'issue_id'"` + EntityID int64 `xorm:"INDEX NOT NULL 'entity_id'"` // issue ID or repo ID + EntityType string `xorm:"VARCHAR(10) NOT NULL DEFAULT 'issue' 'entity_type'"` // "issue" or "repo" FieldID int64 `xorm:"INDEX NOT NULL 'field_id'"` Value string `xorm:"TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"` @@ -60,16 +73,55 @@ func (CustomFieldValue) TableName() string { return "custom_field_value" } -// GetCustomFieldsByRepo returns all active custom field definitions for a repo. -func GetCustomFieldsByRepo(ctx context.Context, repoID int64) ([]*CustomFieldDef, error) { +// ────────────────────────────────────────────────────────────────────── +// Queries for org-level field definitions +// ────────────────────────────────────────────────────────────────────── + +// GetCustomFieldsByOwner returns all active field definitions for an org with a given scope. +func GetCustomFieldsByOwner(ctx context.Context, ownerID int64, scope CustomFieldScope) ([]*CustomFieldDef, error) { fields := make([]*CustomFieldDef, 0, 10) return fields, db.GetEngine(ctx). - Where("repo_id = ? AND is_active = ?", repoID, true). + Where("owner_id = ? AND scope = ? AND is_active = ?", ownerID, scope, true). OrderBy("sort_order ASC, id ASC"). Find(&fields) } -// GetAllCustomFieldsByRepo returns all custom field definitions including inactive. +// GetAllCustomFieldsByOwner returns all field definitions for an org (including inactive). +func GetAllCustomFieldsByOwner(ctx context.Context, ownerID int64) ([]*CustomFieldDef, error) { + fields := make([]*CustomFieldDef, 0, 10) + return fields, db.GetEngine(ctx). + Where("owner_id = ?", ownerID). + OrderBy("scope ASC, sort_order ASC, id ASC"). + Find(&fields) +} + +// GetCustomFieldsByOwnerAndScope returns all fields for an org filtered by scope. +func GetCustomFieldsByOwnerAndScope(ctx context.Context, ownerID int64, scope CustomFieldScope) ([]*CustomFieldDef, error) { + fields := make([]*CustomFieldDef, 0, 10) + return fields, db.GetEngine(ctx). + Where("owner_id = ? AND scope = ?", ownerID, scope). + OrderBy("sort_order ASC, id ASC"). + Find(&fields) +} + +// ────────────────────────────────────────────────────────────────────── +// Backward-compatible queries (load by repo's owner) +// ────────────────────────────────────────────────────────────────────── + +// GetCustomFieldsByRepo returns active issue-scoped fields for a repo's org. +// This is the main query used by the issue sidebar. +func GetCustomFieldsByRepo(ctx context.Context, repoID int64) ([]*CustomFieldDef, error) { + // First try org-level fields (owner_id != 0, repo_id = 0) + // Fall back to legacy repo-level fields (repo_id = repoID) + fields := make([]*CustomFieldDef, 0, 10) + return fields, db.GetEngine(ctx). + Where("((owner_id != 0 AND repo_id = 0) OR repo_id = ?) AND scope = ? AND is_active = ?", + repoID, CustomFieldScopeIssue, true). + OrderBy("sort_order ASC, id ASC"). + Find(&fields) +} + +// GetAllCustomFieldsByRepo returns all field definitions for a repo (for settings page). func GetAllCustomFieldsByRepo(ctx context.Context, repoID int64) ([]*CustomFieldDef, error) { fields := make([]*CustomFieldDef, 0, 10) return fields, db.GetEngine(ctx). @@ -78,6 +130,10 @@ func GetAllCustomFieldsByRepo(ctx context.Context, repoID int64) ([]*CustomField Find(&fields) } +// ────────────────────────────────────────────────────────────────────── +// Field definition CRUD +// ────────────────────────────────────────────────────────────────────── + // GetCustomFieldDefByID returns a single field definition. func GetCustomFieldDefByID(ctx context.Context, id int64) (*CustomFieldDef, error) { field := new(CustomFieldDef) @@ -112,10 +168,14 @@ func DeleteCustomFieldDef(ctx context.Context, id int64) error { return err } -// GetCustomFieldValuesMap returns field_id -> value for an issue. -func GetCustomFieldValuesMap(ctx context.Context, issueID int64) (map[int64]string, error) { +// ────────────────────────────────────────────────────────────────────── +// Field values — generic entity-based (works for issues and repos) +// ────────────────────────────────────────────────────────────────────── + +// GetCustomFieldValuesMap returns field_id -> value for an entity. +func GetCustomFieldValuesMap(ctx context.Context, entityID int64) (map[int64]string, error) { values := make([]*CustomFieldValue, 0, 10) - if err := db.GetEngine(ctx).Where("issue_id = ?", issueID).Find(&values); err != nil { + if err := db.GetEngine(ctx).Where("entity_id = ?", entityID).Find(&values); err != nil { return nil, err } result := make(map[int64]string, len(values)) @@ -126,9 +186,9 @@ func GetCustomFieldValuesMap(ctx context.Context, issueID int64) (map[int64]stri } // SetCustomFieldValue creates or updates a single custom field value. -func SetCustomFieldValue(ctx context.Context, issueID, fieldID int64, value string) error { +func SetCustomFieldValue(ctx context.Context, entityID, fieldID int64, value string) error { existing := new(CustomFieldValue) - has, err := db.GetEngine(ctx).Where("issue_id = ? AND field_id = ?", issueID, fieldID).Get(existing) + has, err := db.GetEngine(ctx).Where("entity_id = ? AND field_id = ?", entityID, fieldID).Get(existing) if err != nil { return err } @@ -138,17 +198,17 @@ func SetCustomFieldValue(ctx context.Context, issueID, fieldID int64, value stri return err } _, err = db.GetEngine(ctx).Insert(&CustomFieldValue{ - IssueID: issueID, - FieldID: fieldID, - Value: value, + EntityID: entityID, + FieldID: fieldID, + Value: value, }) return err } -// SetCustomFieldValues sets multiple custom field values for an issue. -func SetCustomFieldValues(ctx context.Context, issueID int64, values map[int64]string) error { +// SetCustomFieldValues sets multiple custom field values for an entity. +func SetCustomFieldValues(ctx context.Context, entityID int64, values map[int64]string) error { for fieldID, value := range values { - if err := SetCustomFieldValue(ctx, issueID, fieldID, value); err != nil { + if err := SetCustomFieldValue(ctx, entityID, fieldID, value); err != nil { return err } } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index b9315a029c..0c0a515881 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -422,6 +422,7 @@ func prepareMigrationTasks() []*migration { newMigration(342, "Add is_hidden to repository for three-level visibility", v1_27.AddIsHiddenToRepository), newMigration(343, "Add custom field tables for issue custom fields", v1_27.AddCustomFieldTables), newMigration(344, "Add domain_restriction to license_package table", v1_27.AddDomainRestrictionToLicensePackage), + newMigration(345, "Migrate custom fields to org-level with scope", v1_27.MigrateCustomFieldsToOrgLevel), } return preparedMigrations } diff --git a/models/migrations/v1_27/v345.go b/models/migrations/v1_27/v345.go new file mode 100644 index 0000000000..026cc1da8d --- /dev/null +++ b/models/migrations/v1_27/v345.go @@ -0,0 +1,34 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package v1_27 + +import ( + "xorm.io/xorm" +) + +// MigrateCustomFieldsToOrgLevel adds owner_id, scope to custom_field_def +// and renames issue_id to entity_id + adds entity_type in custom_field_value. +func MigrateCustomFieldsToOrgLevel(x *xorm.Engine) error { + // Add new columns to custom_field_def + type CustomFieldDef struct { + OwnerID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'owner_id'"` + Scope string `xorm:"VARCHAR(10) NOT NULL DEFAULT 'issue' 'scope'"` + } + if err := x.Sync(new(CustomFieldDef)); err != nil { + return err + } + + // Add entity_type and entity_id to custom_field_value + type CustomFieldValue struct { + EntityID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'entity_id'"` + EntityType string `xorm:"VARCHAR(10) NOT NULL DEFAULT 'issue' 'entity_type'"` + } + if err := x.Sync(new(CustomFieldValue)); err != nil { + return err + } + + // Migrate existing data: copy issue_id to entity_id where entity_id is 0 + _, err := x.Exec("UPDATE custom_field_value SET entity_id = issue_id WHERE entity_id = 0 AND issue_id != 0") + return err +} diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index c3f2e899c2..f0fa63ad5c 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2722,6 +2722,9 @@ "repo.settings.support_url": "Support / Product Page URL", "repo.settings.support_url_help": "Shown when downloads are gated. Can point to your wiki, product page, or external support site.", "repo.settings.custom_fields": "Custom Fields", + "repo.settings.metadata": "Metadata", + "repo.settings.metadata_saved": "Repository metadata saved.", + "repo.settings.metadata_empty": "No metadata fields defined. Org admins can add fields in Organization Settings > Custom Fields.", "repo.settings.custom_field_new": "New Field", "repo.settings.custom_field_create": "Create Field", "repo.settings.custom_field_name": "Field Name", @@ -2895,6 +2898,19 @@ "org.form.create_org_not_allowed": "You are not allowed to create an organization.", "org.settings": "Settings", "org.settings.options": "Organization", + "org.settings.custom_fields": "Custom Fields", + "org.settings.custom_fields_desc": "Define custom fields that appear across all repositories in this organization. Issue fields show in issue sidebars. Repo fields show in repo settings metadata.", + "org.settings.custom_fields_empty": "No custom fields defined yet.", + "org.settings.custom_field_add": "Add Custom Field", + "org.settings.custom_field_name": "Field Name", + "org.settings.custom_field_scope": "Scope", + "org.settings.custom_field_type": "Type", + "org.settings.custom_field_options": "Options (JSON)", + "org.settings.custom_field_options_help": "For dropdown fields, enter options as a JSON array.", + "org.settings.custom_field_description": "Description", + "org.settings.custom_field_created": "Custom field created.", + "org.settings.custom_field_updated": "Custom field updated.", + "org.settings.custom_field_deleted": "Custom field deleted.", "org.settings.update_streams": "Update Server", "org.settings.licensing": "Update Server", "org.settings.licensing_desc": "Manage update feeds and optional license key gating across all repositories in this organization.", diff --git a/routers/web/org/custom_fields.go b/routers/web/org/custom_fields.go new file mode 100644 index 0000000000..97b7fc787c --- /dev/null +++ b/routers/web/org/custom_fields.go @@ -0,0 +1,120 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package org + +import ( + "net/http" + "strconv" + + issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" +) + +const tplOrgCustomFields templates.TplName = "org/settings/custom_fields" + +// SettingsCustomFields shows the org-level custom fields management page. +func SettingsCustomFields(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("org.settings.custom_fields") + ctx.Data["PageIsOrgSettings"] = true + ctx.Data["PageIsSettingsCustomFields"] = true + + fields, err := issues_model.GetAllCustomFieldsByOwner(ctx, ctx.Org.Organization.ID) + if err != nil { + ctx.ServerError("GetAllCustomFieldsByOwner", err) + return + } + ctx.Data["CustomFields"] = fields + + ctx.HTML(http.StatusOK, tplOrgCustomFields) +} + +// SettingsCustomFieldsCreatePost creates a new org-level custom field. +func SettingsCustomFieldsCreatePost(ctx *context.Context) { + sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order")) + scope := issues_model.CustomFieldScope(ctx.FormString("scope")) + if scope != issues_model.CustomFieldScopeIssue && scope != issues_model.CustomFieldScopeRepo { + scope = issues_model.CustomFieldScopeIssue + } + + field := &issues_model.CustomFieldDef{ + OwnerID: ctx.Org.Organization.ID, + RepoID: 0, // org-level + Scope: scope, + Name: ctx.FormString("name"), + FieldType: issues_model.CustomFieldType(ctx.FormString("field_type")), + Description: ctx.FormString("description"), + Options: ctx.FormString("options"), + Required: ctx.FormString("required") == "on", + SortOrder: sortOrder, + IsActive: true, + } + + if field.Name == "" { + ctx.Flash.Error("Field name is required") + ctx.Redirect(ctx.Org.OrgLink + "/settings/custom-fields") + return + } + + if err := issues_model.CreateCustomFieldDef(ctx, field); err != nil { + ctx.ServerError("CreateCustomFieldDef", err) + return + } + + ctx.Flash.Success(ctx.Tr("org.settings.custom_field_created")) + ctx.Redirect(ctx.Org.OrgLink + "/settings/custom-fields") +} + +// SettingsCustomFieldsEditPost updates an org-level custom field. +func SettingsCustomFieldsEditPost(ctx *context.Context) { + id := ctx.PathParamInt64("id") + field, err := issues_model.GetCustomFieldDefByID(ctx, id) + if err != nil { + ctx.ServerError("GetCustomFieldDefByID", err) + return + } + if field.OwnerID != ctx.Org.Organization.ID { + ctx.NotFound(nil) + return + } + + field.Name = ctx.FormString("name") + field.FieldType = issues_model.CustomFieldType(ctx.FormString("field_type")) + field.Description = ctx.FormString("description") + field.Options = ctx.FormString("options") + field.Required = ctx.FormString("required") == "on" + field.IsActive = ctx.FormString("is_active") == "on" + sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order")) + field.SortOrder = sortOrder + + if err := issues_model.UpdateCustomFieldDef(ctx, field); err != nil { + ctx.ServerError("UpdateCustomFieldDef", err) + return + } + + ctx.Flash.Success(ctx.Tr("org.settings.custom_field_updated")) + ctx.Redirect(ctx.Org.OrgLink + "/settings/custom-fields") +} + +// SettingsCustomFieldsDeletePost deletes an org-level custom field. +func SettingsCustomFieldsDeletePost(ctx *context.Context) { + id := ctx.PathParamInt64("id") + field, err := issues_model.GetCustomFieldDefByID(ctx, id) + if err != nil { + ctx.ServerError("GetCustomFieldDefByID", err) + return + } + if field.OwnerID != ctx.Org.Organization.ID { + ctx.NotFound(nil) + return + } + + if err := issues_model.DeleteCustomFieldDef(ctx, id); err != nil { + ctx.ServerError("DeleteCustomFieldDef", err) + return + } + + ctx.Flash.Success(ctx.Tr("org.settings.custom_field_deleted")) + ctx.Redirect(ctx.Org.OrgLink + "/settings/custom-fields") +} diff --git a/routers/web/repo/setting/metadata.go b/routers/web/repo/setting/metadata.go new file mode 100644 index 0000000000..bb571dbcdf --- /dev/null +++ b/routers/web/repo/setting/metadata.go @@ -0,0 +1,64 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package setting + +import ( + "encoding/json" + "fmt" + "net/http" + + issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues" + "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). +func Metadata(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.settings.metadata") + ctx.Data["PageIsSettingsMetadata"] = true + + ownerID := ctx.Repo.Repository.OwnerID + repoID := ctx.Repo.Repository.ID + + fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo) + ctx.Data["CustomFieldDefs"] = fields + + values := make(map[int64]string) + fieldOptions := make(map[int64][]string) + if len(fields) > 0 { + values, _ = issues_model.GetCustomFieldValuesMap(ctx, repoID) + for _, f := range fields { + if f.Options != "" { + var opts []string + if err := json.Unmarshal([]byte(f.Options), &opts); err == nil { + fieldOptions[f.ID] = opts + } + } + } + } + ctx.Data["CustomFieldValues"] = values + ctx.Data["CustomFieldOptions"] = fieldOptions + + ctx.HTML(http.StatusOK, tplSettingsMetadata) +} + +// MetadataPost saves repo-scoped custom field values. +func MetadataPost(ctx *context.Context) { + repoID := ctx.Repo.Repository.ID + ownerID := ctx.Repo.Repository.OwnerID + + fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo) + for _, f := range fields { + val := ctx.Req.FormValue(fmt.Sprintf("field_%d", f.ID)) + if err := issues_model.SetCustomFieldValue(ctx, repoID, f.ID, val); err != nil { + ctx.ServerError("SetCustomFieldValue", err) + return + } + } + + ctx.Flash.Success(ctx.Tr("repo.settings.metadata_saved")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata") +} diff --git a/routers/web/web.go b/routers/web/web.go index 745d4d34e0..98b7eb3e9f 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1061,6 +1061,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("", org.SettingsUpdateStreams) m.Post("", org.SettingsUpdateStreamsPost) }) + m.Group("/custom-fields", func() { + m.Get("", org.SettingsCustomFields) + m.Post("", org.SettingsCustomFieldsCreatePost) + m.Post("/{id}/edit", org.SettingsCustomFieldsEditPost) + m.Post("/{id}/delete", org.SettingsCustomFieldsDeletePost) + }) }, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true)) }, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true})) }, reqSignIn) @@ -1187,12 +1193,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { 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.Group("/custom-fields", func() { - m.Get("", repo_setting.CustomFields) - m.Post("", repo_setting.CustomFieldsCreatePost) - m.Post("/{id}/edit", repo_setting.CustomFieldsEditPost) - m.Post("/{id}/delete", repo_setting.CustomFieldsDeletePost) - }) + m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost) m.Group("/collaboration", func() { m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost) diff --git a/templates/org/settings/custom_fields.tmpl b/templates/org/settings/custom_fields.tmpl new file mode 100644 index 0000000000..37809fdab9 --- /dev/null +++ b/templates/org/settings/custom_fields.tmpl @@ -0,0 +1,85 @@ +{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings custom-fields")}} +

+ {{ctx.Locale.Tr "org.settings.custom_fields"}} +

+
+

{{ctx.Locale.Tr "org.settings.custom_fields_desc"}}

+ + {{if .CustomFields}} + + + + + + + + + + + + {{range .CustomFields}} + + + + + + + + {{end}} + +
{{ctx.Locale.Tr "org.settings.custom_field_name"}}{{ctx.Locale.Tr "org.settings.custom_field_scope"}}{{ctx.Locale.Tr "org.settings.custom_field_type"}}{{ctx.Locale.Tr "org.settings.custom_field_options"}}
{{.Name}}{{if .Description}}
{{.Description}}{{end}}
{{if eq .Scope "issue"}}{{svg "octicon-issue-opened" 14}} Issue{{else}}{{svg "octicon-repo" 14}} Repo{{end}}{{.FieldType}}{{if .Options}}{{.Options}}{{else}}-{{end}} +
+ {{$.CsrfTokenHtml}} + +
+
+ {{else}} +
+

{{ctx.Locale.Tr "org.settings.custom_fields_empty"}}

+
+ {{end}} + +
+ +
{{ctx.Locale.Tr "org.settings.custom_field_add"}}
+
+ {{.CsrfTokenHtml}} +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +

{{ctx.Locale.Tr "org.settings.custom_field_options_help"}}

+
+
+ + +
+
+ +
+
+{{template "org/settings/layout_footer" .}} diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl index e71dcaf4a2..85417f1870 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -28,6 +28,9 @@ {{svg "octicon-broadcast"}} {{ctx.Locale.Tr "org.settings.update_streams"}} + + {{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "org.settings.custom_fields"}} + {{if .EnableActions}}
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}} diff --git a/templates/repo/settings/metadata.tmpl b/templates/repo/settings/metadata.tmpl new file mode 100644 index 0000000000..896c7cc942 --- /dev/null +++ b/templates/repo/settings/metadata.tmpl @@ -0,0 +1,49 @@ +{{template "repo/settings/layout_head" (dict "pageClass" "repository settings metadata")}} +
+

+ {{svg "octicon-list-unordered" 16}} {{ctx.Locale.Tr "repo.settings.metadata"}} +

+
+ {{if .CustomFieldDefs}} +
+ {{.CsrfTokenHtml}} + {{$values := .CustomFieldValues}} + {{$options := .CustomFieldOptions}} + {{range .CustomFieldDefs}} + {{$currentVal := index $values .ID}} +
+ + {{if .Options}} + {{$opts := index $options .ID}} + + {{else if eq (printf "%s" .FieldType) "checkbox"}} +
+ + +
+ {{else if eq (printf "%s" .FieldType) "number"}} + + {{else if eq (printf "%s" .FieldType) "url"}} + + {{else if eq (printf "%s" .FieldType) "date"}} + + {{else}} + + {{end}} +
+ {{end}} +
+ +
+
+ {{else}} +

{{ctx.Locale.Tr "repo.settings.metadata_empty"}}

+ {{end}} +
+
+{{template "repo/settings/layout_footer" .}} diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl index 85645c6fcf..0d7ac89133 100644 --- a/templates/repo/settings/navbar.tmpl +++ b/templates/repo/settings/navbar.tmpl @@ -12,8 +12,8 @@ {{svg "octicon-broadcast"}} {{ctx.Locale.Tr "repo.settings.licensing_section"}} {{end}} - - {{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.custom_fields"}} + + {{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}} {{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}} -- 2.52.0