feat(issues): custom fields foundation #447

Merged
jmiller merged 3 commits from dev into main 2026-06-04 11:48:47 +00:00
9 changed files with 406 additions and 168 deletions
+9 -115
View File
@@ -11,7 +11,7 @@
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================
name: "Generic: Repo Health"
@@ -24,13 +24,12 @@ on:
workflow_dispatch:
inputs:
profile:
description: 'Validation profile: all, release, scripts, or repo'
description: 'Validation profile: all, scripts, or repo'
required: true
default: all
type: choice
options:
- all
- release
- scripts
- repo
pull_request:
@@ -40,10 +39,6 @@ permissions:
contents: read
env:
# Release policy - Repository Variables Only
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
# Scripts governance policy
SCRIPTS_REQUIRED_DIRS:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
@@ -138,101 +133,6 @@ jobs:
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
exit 1
release_config:
name: Release configuration
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Guardrails release vars
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes release validation'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
missing=()
missing_optional=()
for k in "${required[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing+=("${k}")
done
for k in "${optional[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing_optional+=("${k}")
done
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Variable | Status |'
printf '%s\n' '|---|---|'
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repository variables'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#missing[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repository variables'
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
{
printf '%s\n' '### Repository variables validation result'
printf '%s\n' 'Status: OK'
printf '%s\n' 'All required repository variables present.'
printf '%s\n' ''
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
scripts_governance:
name: Scripts governance
needs: access_check
@@ -256,14 +156,14 @@ jobs:
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
all|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
if [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
@@ -370,14 +270,14 @@ jobs:
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
all|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
if [ "${profile}" = 'scripts' ]; then
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
@@ -704,7 +604,7 @@ jobs:
printf '%s\n' '| Domain | Status | Notes |'
printf '%s\n' '|---|---|---|'
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
printf '%s\n' '| Release variables | OK | Repository variables validation |'
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
@@ -773,11 +673,10 @@ jobs:
report-issues:
name: "Report Issues"
runs-on: ubuntu-latest
needs: [access_check, release_config, scripts_governance, repo_health]
needs: [access_check, scripts_governance, repo_health]
if: >-
always() &&
(needs.release_config.result == 'failure' ||
needs.scripts_governance.result == 'failure' ||
(needs.scripts_governance.result == 'failure' ||
needs.repo_health.result == 'failure')
steps:
@@ -803,10 +702,6 @@ jobs:
fi
}
report_gate "Release Configuration" \
"${{ needs.release_config.result }}" \
"Required repository variables are missing (RS_FTP_PATH_SUFFIX). Check repository settings."
report_gate "Scripts Governance" \
"${{ needs.scripts_governance.result }}" \
"Scripts directory policy violations detected. Review required and allowed directories."
@@ -814,4 +709,3 @@ jobs:
report_gate "Repository Health" \
"${{ needs.repo_health.result }}" \
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
+95 -53
View File
@@ -1,5 +1,5 @@
// Copyright 2026 Moko Consulting. All rights reserved.
// SPDX-License-Identifier: MIT
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package issues
@@ -10,86 +10,124 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)
// CustomFieldDefinition defines a custom field for a repository's issues
type CustomFieldDefinition struct {
ID int64 `xorm:"pk autoincr" json:"id"`
RepoID int64 `xorm:"INDEX NOT NULL" json:"repo_id"`
Name string `xorm:"NOT NULL" json:"name"`
FieldType string `xorm:"NOT NULL" json:"field_type"` // text, number, date, dropdown, checkbox
Description string `json:"description"`
Required bool `xorm:"NOT NULL DEFAULT false" json:"required"`
Position int `xorm:"NOT NULL DEFAULT 0" json:"position"`
Options string `xorm:"TEXT" json:"options"` // JSON array for dropdown options
DefaultVal string `json:"default_value"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created" json:"created_at"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated" json:"updated_at"`
}
func init() {
db.RegisterModel(new(CustomFieldDefinition))
db.RegisterModel(new(CustomFieldDef))
db.RegisterModel(new(CustomFieldValue))
}
// CustomFieldValue stores the value of a custom field for a specific issue
// CustomFieldType represents the data type of a custom field.
type CustomFieldType string
const (
CustomFieldTypeText CustomFieldType = "text"
CustomFieldTypeNumber CustomFieldType = "number"
CustomFieldTypeDate CustomFieldType = "date"
CustomFieldTypeDropdown CustomFieldType = "dropdown"
CustomFieldTypeCheckbox CustomFieldType = "checkbox"
CustomFieldTypeURL CustomFieldType = "url"
)
// CustomFieldDef defines a custom field available for issues in a repository.
type CustomFieldDef struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL 'repo_id'"`
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
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'"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (CustomFieldDef) TableName() string {
return "custom_field_def"
}
// CustomFieldValue stores a custom field value for a specific issue.
type CustomFieldValue struct {
ID int64 `xorm:"pk autoincr" json:"id"`
IssueID int64 `xorm:"INDEX NOT NULL" json:"issue_id"`
FieldID int64 `xorm:"INDEX NOT NULL" json:"field_id"`
Value string `xorm:"TEXT" json:"value"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created" json:"created_at"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated" json:"updated_at"`
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"INDEX NOT NULL 'issue_id'"`
FieldID int64 `xorm:"INDEX NOT NULL 'field_id'"`
Value string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
// GetCustomFieldsByRepoID returns all custom field definitions for a repo
func GetCustomFieldsByRepoID(ctx context.Context, repoID int64) ([]*CustomFieldDefinition, error) {
fields := make([]*CustomFieldDefinition, 0)
return fields, db.GetEngine(ctx).Where("repo_id = ?", repoID).OrderBy("position ASC").Find(&fields)
func (CustomFieldValue) TableName() string {
return "custom_field_value"
}
// GetCustomFieldByID returns a custom field definition by ID
func GetCustomFieldByID(ctx context.Context, id int64) (*CustomFieldDefinition, error) {
field := &CustomFieldDefinition{ID: id}
has, err := db.GetEngine(ctx).Get(field)
// GetCustomFieldsByRepo returns all active custom field definitions for a repo.
func GetCustomFieldsByRepo(ctx context.Context, repoID int64) ([]*CustomFieldDef, error) {
fields := make([]*CustomFieldDef, 0, 10)
return fields, db.GetEngine(ctx).
Where("repo_id = ? AND is_active = ?", repoID, true).
OrderBy("sort_order ASC, id ASC").
Find(&fields)
}
// GetAllCustomFieldsByRepo returns all custom field definitions including inactive.
func GetAllCustomFieldsByRepo(ctx context.Context, repoID int64) ([]*CustomFieldDef, error) {
fields := make([]*CustomFieldDef, 0, 10)
return fields, db.GetEngine(ctx).
Where("repo_id = ?", repoID).
OrderBy("sort_order ASC, id ASC").
Find(&fields)
}
// GetCustomFieldDefByID returns a single field definition.
func GetCustomFieldDefByID(ctx context.Context, id int64) (*CustomFieldDef, error) {
field := new(CustomFieldDef)
has, err := db.GetEngine(ctx).ID(id).Get(field)
if err != nil {
return nil, err
}
if !has {
return nil, nil
return nil, db.ErrNotExist{Resource: "CustomFieldDef", ID: id}
}
return field, nil
}
// CreateCustomField creates a new custom field definition
func CreateCustomField(ctx context.Context, field *CustomFieldDefinition) error {
// CreateCustomFieldDef creates a new custom field definition.
func CreateCustomFieldDef(ctx context.Context, field *CustomFieldDef) error {
_, err := db.GetEngine(ctx).Insert(field)
return err
}
// UpdateCustomField updates a custom field definition
func UpdateCustomField(ctx context.Context, field *CustomFieldDefinition) error {
// UpdateCustomFieldDef updates a custom field definition.
func UpdateCustomFieldDef(ctx context.Context, field *CustomFieldDef) error {
_, err := db.GetEngine(ctx).ID(field.ID).AllCols().Update(field)
return err
}
// DeleteCustomField deletes a custom field and all its values
func DeleteCustomField(ctx context.Context, id int64) error {
sess := db.GetEngine(ctx)
if _, err := sess.Where("field_id = ?", id).Delete(&CustomFieldValue{}); err != nil {
// DeleteCustomFieldDef deletes a field definition and all its values.
func DeleteCustomFieldDef(ctx context.Context, id int64) error {
if _, err := db.GetEngine(ctx).Where("field_id = ?", id).Delete(new(CustomFieldValue)); err != nil {
return err
}
_, err := sess.ID(id).Delete(&CustomFieldDefinition{})
_, err := db.GetEngine(ctx).ID(id).Delete(new(CustomFieldDef))
return err
}
// GetCustomFieldValues returns all custom field values for an issue
func GetCustomFieldValues(ctx context.Context, issueID int64) ([]*CustomFieldValue, error) {
values := make([]*CustomFieldValue, 0)
return values, db.GetEngine(ctx).Where("issue_id = ?", issueID).Find(&values)
// GetCustomFieldValuesMap returns field_id -> value for an issue.
func GetCustomFieldValuesMap(ctx context.Context, issueID int64) (map[int64]string, error) {
values := make([]*CustomFieldValue, 0, 10)
if err := db.GetEngine(ctx).Where("issue_id = ?", issueID).Find(&values); err != nil {
return nil, err
}
result := make(map[int64]string, len(values))
for _, v := range values {
result[v.FieldID] = v.Value
}
return result, nil
}
// SetCustomFieldValue sets or updates a custom field value for an issue
// SetCustomFieldValue creates or updates a single custom field value.
func SetCustomFieldValue(ctx context.Context, issueID, fieldID int64, value string) error {
existing := &CustomFieldValue{}
existing := new(CustomFieldValue)
has, err := db.GetEngine(ctx).Where("issue_id = ? AND field_id = ?", issueID, fieldID).Get(existing)
if err != nil {
return err
@@ -107,8 +145,12 @@ func SetCustomFieldValue(ctx context.Context, issueID, fieldID int64, value stri
return err
}
// DeleteCustomFieldValue deletes a specific custom field value
func DeleteCustomFieldValue(ctx context.Context, issueID, fieldID int64) error {
_, err := db.GetEngine(ctx).Where("issue_id = ? AND field_id = ?", issueID, fieldID).Delete(&CustomFieldValue{})
return err
// SetCustomFieldValues sets multiple custom field values for an issue.
func SetCustomFieldValues(ctx context.Context, issueID int64, values map[int64]string) error {
for fieldID, value := range values {
if err := SetCustomFieldValue(ctx, issueID, fieldID, value); err != nil {
return err
}
}
return nil
}
+1
View File
@@ -420,6 +420,7 @@ func prepareMigrationTasks() []*migration {
newMigration(340, "Sync license system columns (key_raw, payment_ref, heartbeat, archive, metadata)", v1_27.SyncLicenseSystemColumns),
newMigration(341, "Add parent_org_id to user table for enterprise sub-org hierarchy", v1_27.AddParentOrgIDToUser),
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),
}
return preparedMigrations
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
type customFieldDef343 struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL 'repo_id'"`
Name string `xorm:"NOT NULL"`
FieldType string `xorm:"VARCHAR(20) NOT NULL 'field_type'"`
Description string `xorm:"TEXT"`
Options string `xorm:"TEXT"`
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'"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (customFieldDef343) TableName() string {
return "custom_field_def"
}
type customFieldValue343 struct {
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"INDEX NOT NULL 'issue_id'"`
FieldID int64 `xorm:"INDEX NOT NULL 'field_id'"`
Value string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (customFieldValue343) TableName() string {
return "custom_field_value"
}
// AddCustomFieldTables creates the custom_field_def and custom_field_value tables.
func AddCustomFieldTables(x *xorm.Engine) error {
return x.Sync(new(customFieldDef343), new(customFieldValue343))
}
+16
View File
@@ -2716,6 +2716,22 @@
"repo.settings.download_gating": "Download Gating",
"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.custom_field_new": "New Field",
"repo.settings.custom_field_create": "Create Field",
"repo.settings.custom_field_name": "Field Name",
"repo.settings.custom_field_type": "Type",
"repo.settings.custom_field_description": "Description",
"repo.settings.custom_field_options": "Options (JSON)",
"repo.settings.custom_field_options_help": "JSON array for dropdown fields. e.g. [\"Low\",\"Medium\",\"High\"]",
"repo.settings.custom_field_required": "Required",
"repo.settings.custom_field_sort_order": "Sort Order",
"repo.settings.custom_field_created": "Custom field created.",
"repo.settings.custom_field_updated": "Custom field updated.",
"repo.settings.custom_field_deleted": "Custom field deleted.",
"repo.settings.custom_field_confirm_delete": "Delete this custom field? All values stored for this field will be lost.",
"repo.settings.custom_fields_none": "No Custom Fields",
"repo.settings.custom_fields_none_desc": "Define custom fields to add structured metadata to issues.",
"repo.settings.features": "Features",
"repo.settings.features_units": "Units",
"repo.settings.change_visibility": "Change Visibility",
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
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 tplSettingsCustomFields templates.TplName = "repo/settings/custom_fields"
// CustomFields displays the custom fields settings page.
func CustomFields(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.custom_fields")
ctx.Data["PageIsSettingsCustomFields"] = true
fields, err := issues_model.GetAllCustomFieldsByRepo(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetAllCustomFieldsByRepo", err)
return
}
ctx.Data["CustomFields"] = fields
ctx.HTML(http.StatusOK, tplSettingsCustomFields)
}
// CustomFieldsCreatePost creates a new custom field definition.
func CustomFieldsCreatePost(ctx *context.Context) {
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
field := &issues_model.CustomFieldDef{
RepoID: ctx.Repo.Repository.ID,
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.Repo.RepoLink + "/settings/custom-fields")
return
}
if err := issues_model.CreateCustomFieldDef(ctx, field); err != nil {
ctx.ServerError("CreateCustomFieldDef", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.custom_field_created"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/custom-fields")
}
// CustomFieldsEditPost updates a custom field definition.
func CustomFieldsEditPost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
field, err := issues_model.GetCustomFieldDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetCustomFieldDefByID", err)
return
}
if field.RepoID != ctx.Repo.Repository.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("repo.settings.custom_field_updated"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/custom-fields")
}
// CustomFieldsDeletePost deletes a custom field definition.
func CustomFieldsDeletePost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
field, err := issues_model.GetCustomFieldDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetCustomFieldDefByID", err)
return
}
if field.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound(nil)
return
}
if err := issues_model.DeleteCustomFieldDef(ctx, id); err != nil {
ctx.ServerError("DeleteCustomFieldDef", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.custom_field_deleted"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/custom-fields")
}
+6
View File
@@ -1187,6 +1187,12 @@ 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.Group("/collaboration", func() {
m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost)
+116
View File
@@ -0,0 +1,116 @@
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings custom-fields")}}
<div class="user-main-content twelve wide column">
<h4 class="ui top attached header tw-flex tw-items-center tw-justify-between">
<span>{{svg "octicon-list-unordered" 16}} {{ctx.Locale.Tr "repo.settings.custom_fields"}}</span>
<button class="ui primary small button show-modal" data-modal="#custom-field-create-modal">
{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.settings.custom_field_new"}}
</button>
</h4>
<div class="ui attached segment">
{{if .CustomFields}}
<table class="ui compact table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "repo.settings.custom_field_name"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.custom_field_type"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.custom_field_required"}}</th>
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</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><code>{{.FieldType}}</code></td>
<td>{{if .Required}}{{svg "octicon-check" 14}}{{end}}</td>
<td>
{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>
{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}
</td>
<td class="tw-text-right tw-flex tw-gap-1 tw-justify-end">
<button class="ui tiny button show-modal"
data-modal="#custom-field-edit-modal"
data-modal-custom-field-edit-modal-field-id="{{.ID}}"
data-modal-custom-field-edit-modal-field-name="{{.Name}}"
data-modal-custom-field-edit-modal-field-type="{{.FieldType}}"
data-modal-custom-field-edit-modal-field-desc="{{.Description}}"
data-modal-custom-field-edit-modal-field-options="{{.Options}}"
data-modal-custom-field-edit-modal-field-required="{{.Required}}"
data-modal-custom-field-edit-modal-field-active="{{.IsActive}}"
data-modal-custom-field-edit-modal-field-order="{{.SortOrder}}"
title="{{ctx.Locale.Tr "edit"}}">
{{svg "octicon-pencil" 14}}
</button>
<button class="ui tiny red button link-action"
data-url="{{$.RepoLink}}/settings/custom-fields/{{.ID}}/delete"
data-modal-confirm="{{ctx.Locale.Tr "repo.settings.custom_field_confirm_delete"}}"
title="{{ctx.Locale.Tr "delete"}}">
{{svg "octicon-trash" 14}}
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="empty-placeholder">
{{svg "octicon-list-unordered" 48}}
<h2>{{ctx.Locale.Tr "repo.settings.custom_fields_none"}}</h2>
<p>{{ctx.Locale.Tr "repo.settings.custom_fields_none_desc"}}</p>
</div>
{{end}}
</div>
</div>
{{/* Create Custom Field Modal */}}
<div class="ui small modal" id="custom-field-create-modal">
<div class="header">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.settings.custom_field_new"}}</div>
<div class="content">
<form class="ui form" method="post" action="{{.RepoLink}}/settings/custom-fields">
{{$.CsrfTokenHtml}}
<div class="required field">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_name"}}</label>
<input name="name" required placeholder="e.g. Priority, Sprint, Client Name">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_type"}}</label>
<select name="field_type" class="ui dropdown">
<option value="text">Text</option>
<option value="number">Number</option>
<option value="date">Date</option>
<option value="dropdown">Dropdown</option>
<option value="checkbox">Checkbox</option>
<option value="url">URL</option>
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_description"}}</label>
<input name="description" placeholder="Help text shown below the field">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_options"}}</label>
<input name="options" placeholder='["Option A","Option B","Option C"]'>
<p class="help">{{ctx.Locale.Tr "repo.settings.custom_field_options_help"}}</p>
</div>
<div class="two fields">
<div class="field">
<div class="ui checkbox">
<input name="required" type="checkbox">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_required"}}</label>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_sort_order"}}</label>
<input name="sort_order" type="number" value="0" min="0">
</div>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.custom_field_create"))}}
</form>
</div>
</div>
{{template "repo/settings/layout_footer" .}}
+3
View File
@@ -12,6 +12,9 @@
{{svg "octicon-key"}} {{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>
{{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}}
<a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access">
{{svg "octicon-eye"}} {{ctx.Locale.Tr "repo.settings.public_access"}}