Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 082c550bc4 | |||
| 8fa56271de | |||
| 0fe1d769ea | |||
| 18372c84a7 |
@@ -4,7 +4,7 @@
|
||||
<name>MokoGitea</name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
|
||||
<version>06.12.03</version>
|
||||
<version>06.13.00</version>
|
||||
<version-prefix>v1.26.1+MOKO</version-prefix>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokoplatform.Automation
|
||||
# VERSION: 06.12.03
|
||||
# VERSION: 06.13.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -2,22 +2,6 @@
|
||||
|
||||
All notable changes to MokoGitea are documented here. Versions follow the format
|
||||
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
* FEATURES
|
||||
* feat(api): issue status/priority/type exposed in REST API - GET/PATCH on issues now includes status_id, priority_id, type_id with resolved names
|
||||
* feat(api): org-level issue metadata endpoints - GET /orgs/{org}/issue-statuses, /issue-priorities, /issue-types
|
||||
* feat(wiki): org wiki tab - inline wiki rendering from convention repos (wiki / wiki-private)
|
||||
* feat(wiki): public/private wiki toggle dropdown (same UX as org profile README selector)
|
||||
* feat(wiki): external wiki support - link to an outside URL from the org wiki tab
|
||||
* feat(settings): wiki mode setting in org settings (internal repos vs external URL)
|
||||
* feat(mcp): 5 new MCP tools - gitea_org_issue_statuses_list, gitea_org_issue_priorities_list, gitea_org_issue_types_list, gitea_issue_set_status, gitea_issue_set_priority
|
||||
* feat(mcp): gitea_issue_create and gitea_issue_update now accept status_id, priority_id, type_id
|
||||
|
||||
* MIGRATIONS
|
||||
* migration 354: add wiki_mode and wiki_url columns to user table for org wiki settings
|
||||
|
||||
## [v1.26.1-moko.06.12] - 2026-06-07
|
||||
|
||||
* FEATURES
|
||||
@@ -227,14 +211,3 @@ All notable changes to MokoGitea are documented here. Versions follow the format
|
||||
* PROCESS
|
||||
* Created `type: bug` and `upstream` labels for automated issue tracking
|
||||
* Closed 24 upstream bug/security issues after backporting
|
||||
|
||||
## [v1.26.1-moko.03] - 2026-05-15
|
||||
|
||||
* FEATURES
|
||||
* feat(api): Bulk issue operations — add/remove/replace labels, close/reopen, set milestone, assignees (#21)
|
||||
* INFRASTRUCTURE
|
||||
* Grafana: Standardized kiosk header across all 14 playlist dashboards
|
||||
* PROCESS
|
||||
* Reopened 9 closed issues lacking documented testing proof
|
||||
* Created `pending: testing` label for features awaiting verification
|
||||
* Established policy: issues must not be closed without documented testing proof
|
||||
|
||||
+1
-1
Submodule mcp-mokogitea-api updated: c9eb6cfc89...bc8bc0b53a
@@ -431,7 +431,6 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(351, "Add CDN public flag to attachments", v1_27.AddAttachmentCDNPublic),
|
||||
newMigration(352, "Add version prefix and element name to repo manifest", v1_27.AddManifestVersionPrefixAndElement),
|
||||
newMigration(353, "Add distribution metadata fields to repo manifest", v1_27.AddManifestDistributionFields),
|
||||
newMigration(354, "Add org wiki settings to user table", v1_27.AddOrgWikiSettings),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// AddOrgWikiSettings adds wiki_mode and wiki_url columns to the user table
|
||||
// for configuring org-level wiki behavior (internal convention repos vs external link).
|
||||
func AddOrgWikiSettings(x *xorm.Engine) error {
|
||||
type User struct {
|
||||
WikiMode string `xorm:"VARCHAR(20) NOT NULL DEFAULT '' 'wiki_mode'"`
|
||||
WikiURL string `xorm:"TEXT 'wiki_url'"`
|
||||
}
|
||||
return x.Sync(new(User))
|
||||
}
|
||||
@@ -153,8 +153,6 @@ type User struct {
|
||||
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
|
||||
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ParentOrgID int64 `xorm:"INDEX DEFAULT 0"` // 0 = no parent (top-level org)
|
||||
WikiMode string `xorm:"VARCHAR(20) NOT NULL DEFAULT '' 'wiki_mode'"` // "" = internal (convention repos), "external" = link to WikiURL
|
||||
WikiURL string `xorm:"TEXT 'wiki_url'"` // external wiki URL (used when WikiMode == "external")
|
||||
|
||||
// Preferences
|
||||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
||||
|
||||
@@ -84,14 +84,6 @@ type Issue struct {
|
||||
PinOrder int `json:"pin_order"`
|
||||
// The version of the issue content for optimistic locking
|
||||
ContentVersion int `json:"content_version"`
|
||||
|
||||
// Issue metadata (org-level definitions)
|
||||
StatusID int64 `json:"status_id"`
|
||||
StatusName string `json:"status_name"`
|
||||
PriorityID int64 `json:"priority_id"`
|
||||
PriorityName string `json:"priority_name"`
|
||||
TypeID int64 `json:"type_id"`
|
||||
TypeName string `json:"type_name"`
|
||||
}
|
||||
|
||||
// CreateIssueOption options to create one issue
|
||||
@@ -114,10 +106,6 @@ type CreateIssueOption struct {
|
||||
Closed bool `json:"closed"`
|
||||
// custom field values keyed by field name
|
||||
CustomFields map[string]string `json:"custom_fields,omitempty"`
|
||||
// org-level issue metadata IDs
|
||||
StatusID *int64 `json:"status_id,omitempty"`
|
||||
PriorityID *int64 `json:"priority_id,omitempty"`
|
||||
TypeID *int64 `json:"type_id,omitempty"`
|
||||
}
|
||||
|
||||
// EditIssueOption options for editing an issue
|
||||
@@ -137,10 +125,6 @@ type EditIssueOption struct {
|
||||
RemoveDeadline *bool `json:"unset_due_date"`
|
||||
// The current version of the issue content to detect conflicts during editing
|
||||
ContentVersion *int `json:"content_version"`
|
||||
// org-level issue metadata IDs
|
||||
StatusID *int64 `json:"status_id,omitempty"`
|
||||
PriorityID *int64 `json:"priority_id,omitempty"`
|
||||
TypeID *int64 `json:"type_id,omitempty"`
|
||||
}
|
||||
|
||||
// EditDeadlineOption options for creating a deadline
|
||||
@@ -157,39 +141,6 @@ type IssueDeadline struct {
|
||||
Deadline *time.Time `json:"due_date"`
|
||||
}
|
||||
|
||||
// IssueStatusDef represents an org-level issue status definition
|
||||
// swagger:model
|
||||
type IssueStatusDef struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
Description string `json:"description"`
|
||||
ClosesIssue bool `json:"closes_issue"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// IssuePriorityDef represents an org-level issue priority definition
|
||||
// swagger:model
|
||||
type IssuePriorityDef struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
Description string `json:"description"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// IssueTypeDef represents an org-level issue type definition
|
||||
// swagger:model
|
||||
type IssueTypeDef struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
Description string `json:"description"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes"
|
||||
//
|
||||
// swagger:enum IssueFormFieldType
|
||||
|
||||
@@ -1773,9 +1773,6 @@ func Routes() *web.Router {
|
||||
m.Post("", reqToken(), reqOrgOwnership(), org.CreateOrgCustomField)
|
||||
m.Delete("/{id}", reqToken(), reqOrgOwnership(), org.DeleteOrgCustomField)
|
||||
})
|
||||
m.Get("/issue-statuses", org.ListIssueStatuses)
|
||||
m.Get("/issue-priorities", org.ListIssuePriorities)
|
||||
m.Get("/issue-types", org.ListIssueTypes)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
|
||||
m.Group("/teams/{teamid}", func() {
|
||||
m.Combo("").Get(reqToken(), org.GetTeam).
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
// ListIssueStatuses returns active issue status definitions for an org.
|
||||
func ListIssueStatuses(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/issue-statuses organization orgListIssueStatuses
|
||||
// ---
|
||||
// summary: List an organization's issue status definitions
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: "IssueStatusDefList"
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/IssueStatusDef"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
result := make([]*api.IssueStatusDef, 0, len(defs))
|
||||
for _, d := range defs {
|
||||
result = append(result, &api.IssueStatusDef{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Color: d.Color,
|
||||
Description: d.Description,
|
||||
ClosesIssue: d.ClosesIssue,
|
||||
SortOrder: d.SortOrder,
|
||||
})
|
||||
}
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// ListIssuePriorities returns active issue priority definitions for an org.
|
||||
func ListIssuePriorities(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/issue-priorities organization orgListIssuePriorities
|
||||
// ---
|
||||
// summary: List an organization's issue priority definitions
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: "IssuePriorityDefList"
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/IssuePriorityDef"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
result := make([]*api.IssuePriorityDef, 0, len(defs))
|
||||
for _, d := range defs {
|
||||
result = append(result, &api.IssuePriorityDef{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Color: d.Color,
|
||||
Description: d.Description,
|
||||
SortOrder: d.SortOrder,
|
||||
IsDefault: d.IsDefault,
|
||||
})
|
||||
}
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// ListIssueTypes returns active issue type definitions for an org.
|
||||
func ListIssueTypes(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/issue-types organization orgListIssueTypes
|
||||
// ---
|
||||
// summary: List an organization's issue type definitions
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: "IssueTypeDefList"
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/IssueTypeDef"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
result := make([]*api.IssueTypeDef, 0, len(defs))
|
||||
for _, d := range defs {
|
||||
result = append(result, &api.IssueTypeDef{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Color: d.Color,
|
||||
Description: d.Description,
|
||||
SortOrder: d.SortOrder,
|
||||
IsDefault: d.IsDefault,
|
||||
})
|
||||
}
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
@@ -756,26 +756,6 @@ func CreateIssue(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
// Set org-level issue metadata (status/priority/type) if provided
|
||||
if form.StatusID != nil && *form.StatusID > 0 {
|
||||
if err := issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if form.PriorityID != nil && *form.PriorityID > 0 {
|
||||
if err := issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if form.TypeID != nil && *form.TypeID > 0 {
|
||||
if err := issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if form.Closed {
|
||||
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||
if issues_model.IsErrDependenciesLeft(err) {
|
||||
@@ -1000,26 +980,6 @@ func EditIssue(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update org-level issue metadata (status/priority/type)
|
||||
if canWrite && form.StatusID != nil {
|
||||
if err := issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if canWrite && form.PriorityID != nil {
|
||||
if err := issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if canWrite && form.TypeID != nil {
|
||||
if err := issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Refetch from database to assign some automatic values
|
||||
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -126,17 +126,6 @@ func SettingsPost(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Save wiki mode settings.
|
||||
wikiMode := ctx.FormString("wiki_mode")
|
||||
wikiURL := ctx.FormString("wiki_url")
|
||||
orgUser := org.AsUser()
|
||||
orgUser.WikiMode = wikiMode
|
||||
orgUser.WikiURL = wikiURL
|
||||
if err := user_model.UpdateUserCols(ctx, orgUser, "wiki_mode", "wiki_url"); err != nil {
|
||||
ctx.ServerError("UpdateUserCols(wiki)", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Organization setting updated: %s", org.Name)
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings")
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/gitrepo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/markup/markdown"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
|
||||
shared_user "code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/web/shared/user"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplOrgWiki templates.TplName = "org/wiki/view"
|
||||
|
||||
// OrgWikiPage represents a single page in the org wiki sidebar.
|
||||
type OrgWikiPage struct {
|
||||
Name string
|
||||
SubURL string
|
||||
}
|
||||
|
||||
// Wiki renders the org wiki tab.
|
||||
func Wiki(ctx *context.Context) {
|
||||
org := ctx.Org.Organization
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["PageIsViewWiki"] = true
|
||||
ctx.Data["Title"] = org.DisplayName() + " - Wiki"
|
||||
|
||||
// Determine which wiki repo to use (public vs member).
|
||||
viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public"))
|
||||
viewAsMember := viewAs == "member"
|
||||
|
||||
wikiRepo, commit := findOrgWikiCommit(ctx, org.ID, util.Iif(viewAsMember, shared_user.RepoNameWikiPrivate, shared_user.RepoNameWikiPublic))
|
||||
if wikiRepo == nil && viewAsMember {
|
||||
// Fall back to public wiki if member wiki doesn't exist.
|
||||
wikiRepo, commit = findOrgWikiCommit(ctx, org.ID, shared_user.RepoNameWikiPublic)
|
||||
viewAsMember = false
|
||||
}
|
||||
if wikiRepo == nil && !viewAsMember {
|
||||
// Fall back to member wiki if public wiki doesn't exist.
|
||||
wikiRepo, commit = findOrgWikiCommit(ctx, org.ID, shared_user.RepoNameWikiPrivate)
|
||||
viewAsMember = true
|
||||
}
|
||||
|
||||
ctx.Data["IsViewingWikiAsMember"] = viewAsMember
|
||||
|
||||
// Check whether both repos exist (for the dropdown toggle).
|
||||
publicExists := shared_user.OrgWikiRepoExists(ctx, org.ID, shared_user.RepoNameWikiPublic)
|
||||
privateExists := shared_user.OrgWikiRepoExists(ctx, org.ID, shared_user.RepoNameWikiPrivate)
|
||||
ctx.Data["ShowWikiViewSelector"] = publicExists && privateExists && ctx.Org.IsMember
|
||||
|
||||
if wikiRepo == nil || commit == nil {
|
||||
ctx.Data["WikiEmpty"] = true
|
||||
ctx.HTML(http.StatusOK, tplOrgWiki)
|
||||
return
|
||||
}
|
||||
ctx.Data["WikiRepoLink"] = wikiRepo.Link()
|
||||
|
||||
// Build page list from repo root.
|
||||
entries, err := commit.ListEntries()
|
||||
if err != nil {
|
||||
ctx.ServerError("ListEntries", err)
|
||||
return
|
||||
}
|
||||
pages := make([]OrgWikiPage, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if !entry.IsRegular() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if !isMarkdownFile(name) {
|
||||
continue
|
||||
}
|
||||
displayName := strings.TrimSuffix(name, path.Ext(name))
|
||||
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
|
||||
continue
|
||||
}
|
||||
pages = append(pages, OrgWikiPage{
|
||||
Name: displayName,
|
||||
SubURL: displayName,
|
||||
})
|
||||
}
|
||||
ctx.Data["Pages"] = pages
|
||||
|
||||
// Determine which page to render.
|
||||
pageName := ctx.PathParamRaw("*")
|
||||
if pageName == "" {
|
||||
pageName = "Home"
|
||||
}
|
||||
ctx.Data["CurrentPage"] = pageName
|
||||
|
||||
// Try to find the file: exact match, then with .md extension.
|
||||
blob := findWikiBlob(commit, pageName)
|
||||
if blob == nil {
|
||||
// Page not found — show empty state with page list.
|
||||
ctx.Data["WikiPageNotFound"] = true
|
||||
ctx.HTML(http.StatusOK, tplOrgWiki)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBlobContent", err)
|
||||
return
|
||||
}
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, wikiRepo, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(wikiRepo.DefaultBranch)),
|
||||
})
|
||||
renderedContent, err := markdown.RenderString(rctx, content)
|
||||
if err != nil {
|
||||
log.Error("Failed to render org wiki page %q: %v", pageName, err)
|
||||
ctx.ServerError("RenderString", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["WikiContent"] = renderedContent
|
||||
|
||||
// Render _Sidebar if it exists.
|
||||
sidebarBlob := findWikiBlob(commit, "_Sidebar")
|
||||
if sidebarBlob != nil {
|
||||
sidebarContent, err := sidebarBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err == nil {
|
||||
rendered, err := markdown.RenderString(rctx, sidebarContent)
|
||||
if err == nil {
|
||||
ctx.Data["WikiSidebarHTML"] = rendered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render _Footer if it exists.
|
||||
footerBlob := findWikiBlob(commit, "_Footer")
|
||||
if footerBlob != nil {
|
||||
footerContent, err := footerBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err == nil {
|
||||
rendered, err := markdown.RenderString(rctx, footerContent)
|
||||
if err == nil {
|
||||
ctx.Data["WikiFooterHTML"] = rendered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplOrgWiki)
|
||||
}
|
||||
|
||||
// findOrgWikiCommit locates the convention wiki repo and returns its HEAD commit.
|
||||
func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) {
|
||||
dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
log.Error("findOrgWikiCommit: GetRepositoryByName(%d, %s): %v", orgID, repoName, err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if dbRepo.IsEmpty {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo)
|
||||
if err != nil {
|
||||
log.Error("findOrgWikiCommit: OpenRepository(%s): %v", dbRepo.FullName(), err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commit, err := gitRepo.GetBranchCommit(dbRepo.DefaultBranch)
|
||||
if err != nil {
|
||||
log.Error("findOrgWikiCommit: GetBranchCommit(%s, %s): %v", dbRepo.FullName(), dbRepo.DefaultBranch, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return dbRepo, commit
|
||||
}
|
||||
|
||||
// findWikiBlob looks up a markdown file in the commit by name.
|
||||
// Tries exact match first, then appends .md.
|
||||
func findWikiBlob(commit *git.Commit, name string) *git.Blob {
|
||||
// Try exact match (e.g., "Home.md").
|
||||
if blob, _ := commit.GetBlobByPath(name); blob != nil {
|
||||
return blob
|
||||
}
|
||||
// Try with .md extension (e.g., "Home" → "Home.md").
|
||||
if blob, _ := commit.GetBlobByPath(name + ".md"); blob != nil {
|
||||
return blob
|
||||
}
|
||||
// Try with .markdown extension.
|
||||
if blob, _ := commit.GetBlobByPath(name + ".markdown"); blob != nil {
|
||||
return blob
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isMarkdownFile returns true if the filename looks like a markdown file.
|
||||
func isMarkdownFile(name string) bool {
|
||||
ext := strings.ToLower(path.Ext(name))
|
||||
return ext == ".md" || ext == ".markdown"
|
||||
}
|
||||
@@ -137,8 +137,6 @@ type PrepareOwnerHeaderResult struct {
|
||||
const (
|
||||
RepoNameProfilePrivate = ".profile-private"
|
||||
RepoNameProfile = ".profile"
|
||||
RepoNameWikiPublic = "wiki"
|
||||
RepoNameWikiPrivate = "wiki-private"
|
||||
)
|
||||
|
||||
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
|
||||
@@ -157,18 +155,6 @@ func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult
|
||||
result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
|
||||
result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
|
||||
ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab
|
||||
|
||||
// Check if org has a wiki (internal convention repos or external URL).
|
||||
orgUser := ctx.ContextUser
|
||||
if orgUser.WikiMode == "external" && orgUser.WikiURL != "" {
|
||||
ctx.Data["HasOrgWiki"] = true
|
||||
ctx.Data["OrgWikiIsExternal"] = true
|
||||
ctx.Data["OrgWikiExternalURL"] = orgUser.WikiURL
|
||||
} else {
|
||||
hasWiki := OrgWikiRepoExists(ctx, ctx.ContextUser.ID, RepoNameWikiPublic) ||
|
||||
OrgWikiRepoExists(ctx, ctx.ContextUser.ID, RepoNameWikiPrivate)
|
||||
ctx.Data["HasOrgWiki"] = hasWiki
|
||||
}
|
||||
} else {
|
||||
_, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer)
|
||||
ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil
|
||||
@@ -208,12 +194,3 @@ func loadHeaderCount(ctx *context.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OrgWikiRepoExists checks whether a convention wiki repo exists and is non-empty.
|
||||
func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool {
|
||||
dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName)
|
||||
if err != nil || dbRepo.IsEmpty {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1162,11 +1162,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/repositories", org.Repositories)
|
||||
m.Get("/heatmap", user.DashboardHeatmap)
|
||||
|
||||
m.Group("/wiki", func() {
|
||||
m.Get("", org.Wiki)
|
||||
m.Get("/*", org.Wiki)
|
||||
})
|
||||
|
||||
m.Group("/projects", func() {
|
||||
m.Group("", func() {
|
||||
m.Get("", org.Projects)
|
||||
|
||||
@@ -131,26 +131,6 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
|
||||
apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr()
|
||||
}
|
||||
|
||||
// Populate org-level issue metadata (status/priority/type)
|
||||
apiIssue.StatusID = issue.StatusID
|
||||
apiIssue.PriorityID = issue.PriorityID
|
||||
apiIssue.TypeID = issue.TypeID
|
||||
if issue.StatusID > 0 {
|
||||
if def, err := issues_model.GetIssueStatusDefByID(ctx, issue.StatusID); err == nil {
|
||||
apiIssue.StatusName = def.Name
|
||||
}
|
||||
}
|
||||
if issue.PriorityID > 0 {
|
||||
if def, err := issues_model.GetIssuePriorityDefByID(ctx, issue.PriorityID); err == nil {
|
||||
apiIssue.PriorityName = def.Name
|
||||
}
|
||||
}
|
||||
if issue.TypeID > 0 {
|
||||
if def, err := issues_model.GetIssueTypeDefByID(ctx, issue.TypeID); err == nil {
|
||||
apiIssue.TypeName = def.Name
|
||||
}
|
||||
}
|
||||
|
||||
return apiIssue
|
||||
}
|
||||
|
||||
|
||||
@@ -38,17 +38,6 @@
|
||||
{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .HasOrgWiki}}
|
||||
{{if .OrgWikiIsExternal}}
|
||||
<a class="item" href="{{.OrgWikiExternalURL}}" target="_blank" rel="noopener noreferrer">
|
||||
{{svg "octicon-book"}} Wiki {{svg "octicon-link-external" 12}}
|
||||
</a>
|
||||
{{else}}
|
||||
<a class="{{if .PageIsViewWiki}}active {{end}}item" href="{{$.Org.HomeLink}}/-/wiki/">
|
||||
{{svg "octicon-book"}} Wiki
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if .NumMembers}}
|
||||
<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
|
||||
{{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}}
|
||||
|
||||
@@ -63,31 +63,6 @@
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-book" 16}} Wiki</label>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input class="enable-system-radio" name="wiki_mode" type="radio" value="" data-context="#external_wiki_box" data-target="#internal_wiki_box" {{if eq .Org.WikiMode ""}}checked{{end}}>
|
||||
<label>Internal wiki (uses <code>wiki</code> / <code>wiki-private</code> repos)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="internal_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode ""}}disabled{{end}}">
|
||||
<p class="help">Create repos named <code>wiki</code> (public) and/or <code>wiki-private</code> (members-only) under this organization.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input class="enable-system-radio" name="wiki_mode" type="radio" value="external" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if eq .Org.WikiMode "external"}}checked{{end}}>
|
||||
<label>External wiki</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="external_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode "external"}}disabled{{end}}">
|
||||
<label for="wiki_url">External wiki URL</label>
|
||||
<input id="wiki_url" name="wiki_url" type="url" value="{{.Org.WikiURL}}" placeholder="https://wiki.example.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="require_2fa" {{if .Org.Require2FA}}checked{{end}}>
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization wiki">
|
||||
{{template "org/header" .}}
|
||||
{{template "org/menu" .}}
|
||||
<div class="ui container">
|
||||
{{if .WikiEmpty}}
|
||||
<div class="ui placeholder segment">
|
||||
<div class="ui icon header">
|
||||
{{svg "octicon-book" 48}}
|
||||
<br>
|
||||
This organization doesn't have a wiki yet.
|
||||
</div>
|
||||
<p class="tw-text-center">
|
||||
Create a repository named <code>wiki</code> (public) or <code>wiki-private</code> (members-only)
|
||||
with markdown files to get started.
|
||||
</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="repo-button-row tw-flex tw-items-center tw-gap-2 tw-mb-4">
|
||||
<div class="tw-flex-1 tw-flex tw-items-center tw-gap-2">
|
||||
{{svg "octicon-book" 16}}
|
||||
<strong>{{.CurrentPage}}</strong>
|
||||
</div>
|
||||
{{if .ShowWikiViewSelector}}
|
||||
<div class="ui dropdown jump">
|
||||
{{- $viewAsRole := Iif (.IsViewingWikiAsMember) (ctx.Locale.Tr "org.members.member") (ctx.Locale.Tr "settings.visibility.public") -}}
|
||||
<span class="text">{{svg "octicon-eye"}} {{ctx.Locale.Tr "org.view_as_role" $viewAsRole}}</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<a href="{{$.Org.HomeLink}}/-/wiki/{{.CurrentPage}}?view_as=public" class="item {{if not .IsViewingWikiAsMember}}selected{{end}}">
|
||||
{{svg "octicon-check" 14 (Iif (not .IsViewingWikiAsMember) "" "tw-invisible")}} {{ctx.Locale.Tr "settings.visibility.public"}}
|
||||
</a>
|
||||
<a href="{{$.Org.HomeLink}}/-/wiki/{{.CurrentPage}}?view_as=member" class="item {{if .IsViewingWikiAsMember}}selected{{end}}">
|
||||
{{svg "octicon-check" 14 (Iif .IsViewingWikiAsMember "" "tw-invisible")}} {{ctx.Locale.Tr "org.members.member"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .WikiPageNotFound}}
|
||||
<div class="ui segment">
|
||||
<div class="ui icon message">
|
||||
{{svg "octicon-alert" 24}}
|
||||
<div class="content">
|
||||
<div class="header">Page not found</div>
|
||||
<p>The page "{{.CurrentPage}}" does not exist in this wiki.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Pages}}
|
||||
<h4>Available pages:</h4>
|
||||
<ul>
|
||||
{{range .Pages}}
|
||||
<li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="wiki-content-parts">
|
||||
<div class="render-content markup wiki-content-main {{if or .WikiSidebarHTML .Pages}}with-sidebar{{end}}">
|
||||
{{.WikiContent}}
|
||||
</div>
|
||||
|
||||
{{if or .WikiSidebarHTML .Pages}}
|
||||
<div class="render-content markup wiki-content-sidebar">
|
||||
{{if .WikiSidebarHTML}}
|
||||
{{.WikiSidebarHTML}}
|
||||
<div class="ui divider"></div>
|
||||
{{end}}
|
||||
{{if .Pages}}
|
||||
<strong>{{svg "octicon-list-unordered" 14}} Pages</strong>
|
||||
<ul class="wiki-tree-list">
|
||||
{{range .Pages}}
|
||||
<li>
|
||||
{{svg "octicon-file" 14}}
|
||||
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="tw-clear-both"></div>
|
||||
|
||||
{{if .WikiFooterHTML}}
|
||||
<div class="render-content markup wiki-content-footer">
|
||||
{{.WikiFooterHTML}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
Reference in New Issue
Block a user