From 8741096fb468ae104b61e9b204febc1efbe422c9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 3 Jun 2026 03:11:01 +0000 Subject: [PATCH 1/3] chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] --- .mokogitea/workflows/repo-health.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index b23d971edf..d7743f0aac 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -41,7 +41,8 @@ permissions: env: # Release policy - Repository Variables Only - RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX + # RS_FTP_PATH_SUFFIX removed — MokoGitea handles all releases now + RELEASE_REQUIRED_REPO_VARS: RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX # Scripts governance policy -- 2.52.0 From 6b0ec5196a96f7db906fc97cfbef30c715d80c89 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 3 Jun 2026 09:37:15 +0000 Subject: [PATCH 2/3] chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] --- .mokogitea/workflows/repo-health.yml | 125 ++------------------------- 1 file changed, 9 insertions(+), 116 deletions(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index d7743f0aac..8d57aaf091 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -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,11 +39,6 @@ permissions: contents: read env: - # Release policy - Repository Variables Only - # RS_FTP_PATH_SUFFIX removed — MokoGitea handles all releases now - RELEASE_REQUIRED_REPO_VARS: - 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 @@ -139,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 @@ -257,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}" @@ -371,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}" @@ -705,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 |' @@ -774,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: @@ -804,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." @@ -815,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." - -- 2.52.0 From c7d8f6066f9b1b176c11c568af749966cd13df9d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 06:44:33 -0500 Subject: [PATCH 3/3] =?UTF-8?q?feat(issues):=20custom=20fields=20foundatio?= =?UTF-8?q?n=20=E2=80=94=20model,=20migration,=20settings=20UI=20(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- models/issues/custom_field.go | 148 +++++++++++++-------- models/migrations/migrations.go | 1 + models/migrations/v1_27/v343.go | 46 +++++++ options/locale/locale_en-US.json | 16 +++ routers/web/repo/setting/custom_fields.go | 114 ++++++++++++++++ routers/web/web.go | 6 + templates/repo/settings/custom_fields.tmpl | 116 ++++++++++++++++ templates/repo/settings/navbar.tmpl | 3 + 8 files changed, 397 insertions(+), 53 deletions(-) create mode 100644 models/migrations/v1_27/v343.go create mode 100644 routers/web/repo/setting/custom_fields.go create mode 100644 templates/repo/settings/custom_fields.tmpl diff --git a/models/issues/custom_field.go b/models/issues/custom_field.go index e9787f7228..2d56b94034 100644 --- a/models/issues/custom_field.go +++ b/models/issues/custom_field.go @@ -1,5 +1,5 @@ -// Copyright 2026 Moko Consulting. All rights reserved. -// SPDX-License-Identifier: MIT +// Copyright 2026 Moko Consulting +// 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 } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 06031df8be..eea2aab73b 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -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 } diff --git a/models/migrations/v1_27/v343.go b/models/migrations/v1_27/v343.go new file mode 100644 index 0000000000..36f66418f9 --- /dev/null +++ b/models/migrations/v1_27/v343.go @@ -0,0 +1,46 @@ +// Copyright 2026 Moko Consulting +// 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)) +} diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 2a8ec5f596..321a0666bb 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -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", diff --git a/routers/web/repo/setting/custom_fields.go b/routers/web/repo/setting/custom_fields.go new file mode 100644 index 0000000000..432caa9e22 --- /dev/null +++ b/routers/web/repo/setting/custom_fields.go @@ -0,0 +1,114 @@ +// Copyright 2026 Moko Consulting +// 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") +} diff --git a/routers/web/web.go b/routers/web/web.go index 4dc4b809a9..19d71398c5 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -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) diff --git a/templates/repo/settings/custom_fields.tmpl b/templates/repo/settings/custom_fields.tmpl new file mode 100644 index 0000000000..d381a6c8d7 --- /dev/null +++ b/templates/repo/settings/custom_fields.tmpl @@ -0,0 +1,116 @@ +{{template "repo/settings/layout_head" (dict "pageClass" "repository settings custom-fields")}} +
+

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

+
+ {{if .CustomFields}} + + + + + + + + + + + + {{range .CustomFields}} + + + + + + + + {{end}} + +
{{ctx.Locale.Tr "repo.settings.custom_field_name"}}{{ctx.Locale.Tr "repo.settings.custom_field_type"}}{{ctx.Locale.Tr "repo.settings.custom_field_required"}}{{ctx.Locale.Tr "repo.licenses.status"}}
+ {{.Name}} + {{if .Description}}
{{.Description}}{{end}} +
{{.FieldType}}{{if .Required}}{{svg "octicon-check" 14}}{{end}} + {{if .IsActive}}{{ctx.Locale.Tr "repo.licenses.active"}} + {{else}}{{ctx.Locale.Tr "repo.licenses.inactive"}}{{end}} + + + +
+ {{else}} +
+ {{svg "octicon-list-unordered" 48}} +

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

+

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

+
+ {{end}} +
+
+ +{{/* ── Create Custom Field Modal ── */}} + + +{{template "repo/settings/layout_footer" .}} diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl index ccb1f60d12..ca126fea69 100644 --- a/templates/repo/settings/navbar.tmpl +++ b/templates/repo/settings/navbar.tmpl @@ -12,6 +12,9 @@ {{svg "octicon-key"}} {{ctx.Locale.Tr "repo.settings.licensing_section"}} {{end}} + + {{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.custom_fields"}} + {{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}} {{svg "octicon-eye"}} {{ctx.Locale.Tr "repo.settings.public_access"}} -- 2.52.0