bfb4b53da3
Deploy MokoGitea / deploy (push) Successful in 3m31s
Replace flat page list with hierarchical folder tree in org wiki sidebar. _Sidebar.md takes precedence when present; otherwise auto-generates collapsible folder menus up to 2 levels deep.
273 lines
8.4 KiB
Go
273 lines
8.4 KiB
Go
// 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
|
|
}
|
|
|
|
// OrgWikiTreeNode represents a node in the org wiki folder tree for sidebar navigation.
|
|
type OrgWikiTreeNode struct {
|
|
Name string
|
|
SubURL string
|
|
IsDir bool
|
|
Children []*OrgWikiTreeNode
|
|
}
|
|
|
|
// 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 folder tree for sidebar navigation.
|
|
wikiTree := buildOrgWikiTree(commit)
|
|
ctx.Data["WikiTree"] = wikiTree
|
|
|
|
// 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)
|
|
}
|
|
|
|
// buildOrgWikiTree builds a hierarchical folder tree from the org wiki git repo.
|
|
// Shows up to 2 levels deep (folders and their immediate children).
|
|
func buildOrgWikiTree(commit *git.Commit) []*OrgWikiTreeNode {
|
|
if commit == nil {
|
|
return nil
|
|
}
|
|
entries, err := commit.ListEntries()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var topLevel []*OrgWikiTreeNode
|
|
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
if entry.IsDir() {
|
|
node := &OrgWikiTreeNode{
|
|
Name: name,
|
|
SubURL: name,
|
|
IsDir: true,
|
|
}
|
|
// List children of this directory (1 level deep).
|
|
subTree := entry.Tree()
|
|
if subTree != nil {
|
|
children, _ := subTree.ListEntries()
|
|
for _, child := range children {
|
|
childName := child.Name()
|
|
if child.IsDir() {
|
|
node.Children = append(node.Children, &OrgWikiTreeNode{
|
|
Name: childName,
|
|
SubURL: name + "/" + childName,
|
|
IsDir: true,
|
|
})
|
|
} else if isMarkdownFile(childName) {
|
|
displayName := strings.TrimSuffix(childName, path.Ext(childName))
|
|
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
|
|
continue
|
|
}
|
|
node.Children = append(node.Children, &OrgWikiTreeNode{
|
|
Name: displayName,
|
|
SubURL: name + "/" + displayName,
|
|
IsDir: false,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
topLevel = append(topLevel, node)
|
|
} else if isMarkdownFile(name) {
|
|
displayName := strings.TrimSuffix(name, path.Ext(name))
|
|
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
|
|
continue
|
|
}
|
|
topLevel = append(topLevel, &OrgWikiTreeNode{
|
|
Name: displayName,
|
|
SubURL: displayName,
|
|
IsDir: false,
|
|
})
|
|
}
|
|
}
|
|
return topLevel
|
|
}
|
|
|
|
// findOrgWikiCommit locates the profile repo's wiki and returns its HEAD commit.
|
|
// The org wiki lives in the .wiki.git sidecar of the profile repo (e.g. .mokogitea.wiki.git).
|
|
// Tries fallback repo names (.profile, .github) if the primary doesn't exist.
|
|
func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) {
|
|
namesToTry := shared_user.ProfileRepoFallbacks(repoName)
|
|
|
|
var dbRepo *repo_model.Repository
|
|
var err error
|
|
for _, name := range namesToTry {
|
|
dbRepo, err = repo_model.GetRepositoryByName(ctx, orgID, name)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if !repo_model.IsErrRepoNotExist(err) {
|
|
log.Error("findOrgWikiCommit: GetRepositoryByName(%d, %s): %v", orgID, name, err)
|
|
return nil, nil
|
|
}
|
|
}
|
|
if dbRepo == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// Open the wiki git repo (.wiki.git sidecar), not the main repo.
|
|
wikiGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo.WikiStorageRepo())
|
|
if err != nil {
|
|
// Wiki repo doesn't exist yet — not an error, just no wiki.
|
|
return nil, nil
|
|
}
|
|
|
|
branch := dbRepo.DefaultWikiBranch
|
|
if branch == "" {
|
|
branch = "main"
|
|
}
|
|
commit, err := wikiGitRepo.GetBranchCommit(branch)
|
|
if err != nil {
|
|
log.Error("findOrgWikiCommit: GetBranchCommit wiki(%s, %s): %v", dbRepo.FullName(), branch, 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"
|
|
}
|