feat(repo): add hidden folders feature for code browser
Allow repository admins to hide specific folders from the code browser for non-admin users. Hidden folders are shown dimmed to admins but completely hidden from regular users. Includes database migration, settings UI, tree filtering logic, and frontend support for toggling visibility.
This commit is contained in:
@@ -414,6 +414,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(337, "Add is_private to package for package visibility", v1_26.AddIsPrivateToPackage),
|
||||
newMigration(338, "Add deleted_unix to action_runner table", v1_26.AddDeletedUnixToActionRunner),
|
||||
newMigration(339, "Add is_limited to repository for limited visibility", v1_26.AddIsLimitedToRepository),
|
||||
newMigration(340, "Create repo_hidden_folder table for hidden folders", v1_26.CreateRepoHiddenFolderTable),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
23
models/migrations/v1_26/v340.go
Normal file
23
models/migrations/v1_26/v340.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"code.gitcaddy.com/server/v3/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CreateRepoHiddenFolderTable creates the repo_hidden_folder table.
|
||||
// Hidden folders are not shown in the code browser for non-admin users.
|
||||
func CreateRepoHiddenFolderTable(x *xorm.Engine) error {
|
||||
type RepoHiddenFolder struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
||||
FolderPath string `xorm:"UNIQUE(s) NOT NULL"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
return x.Sync(new(RepoHiddenFolder))
|
||||
}
|
||||
83
models/repo/hidden_folder.go
Normal file
83
models/repo/hidden_folder.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
"code.gitcaddy.com/server/v3/modules/timeutil"
|
||||
)
|
||||
|
||||
// RepoHiddenFolder represents a folder that is hidden from the code browser
|
||||
// for non-admin users.
|
||||
type RepoHiddenFolder struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
||||
FolderPath string `xorm:"UNIQUE(s) NOT NULL"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(RepoHiddenFolder))
|
||||
}
|
||||
|
||||
// GetHiddenFolders returns all hidden folders for a repository.
|
||||
func GetHiddenFolders(ctx context.Context, repoID int64) ([]*RepoHiddenFolder, error) {
|
||||
folders := make([]*RepoHiddenFolder, 0)
|
||||
return folders, db.GetEngine(ctx).Where("repo_id = ?", repoID).OrderBy("folder_path").Find(&folders)
|
||||
}
|
||||
|
||||
// GetHiddenFolderPaths returns a set of hidden folder paths for fast lookup.
|
||||
func GetHiddenFolderPaths(ctx context.Context, repoID int64) (map[string]bool, error) {
|
||||
folders, err := GetHiddenFolders(ctx, repoID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths := make(map[string]bool, len(folders))
|
||||
for _, f := range folders {
|
||||
paths[f.FolderPath] = true
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// IsHiddenFolder checks if a specific folder path is hidden.
|
||||
func IsHiddenFolder(ctx context.Context, repoID int64, folderPath string) (bool, error) {
|
||||
return db.GetEngine(ctx).Where("repo_id = ? AND folder_path = ?", repoID, folderPath).Exist(&RepoHiddenFolder{})
|
||||
}
|
||||
|
||||
// AddHiddenFolder adds a folder to the hidden list.
|
||||
func AddHiddenFolder(ctx context.Context, repoID int64, folderPath string) error {
|
||||
_, err := db.GetEngine(ctx).Insert(&RepoHiddenFolder{
|
||||
RepoID: repoID,
|
||||
FolderPath: folderPath,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveHiddenFolder removes a folder from the hidden list.
|
||||
func RemoveHiddenFolder(ctx context.Context, repoID int64, folderPath string) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id = ? AND folder_path = ?", repoID, folderPath).Delete(&RepoHiddenFolder{})
|
||||
return err
|
||||
}
|
||||
|
||||
// IsPathUnderHiddenFolder checks if a path or any of its parent paths is hidden.
|
||||
func IsPathUnderHiddenFolder(hiddenPaths map[string]bool, targetPath string) bool {
|
||||
if hiddenPaths[targetPath] {
|
||||
return true
|
||||
}
|
||||
// Check parent paths
|
||||
for {
|
||||
idx := strings.LastIndex(targetPath, "/")
|
||||
if idx < 0 {
|
||||
break
|
||||
}
|
||||
targetPath = targetPath[:idx]
|
||||
if hiddenPaths[targetPath] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -4068,6 +4068,17 @@
|
||||
"repo.settings.gallery_error": "Failed to process gallery image.",
|
||||
"repo.settings.gallery_size_error": "Image must be less than 5MB.",
|
||||
"repo.settings.gallery_delete_confirm": "Are you sure you want to delete this image?",
|
||||
"repo.settings.hidden_folders": "Hidden Folders",
|
||||
"repo.settings.hidden_folders.description": "Hidden folders are not shown in the code browser for non-admin users. Admins can see them dimmed.",
|
||||
"repo.settings.hidden_folders.placeholder": "e.g. docs/internal",
|
||||
"repo.settings.hidden_folders.add": "Hide Folder",
|
||||
"repo.settings.hidden_folders.none": "No hidden folders configured.",
|
||||
"repo.settings.hidden_folders.hide": "Hide this folder",
|
||||
"repo.settings.hidden_folders.unhide": "Unhide this folder",
|
||||
"repo.settings.hidden_folders.added": "Folder hidden successfully.",
|
||||
"repo.settings.hidden_folders.removed": "Folder unhidden successfully.",
|
||||
"repo.settings.hidden_folders.not_found": "Folder path not found in repository.",
|
||||
"repo.settings.hidden_folders.already_hidden": "This folder is already hidden.",
|
||||
"repo.gallery": "Gallery",
|
||||
"api": "API",
|
||||
"admin.config.api_header_url": "API Header Link",
|
||||
|
||||
107
routers/web/repo/setting/hidden_folders.go
Normal file
107
routers/web/repo/setting/hidden_folders.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
"code.gitcaddy.com/server/v3/modules/log"
|
||||
"code.gitcaddy.com/server/v3/modules/templates"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
)
|
||||
|
||||
const tplHiddenFolders templates.TplName = "repo/settings/hidden_folders"
|
||||
|
||||
// HiddenFolders renders the hidden folders settings page.
|
||||
func HiddenFolders(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.hidden_folders")
|
||||
ctx.Data["PageIsSettingsHiddenFolders"] = true
|
||||
|
||||
folders, err := repo_model.GetHiddenFolders(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetHiddenFolders", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["HiddenFolders"] = folders
|
||||
ctx.HTML(http.StatusOK, tplHiddenFolders)
|
||||
}
|
||||
|
||||
// HiddenFoldersPost adds a new hidden folder.
|
||||
func HiddenFoldersPost(ctx *context.Context) {
|
||||
ctx.Data["PageIsSettingsHiddenFolders"] = true
|
||||
folderPath := strings.Trim(ctx.FormString("folder_path"), "/")
|
||||
|
||||
if folderPath == "" || strings.Contains(folderPath, "..") {
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.hidden_folders.not_found"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hidden_folders")
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_model.AddHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath); err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE") || strings.Contains(err.Error(), "Duplicate") {
|
||||
ctx.Flash.Warning(ctx.Tr("repo.settings.hidden_folders.already_hidden"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hidden_folders")
|
||||
return
|
||||
}
|
||||
ctx.ServerError("AddHiddenFolder", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.hidden_folders.added"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hidden_folders")
|
||||
}
|
||||
|
||||
// HiddenFoldersDelete removes a hidden folder.
|
||||
func HiddenFoldersDelete(ctx *context.Context) {
|
||||
folderPath := ctx.FormString("folder_path")
|
||||
if folderPath == "" {
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.hidden_folders.not_found"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hidden_folders")
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_model.RemoveHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath); err != nil {
|
||||
ctx.ServerError("RemoveHiddenFolder", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.hidden_folders.removed"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hidden_folders")
|
||||
}
|
||||
|
||||
// HiddenFoldersToggle is an AJAX endpoint to toggle a folder's hidden state.
|
||||
func HiddenFoldersToggle(ctx *context.Context) {
|
||||
folderPath := strings.Trim(ctx.FormString("folder_path"), "/")
|
||||
if folderPath == "" {
|
||||
ctx.JSON(http.StatusBadRequest, map[string]string{"error": "empty path"})
|
||||
return
|
||||
}
|
||||
|
||||
isHidden, err := repo_model.IsHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath)
|
||||
if err != nil {
|
||||
log.Error("IsHiddenFolder: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
|
||||
if isHidden {
|
||||
if err := repo_model.RemoveHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath); err != nil {
|
||||
log.Error("RemoveHiddenFolder: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := repo_model.AddHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath); err != nil {
|
||||
log.Error("AddHiddenFolder: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"hidden": !isHidden,
|
||||
})
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
pull_model "code.gitcaddy.com/server/v3/models/pull"
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
"code.gitcaddy.com/server/v3/modules/base"
|
||||
"code.gitcaddy.com/server/v3/modules/fileicon"
|
||||
"code.gitcaddy.com/server/v3/modules/git"
|
||||
@@ -144,7 +145,9 @@ func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTr
|
||||
|
||||
func TreeViewNodes(ctx *context.Context) {
|
||||
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||
results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.RepoLink, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
|
||||
hiddenPaths, _ := repo_model.GetHiddenFolderPaths(ctx, ctx.Repo.Repository.ID)
|
||||
isAdmin := ctx.Repo.IsAdmin()
|
||||
results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.RepoLink, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"), hiddenPaths, isAdmin)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTreeViewNodes", err)
|
||||
return
|
||||
|
||||
@@ -329,6 +329,28 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
||||
}
|
||||
}
|
||||
|
||||
// Filter hidden folders from the listing
|
||||
hiddenPaths, _ := repo_model.GetHiddenFolderPaths(ctx, ctx.Repo.Repository.ID)
|
||||
ctx.Data["HiddenFolderPaths"] = hiddenPaths
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.IsAdmin()
|
||||
if len(hiddenPaths) > 0 {
|
||||
isAdmin := ctx.Repo.IsAdmin()
|
||||
filtered := make([]git.CommitInfo, 0, len(files))
|
||||
for _, f := range files {
|
||||
if f.Entry.IsDir() {
|
||||
fullPath := path.Join(ctx.Repo.TreePath, f.Entry.Name())
|
||||
if hiddenPaths[fullPath] {
|
||||
if isAdmin {
|
||||
filtered = append(filtered, f)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
filtered = append(filtered, f)
|
||||
}
|
||||
files = filtered
|
||||
}
|
||||
|
||||
ctx.Data["Files"] = files
|
||||
prepareDirectoryFileIcons(ctx, files)
|
||||
for _, f := range files {
|
||||
|
||||
@@ -416,6 +416,15 @@ func handleRepoViewSubmodule(ctx *context.Context, commitSubmoduleFile *git.Comm
|
||||
|
||||
func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
// Block non-admin access to hidden folder contents
|
||||
if ctx.Repo.TreePath != "" && !ctx.Repo.IsAdmin() {
|
||||
hiddenPaths, _ := repo_model.GetHiddenFolderPaths(ctx, ctx.Repo.Repository.ID)
|
||||
if len(hiddenPaths) > 0 && repo_model.IsPathUnderHiddenFolder(hiddenPaths, ctx.Repo.TreePath) {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if entry.IsSubModule() {
|
||||
commitSubmoduleFile, err := git.GetCommitInfoSubmoduleFile(ctx.Repo.RepoLink, ctx.Repo.TreePath, ctx.Repo.Commit, entry.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -1195,6 +1195,13 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/delete", repo_setting.GalleryDelete)
|
||||
}, repo.MustBeNotEmpty)
|
||||
|
||||
m.Group("/hidden_folders", func() {
|
||||
m.Get("", repo_setting.HiddenFolders)
|
||||
m.Post("", repo_setting.HiddenFoldersPost)
|
||||
m.Post("/delete", repo_setting.HiddenFoldersDelete)
|
||||
m.Post("/toggle", repo_setting.HiddenFoldersToggle)
|
||||
})
|
||||
|
||||
m.Combo("/public_access").Get(repo_setting.PublicAccess).Post(repo_setting.PublicAccessPost)
|
||||
|
||||
m.Group("/collaboration", func() {
|
||||
|
||||
@@ -145,6 +145,7 @@ type TreeViewNode struct {
|
||||
FullPath string `json:"fullPath"`
|
||||
SubmoduleURL string `json:"submoduleUrl,omitempty"`
|
||||
Children []*TreeViewNode `json:"children,omitempty"`
|
||||
IsHidden bool `json:"isHidden,omitempty"`
|
||||
}
|
||||
|
||||
func (node *TreeViewNode) sortLevel() int {
|
||||
@@ -191,7 +192,7 @@ func sortTreeViewNodes(nodes []*TreeViewNode) {
|
||||
})
|
||||
}
|
||||
|
||||
func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) {
|
||||
func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string, hiddenPaths map[string]bool, isAdmin bool) ([]*TreeViewNode, error) {
|
||||
entries, err := tree.ListEntries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -201,13 +202,22 @@ func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *filei
|
||||
nodes := make([]*TreeViewNode, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
node := newTreeViewNodeFromEntry(ctx, repoLink, renderedIconPool, commit, treePath, entry)
|
||||
|
||||
// Filter hidden folders
|
||||
if entry.IsDir() && len(hiddenPaths) > 0 && hiddenPaths[node.FullPath] {
|
||||
if !isAdmin {
|
||||
continue // skip for non-admins
|
||||
}
|
||||
node.IsHidden = true
|
||||
}
|
||||
|
||||
nodes = append(nodes, node)
|
||||
if entry.IsDir() && subPathDirName == entry.Name() {
|
||||
subTreePath := treePath + "/" + node.EntryName
|
||||
if subTreePath[0] == '/' {
|
||||
subTreePath = subTreePath[1:]
|
||||
}
|
||||
subNodes, err := listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining)
|
||||
subNodes, err := listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining, hiddenPaths, isAdmin)
|
||||
if err != nil {
|
||||
log.Error("listTreeNodes: %v", err)
|
||||
} else {
|
||||
@@ -219,10 +229,10 @@ func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *filei
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func GetTreeViewNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) {
|
||||
func GetTreeViewNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string, hiddenPaths map[string]bool, isAdmin bool) ([]*TreeViewNode, error) {
|
||||
entry, err := commit.GetTreeEntryByPath(treePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), treePath, subPath)
|
||||
return listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), treePath, subPath, hiddenPaths, isAdmin)
|
||||
}
|
||||
|
||||
38
templates/repo/settings/hidden_folders.tmpl
Normal file
38
templates/repo/settings/hidden_folders.tmpl
Normal file
@@ -0,0 +1,38 @@
|
||||
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit")}}
|
||||
<div class="repo-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "repo.settings.hidden_folders"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p class="tw-text-secondary">{{ctx.Locale.Tr "repo.settings.hidden_folders.description"}}</p>
|
||||
<form class="ui form" method="post" action="{{.Link}}">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="ui action input tw-w-full">
|
||||
<input name="folder_path" placeholder="{{ctx.Locale.Tr "repo.settings.hidden_folders.placeholder"}}" required>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.hidden_folders.add"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{if .HiddenFolders}}
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui relaxed divided list">
|
||||
{{range .HiddenFolders}}
|
||||
<div class="item tw-flex tw-items-center tw-justify-between">
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
{{svg "octicon-file-directory-fill" 16}} <code>{{.FolderPath}}</code>
|
||||
</div>
|
||||
<form method="post" action="{{$.Link}}/delete">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="folder_path" value="{{.FolderPath}}">
|
||||
<button class="ui tiny red button">{{ctx.Locale.Tr "remove"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="ui divider"></div>
|
||||
<p class="tw-text-center tw-text-secondary">{{ctx.Locale.Tr "repo.settings.hidden_folders.none"}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "repo/settings/layout_footer" .}}
|
||||
@@ -30,6 +30,9 @@
|
||||
<a class="{{if .PageIsSettingsTags}}active {{end}}item" href="{{.RepoLink}}/settings/tags">
|
||||
{{ctx.Locale.Tr "repo.settings.tags"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsHiddenFolders}}active {{end}}item" href="{{.RepoLink}}/settings/hidden_folders">
|
||||
{{ctx.Locale.Tr "repo.settings.hidden_folders"}}
|
||||
</a>
|
||||
{{if .SignedUser.CanEditGitHook}}
|
||||
<a class="{{if .PageIsSettingsGitHooks}}active {{end}}item" href="{{.RepoLink}}/settings/hooks/git">
|
||||
{{ctx.Locale.Tr "repo.settings.githooks"}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}}
|
||||
<div id="repo-files-table" {{if .HasFilesWithoutLatestCommit}}hx-indicator="#repo-files-table .repo-file-cell.message" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
|
||||
<div id="repo-files-table" data-toggle-url="{{.RepoLink}}/settings/hidden_folders/toggle" {{if .HasFilesWithoutLatestCommit}}hx-indicator="#repo-files-table .repo-file-cell.message" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
|
||||
<div class="repo-file-line repo-file-last-commit">
|
||||
{{template "repo/latest_commit" .}}
|
||||
<div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div>
|
||||
@@ -11,10 +11,16 @@
|
||||
</a>
|
||||
{{end}}
|
||||
{{range $item := .Files}}
|
||||
<div class="repo-file-item">
|
||||
{{$entry := $item.Entry}}
|
||||
{{$commit := $item.Commit}}
|
||||
{{$submoduleFile := $item.SubmoduleFile}}
|
||||
{{$entry := $item.Entry}}
|
||||
{{$commit := $item.Commit}}
|
||||
{{$submoduleFile := $item.SubmoduleFile}}
|
||||
{{$folderFullPath := $entry.Name}}
|
||||
{{if $.TreePath}}{{$folderFullPath = printf "%s/%s" $.TreePath $entry.Name}}{{end}}
|
||||
{{$isHiddenFolder := false}}
|
||||
{{if and $entry.IsDir $.HiddenFolderPaths}}
|
||||
{{if index $.HiddenFolderPaths $folderFullPath}}{{$isHiddenFolder = true}}{{end}}
|
||||
{{end}}
|
||||
<div class="repo-file-item{{if $isHiddenFolder}} hidden-folder-dimmed{{end}}">
|
||||
<div class="repo-file-cell name muted-links {{if not $commit}}notready{{end}}">
|
||||
{{index $.FileIcons $entry.Name}}
|
||||
{{if $entry.IsSubModule}}
|
||||
@@ -62,6 +68,17 @@
|
||||
{{svg "octicon-shield-lock" 16}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if and $entry.IsDir (not $entry.IsSubModule) $.IsRepoAdmin}}
|
||||
{{if $isHiddenFolder}}
|
||||
<button class="repo-file-cell-action btn-octicon hidden-folder-toggle" data-folder-path="{{$folderFullPath}}" data-tooltip-content="{{ctx.Locale.Tr "repo.settings.hidden_folders.unhide"}}">
|
||||
{{svg "octicon-eye" 16}}
|
||||
</button>
|
||||
{{else}}
|
||||
<button class="repo-file-cell-action btn-octicon hidden-folder-toggle" data-folder-path="{{$folderFullPath}}" data-tooltip-content="{{ctx.Locale.Tr "repo.settings.hidden_folders.hide"}}">
|
||||
{{svg "octicon-eye-closed" 16}}
|
||||
</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -109,3 +109,11 @@
|
||||
#repo-files-table .repo-file-item:hover .repo-file-cell-action {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
#repo-files-table .repo-file-item.hidden-folder-dimmed > .repo-file-cell {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
#repo-files-table .repo-file-item.hidden-folder-dimmed:hover > .repo-file-cell {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ const onItemClick = (e: MouseEvent) => {
|
||||
'type-directory': item.entryMode === 'tree',
|
||||
'type-symlink': item.entryMode === 'symlink',
|
||||
'type-file': item.entryMode === 'blob' || item.entryMode === 'exec',
|
||||
'is-hidden-folder': item.isHidden,
|
||||
}"
|
||||
:title="item.entryName"
|
||||
:href="store.buildTreePathWebUrl(item.fullPath)"
|
||||
@@ -117,4 +118,12 @@ const onItemClick = (e: MouseEvent) => {
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tree-item.is-hidden-folder {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.tree-item.is-hidden-folder:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,7 @@ export type FileTreeItem = {
|
||||
fullPath: string;
|
||||
submoduleUrl?: string;
|
||||
children?: Array<FileTreeItem>;
|
||||
isHidden?: boolean;
|
||||
};
|
||||
|
||||
export function createViewFileTreeStore(props: {repoLink: string, treePath: string, currentRefNameSubURL: string}) {
|
||||
|
||||
26
web_src/js/features/repo-hidden-folders.ts
Normal file
26
web_src/js/features/repo-hidden-folders.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
|
||||
export function initRepoHiddenFolderToggle() {
|
||||
const table = document.querySelector<HTMLElement>('#repo-files-table');
|
||||
if (!table) return;
|
||||
|
||||
const toggleUrl = table.getAttribute('data-toggle-url');
|
||||
if (!toggleUrl) return;
|
||||
|
||||
for (const btn of table.querySelectorAll<HTMLButtonElement>('.hidden-folder-toggle')) {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const folderPath = btn.getAttribute('data-folder-path');
|
||||
if (!folderPath) return;
|
||||
|
||||
const data = new FormData();
|
||||
data.append('folder_path', folderPath);
|
||||
|
||||
const resp = await POST(toggleUrl, {data});
|
||||
if (resp.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton}
|
||||
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
|
||||
import {callInitFunctions} from './modules/init.ts';
|
||||
import {initRepoViewFileTree} from './features/repo-view-file-tree.ts';
|
||||
import {initRepoHiddenFolderToggle} from './features/repo-hidden-folders.ts';
|
||||
|
||||
const initStartTime = performance.now();
|
||||
const initPerformanceTracer = callInitFunctions([
|
||||
@@ -136,6 +137,7 @@ const initPerformanceTracer = callInitFunctions([
|
||||
initRepoReleaseNew,
|
||||
initRepoTopicBar,
|
||||
initRepoViewFileTree,
|
||||
initRepoHiddenFolderToggle,
|
||||
initRepoWikiForm,
|
||||
initRepository,
|
||||
initRepositoryActionView,
|
||||
|
||||
Reference in New Issue
Block a user