feat(custom-fields): org-level definitions with issue and repo scopes #489

Merged
jmiller merged 1 commits from dev into main 2026-06-05 00:12:25 +00:00
11 changed files with 461 additions and 28 deletions
+80 -20
View File
@@ -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
}
}
+1
View File
@@ -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
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// 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
}
+16
View File
@@ -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.",
+120
View File
@@ -0,0 +1,120 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// 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")
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// 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")
}
+7 -6
View File
@@ -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)
+85
View File
@@ -0,0 +1,85 @@
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings custom-fields")}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "org.settings.custom_fields"}}
</h4>
<div class="ui attached segment">
<p class="text grey">{{ctx.Locale.Tr "org.settings.custom_fields_desc"}}</p>
{{if .CustomFields}}
<table class="ui compact table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "org.settings.custom_field_name"}}</th>
<th>{{ctx.Locale.Tr "org.settings.custom_field_scope"}}</th>
<th>{{ctx.Locale.Tr "org.settings.custom_field_type"}}</th>
<th>{{ctx.Locale.Tr "org.settings.custom_field_options"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .CustomFields}}
<tr>
<td><strong>{{.Name}}</strong>{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}</td>
<td>{{if eq .Scope "issue"}}{{svg "octicon-issue-opened" 14}} Issue{{else}}{{svg "octicon-repo" 14}} Repo{{end}}</td>
<td><code>{{.FieldType}}</code></td>
<td>{{if .Options}}<code class="tw-text-xs">{{.Options}}</code>{{else}}<span class="text grey">-</span>{{end}}</td>
<td class="tw-text-right">
<form method="post" action="{{$.OrgLink}}/settings/custom-fields/{{.ID}}/delete" class="tw-inline">
{{$.CsrfTokenHtml}}
<button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="empty-placeholder">
<p>{{ctx.Locale.Tr "org.settings.custom_fields_empty"}}</p>
</div>
{{end}}
<div class="divider"></div>
<h5>{{ctx.Locale.Tr "org.settings.custom_field_add"}}</h5>
<form class="ui form" method="post" action="{{.OrgLink}}/settings/custom-fields">
{{.CsrfTokenHtml}}
<div class="three fields">
<div class="required field">
<label>{{ctx.Locale.Tr "org.settings.custom_field_name"}}</label>
<input name="name" required placeholder="e.g. Status, Platform, Priority">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.custom_field_scope"}}</label>
<select name="scope" class="ui dropdown">
<option value="issue">{{svg "octicon-issue-opened" 14}} Issue (sidebar)</option>
<option value="repo">{{svg "octicon-repo" 14}} Repo (metadata)</option>
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.custom_field_type"}}</label>
<select name="field_type" class="ui dropdown">
<option value="dropdown">Dropdown</option>
<option value="text">Text</option>
<option value="number">Number</option>
<option value="date">Date</option>
<option value="checkbox">Checkbox</option>
<option value="url">URL</option>
</select>
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.custom_field_options"}}</label>
<input name="options" placeholder='["Option 1","Option 2","Option 3"]'>
<p class="help">{{ctx.Locale.Tr "org.settings.custom_field_options_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.custom_field_description"}}</label>
<input name="description" placeholder="Help text shown to users">
</div>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "org.settings.custom_field_add"}}</button>
</form>
</div>
{{template "org/settings/layout_footer" .}}
+3
View File
@@ -28,6 +28,9 @@
<a class="{{if .PageIsSettingsUpdateStreams}}active {{end}}item" href="{{.OrgLink}}/settings/update-streams">
{{svg "octicon-broadcast"}} {{ctx.Locale.Tr "org.settings.update_streams"}}
</a>
<a class="{{if .PageIsSettingsCustomFields}}active {{end}}item" href="{{.OrgLink}}/settings/custom-fields">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "org.settings.custom_fields"}}
</a>
{{if .EnableActions}}
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
+49
View File
@@ -0,0 +1,49 @@
{{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>
{{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>
{{template "repo/settings/layout_footer" .}}
+2 -2
View File
@@ -12,8 +12,8 @@
{{svg "octicon-broadcast"}} {{ctx.Locale.Tr "repo.settings.licensing_section"}}
</a>
{{end}}
<a class="{{if .PageIsSettingsCustomFields}}active {{end}}item" href="{{.RepoLink}}/settings/custom-fields">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.custom_fields"}}
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}}
</a>
{{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}}
<a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access">