feat(custom-fields): org-level definitions with issue and repo scopes #489
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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" .}}
|
||||
@@ -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>
|
||||
|
||||
@@ -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" .}}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user