mirror of
https://github.com/go-gitea/gitea.git
synced 2025-12-05 00:34:09 +09:00
Add "Go to file", "Delete Directory" to repo file list page (#35911)
/claim #35898 Resolves #35898 ### Summary of key changes: 1. Add file name search/Go to file functionality to repo button row. 2. Add backend functionality to delete directory 3. Add context menu for directories with functionality to copy path & delete a directory 4. Move Add/Upload file dropdown to right for parity with Github UI 5. Add tree view to the edit/upload UI --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -1354,8 +1354,11 @@ editor.this_file_locked = File is locked
|
|||||||
editor.must_be_on_a_branch = You must be on a branch to make or propose changes to this file.
|
editor.must_be_on_a_branch = You must be on a branch to make or propose changes to this file.
|
||||||
editor.fork_before_edit = You must fork this repository to make or propose changes to this file.
|
editor.fork_before_edit = You must fork this repository to make or propose changes to this file.
|
||||||
editor.delete_this_file = Delete File
|
editor.delete_this_file = Delete File
|
||||||
|
editor.delete_this_directory = Delete Directory
|
||||||
editor.must_have_write_access = You must have write access to make or propose changes to this file.
|
editor.must_have_write_access = You must have write access to make or propose changes to this file.
|
||||||
editor.file_delete_success = File "%s" has been deleted.
|
editor.file_delete_success = File "%s" has been deleted.
|
||||||
|
editor.directory_delete_success = Directory "%s" has been deleted.
|
||||||
|
editor.delete_directory = Delete directory '%s'
|
||||||
editor.name_your_file = Name your file…
|
editor.name_your_file = Name your file…
|
||||||
editor.filename_help = Add a directory by typing its name followed by a slash ('/'). Remove a directory by typing backspace at the beginning of the input field.
|
editor.filename_help = Add a directory by typing its name followed by a slash ('/'). Remove a directory by typing backspace at the beginning of the input field.
|
||||||
editor.or = or
|
editor.or = or
|
||||||
|
|||||||
@@ -610,10 +610,6 @@ func handleChangeRepoFilesError(ctx *context.APIContext, err error) {
|
|||||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
|
|
||||||
ctx.APIError(http.StatusNotFound, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.Is(err, util.ErrNotExist) {
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
ctx.APIError(http.StatusNotFound, err)
|
ctx.APIError(http.StatusNotFound, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
@@ -42,8 +41,8 @@ type blameRow struct {
|
|||||||
|
|
||||||
// RefBlame render blame page
|
// RefBlame render blame page
|
||||||
func RefBlame(ctx *context.Context) {
|
func RefBlame(ctx *context.Context) {
|
||||||
ctx.Data["PageIsViewCode"] = true
|
|
||||||
ctx.Data["IsBlame"] = true
|
ctx.Data["IsBlame"] = true
|
||||||
|
prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL())
|
||||||
|
|
||||||
// Get current entry user currently looking at.
|
// Get current entry user currently looking at.
|
||||||
if ctx.Repo.TreePath == "" {
|
if ctx.Repo.TreePath == "" {
|
||||||
@@ -56,17 +55,6 @@ func RefBlame(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
treeNames := strings.Split(ctx.Repo.TreePath, "/")
|
|
||||||
var paths []string
|
|
||||||
for i := range treeNames {
|
|
||||||
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["Paths"] = paths
|
|
||||||
ctx.Data["TreeNames"] = treeNames
|
|
||||||
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
|
||||||
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
|
||||||
|
|
||||||
blob := entry.Blob()
|
blob := entry.Blob()
|
||||||
fileSize := blob.Size()
|
fileSize := blob.Size()
|
||||||
ctx.Data["FileSize"] = fileSize
|
ctx.Data["FileSize"] = fileSize
|
||||||
|
|||||||
@@ -41,7 +41,12 @@ const (
|
|||||||
editorCommitChoiceNewBranch string = "commit-to-new-branch"
|
editorCommitChoiceNewBranch string = "commit-to-new-branch"
|
||||||
)
|
)
|
||||||
|
|
||||||
func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
|
func prepareEditorPage(ctx *context.Context, editorAction string) *context.CommitFormOptions {
|
||||||
|
prepareHomeTreeSideBarSwitch(ctx)
|
||||||
|
return prepareEditorPageFormOptions(ctx, editorAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareEditorPageFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
|
||||||
cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
|
cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
|
||||||
if cleanedTreePath != ctx.Repo.TreePath {
|
if cleanedTreePath != ctx.Repo.TreePath {
|
||||||
redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
|
redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
|
||||||
@@ -283,7 +288,7 @@ func EditFile(ctx *context.Context) {
|
|||||||
// on the "New File" page, we should add an empty path field to make end users could input a new name
|
// on the "New File" page, we should add an empty path field to make end users could input a new name
|
||||||
prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath))
|
prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath))
|
||||||
|
|
||||||
prepareEditorCommitFormOptions(ctx, editorAction)
|
prepareEditorPage(ctx, editorAction)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -376,15 +381,16 @@ func EditFilePost(ctx *context.Context) {
|
|||||||
|
|
||||||
// DeleteFile render delete file page
|
// DeleteFile render delete file page
|
||||||
func DeleteFile(ctx *context.Context) {
|
func DeleteFile(ctx *context.Context) {
|
||||||
prepareEditorCommitFormOptions(ctx, "_delete")
|
prepareEditorPage(ctx, "_delete")
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["PageIsDelete"] = true
|
ctx.Data["PageIsDelete"] = true
|
||||||
|
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
|
||||||
ctx.HTML(http.StatusOK, tplDeleteFile)
|
ctx.HTML(http.StatusOK, tplDeleteFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteFilePost response for deleting file
|
// DeleteFilePost response for deleting file or directory
|
||||||
func DeleteFilePost(ctx *context.Context) {
|
func DeleteFilePost(ctx *context.Context) {
|
||||||
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
|
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
@@ -392,7 +398,26 @@ func DeleteFilePost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
treePath := ctx.Repo.TreePath
|
treePath := ctx.Repo.TreePath
|
||||||
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
if treePath == "" {
|
||||||
|
ctx.JSONError("cannot delete root directory") // it should not happen unless someone is trying to be malicious
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the path is a directory
|
||||||
|
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
|
||||||
|
if err != nil {
|
||||||
|
ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var commitMessage string
|
||||||
|
if entry.IsDir() {
|
||||||
|
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete_directory", treePath))
|
||||||
|
} else {
|
||||||
|
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||||
LastCommitID: parsed.form.LastCommit,
|
LastCommitID: parsed.form.LastCommit,
|
||||||
OldBranch: parsed.OldBranchName,
|
OldBranch: parsed.OldBranchName,
|
||||||
NewBranch: parsed.NewBranchName,
|
NewBranch: parsed.NewBranchName,
|
||||||
@@ -400,9 +425,10 @@ func DeleteFilePost(ctx *context.Context) {
|
|||||||
{
|
{
|
||||||
Operation: "delete",
|
Operation: "delete",
|
||||||
TreePath: treePath,
|
TreePath: treePath,
|
||||||
|
DeleteRecursively: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
|
Message: commitMessage,
|
||||||
Signoff: parsed.form.Signoff,
|
Signoff: parsed.form.Signoff,
|
||||||
Author: parsed.GitCommitter,
|
Author: parsed.GitCommitter,
|
||||||
Committer: parsed.GitCommitter,
|
Committer: parsed.GitCommitter,
|
||||||
@@ -412,7 +438,11 @@ func DeleteFilePost(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if entry.IsDir() {
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.editor.directory_delete_success", treePath))
|
||||||
|
} else {
|
||||||
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
|
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
|
||||||
|
}
|
||||||
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
|
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
|
||||||
redirectForCommitChoice(ctx, parsed, redirectTreePath)
|
redirectForCommitChoice(ctx, parsed, redirectTreePath)
|
||||||
}
|
}
|
||||||
@@ -420,7 +450,7 @@ func DeleteFilePost(ctx *context.Context) {
|
|||||||
func UploadFile(ctx *context.Context) {
|
func UploadFile(ctx *context.Context) {
|
||||||
ctx.Data["PageIsUpload"] = true
|
ctx.Data["PageIsUpload"] = true
|
||||||
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
|
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
|
||||||
opts := prepareEditorCommitFormOptions(ctx, "_upload")
|
opts := prepareEditorPage(ctx, "_upload")
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewDiffPatch(ctx *context.Context) {
|
func NewDiffPatch(ctx *context.Context) {
|
||||||
prepareEditorCommitFormOptions(ctx, "_diffpatch")
|
prepareEditorPage(ctx, "_diffpatch")
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func CherryPick(ctx *context.Context) {
|
func CherryPick(ctx *context.Context) {
|
||||||
prepareEditorCommitFormOptions(ctx, "_cherrypick")
|
prepareEditorPage(ctx, "_cherrypick")
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/templates"
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
"code.gitea.io/gitea/services/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tplFindFiles templates.TplName = "repo/find/files"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FindFiles render the page to find repository files
|
|
||||||
func FindFiles(ctx *context.Context) {
|
|
||||||
path := ctx.PathParam("*")
|
|
||||||
ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + util.PathEscapeSegments(path)
|
|
||||||
ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + util.PathEscapeSegments(path)
|
|
||||||
ctx.HTML(http.StatusOK, tplFindFiles)
|
|
||||||
}
|
|
||||||
@@ -245,27 +245,17 @@ func LastCommit(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The "/lastcommit/" endpoint is used to render the embedded HTML content for the directory file listing with latest commit info
|
||||||
|
// It needs to construct correct links to the file items, but the route only accepts a commit ID, not a full ref name (branch or tag).
|
||||||
|
// So we need to get the ref name from the query parameter "refSubUrl".
|
||||||
|
// TODO: LAST-COMMIT-ASYNC-LOADING: it needs more tests to cover this
|
||||||
|
refSubURL := path.Clean(ctx.FormString("refSubUrl"))
|
||||||
|
prepareRepoViewContent(ctx, util.IfZero(refSubURL, ctx.Repo.RefTypeNameSubURL()))
|
||||||
renderDirectoryFiles(ctx, 0)
|
renderDirectoryFiles(ctx, 0)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var treeNames []string
|
|
||||||
paths := make([]string, 0, 5)
|
|
||||||
if len(ctx.Repo.TreePath) > 0 {
|
|
||||||
treeNames = strings.Split(ctx.Repo.TreePath, "/")
|
|
||||||
for i := range treeNames {
|
|
||||||
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["HasParentPath"] = true
|
|
||||||
if len(paths)-2 >= 0 {
|
|
||||||
ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
|
||||||
ctx.Data["BranchLink"] = branchLink
|
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplRepoViewList)
|
ctx.HTML(http.StatusOK, tplRepoViewList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +279,9 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
// TODO: LAST-COMMIT-ASYNC-LOADING: search this keyword to see more details
|
||||||
|
lastCommitLoaderURL := ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
||||||
|
ctx.Data["LastCommitLoaderURL"] = lastCommitLoaderURL + "?refSubUrl=" + url.QueryEscape(ctx.Repo.RefTypeNameSubURL())
|
||||||
|
|
||||||
// Get current entry user currently looking at.
|
// Get current entry user currently looking at.
|
||||||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
||||||
@@ -322,6 +314,21 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
|||||||
ctx.ServerError("GetCommitsInfo", err)
|
ctx.ServerError("GetCommitsInfo", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
if timeout != 0 && !setting.IsProd && !setting.IsInTesting {
|
||||||
|
log.Debug("first call to get directory file commit info")
|
||||||
|
clearFilesCommitInfo := func() {
|
||||||
|
log.Warn("clear directory file commit info to force async loading on frontend")
|
||||||
|
for i := range files {
|
||||||
|
files[i].Commit = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = clearFilesCommitInfo
|
||||||
|
// clearFilesCommitInfo() // TODO: LAST-COMMIT-ASYNC-LOADING: debug the frontend async latest commit info loading, uncomment this line, and it needs more tests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Data["Files"] = files
|
ctx.Data["Files"] = files
|
||||||
prepareDirectoryFileIcons(ctx, files)
|
prepareDirectoryFileIcons(ctx, files)
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
@@ -334,16 +341,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
|||||||
if !loadLatestCommitData(ctx, latestCommit) {
|
if !loadLatestCommitData(ctx, latestCommit) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
|
||||||
treeLink := branchLink
|
|
||||||
|
|
||||||
if len(ctx.Repo.TreePath) > 0 {
|
|
||||||
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["TreeLink"] = treeLink
|
|
||||||
|
|
||||||
return allEntries
|
return allEntries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -362,6 +362,32 @@ func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) b
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareRepoViewContent(ctx *context.Context, refTypeNameSubURL string) {
|
||||||
|
// for: home, file list, file view, blame
|
||||||
|
ctx.Data["PageIsViewCode"] = true
|
||||||
|
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show Upload File button or menu item
|
||||||
|
|
||||||
|
// prepare the tree path navigation
|
||||||
|
var treeNames, paths []string
|
||||||
|
branchLink := ctx.Repo.RepoLink + "/src/" + refTypeNameSubURL
|
||||||
|
treeLink := branchLink
|
||||||
|
if ctx.Repo.TreePath != "" {
|
||||||
|
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
||||||
|
treeNames = strings.Split(ctx.Repo.TreePath, "/")
|
||||||
|
for i := range treeNames {
|
||||||
|
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
|
||||||
|
}
|
||||||
|
ctx.Data["HasParentPath"] = true
|
||||||
|
if len(paths)-2 >= 0 {
|
||||||
|
ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.Data["Paths"] = paths
|
||||||
|
ctx.Data["TreeLink"] = treeLink
|
||||||
|
ctx.Data["TreeNames"] = treeNames
|
||||||
|
ctx.Data["BranchLink"] = branchLink
|
||||||
|
}
|
||||||
|
|
||||||
// Home render repository home page
|
// Home render repository home page
|
||||||
func Home(ctx *context.Context) {
|
func Home(ctx *context.Context) {
|
||||||
if handleRepoHomeFeed(ctx) {
|
if handleRepoHomeFeed(ctx) {
|
||||||
@@ -383,8 +409,7 @@ func Home(ctx *context.Context) {
|
|||||||
title += ": " + ctx.Repo.Repository.Description
|
title += ": " + ctx.Repo.Repository.Description
|
||||||
}
|
}
|
||||||
ctx.Data["Title"] = title
|
ctx.Data["Title"] = title
|
||||||
ctx.Data["PageIsViewCode"] = true
|
prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL())
|
||||||
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show New File / Upload File buttons
|
|
||||||
|
|
||||||
if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
|
if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
|
||||||
// empty or broken repositories need to be handled differently
|
// empty or broken repositories need to be handled differently
|
||||||
@@ -405,26 +430,6 @@ func Home(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare the tree path
|
|
||||||
var treeNames, paths []string
|
|
||||||
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
|
||||||
treeLink := branchLink
|
|
||||||
if ctx.Repo.TreePath != "" {
|
|
||||||
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
|
||||||
treeNames = strings.Split(ctx.Repo.TreePath, "/")
|
|
||||||
for i := range treeNames {
|
|
||||||
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
|
|
||||||
}
|
|
||||||
ctx.Data["HasParentPath"] = true
|
|
||||||
if len(paths)-2 >= 0 {
|
|
||||||
ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.Data["Paths"] = paths
|
|
||||||
ctx.Data["TreeLink"] = treeLink
|
|
||||||
ctx.Data["TreeNames"] = treeNames
|
|
||||||
ctx.Data["BranchLink"] = branchLink
|
|
||||||
|
|
||||||
// some UI components are only shown when the tree path is root
|
// some UI components are only shown when the tree path is root
|
||||||
isTreePathRoot := ctx.Repo.TreePath == ""
|
isTreePathRoot := ctx.Repo.TreePath == ""
|
||||||
|
|
||||||
@@ -455,7 +460,7 @@ func Home(ctx *context.Context) {
|
|||||||
|
|
||||||
if isViewHomeOnlyContent(ctx) {
|
if isViewHomeOnlyContent(ctx) {
|
||||||
ctx.HTML(http.StatusOK, tplRepoViewContent)
|
ctx.HTML(http.StatusOK, tplRepoViewContent)
|
||||||
} else if len(treeNames) != 0 {
|
} else if ctx.Repo.TreePath != "" {
|
||||||
ctx.HTML(http.StatusOK, tplRepoView)
|
ctx.HTML(http.StatusOK, tplRepoView)
|
||||||
} else {
|
} else {
|
||||||
ctx.HTML(http.StatusOK, tplRepoHome)
|
ctx.HTML(http.StatusOK, tplRepoHome)
|
||||||
|
|||||||
@@ -1184,7 +1184,6 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup)
|
m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup)
|
||||||
|
|
||||||
m.Group("/{username}/{reponame}", func() {
|
m.Group("/{username}/{reponame}", func() {
|
||||||
m.Get("/find/*", repo.FindFiles)
|
|
||||||
m.Group("/tree-list", func() {
|
m.Group("/tree-list", func() {
|
||||||
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeList)
|
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeList)
|
||||||
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList)
|
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList)
|
||||||
|
|||||||
@@ -135,6 +135,14 @@ func (t *TemporaryUploadRepository) LsFiles(ctx context.Context, filenames ...st
|
|||||||
return fileList, nil
|
return fileList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TemporaryUploadRepository) RemoveRecursivelyFromIndex(ctx context.Context, path string) error {
|
||||||
|
_, _, err := gitcmd.NewCommand("rm", "--cached", "-r").
|
||||||
|
AddDynamicArguments(path).
|
||||||
|
WithDir(t.basePath).
|
||||||
|
RunStdBytes(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveFilesFromIndex removes the given files from the index
|
// RemoveFilesFromIndex removes the given files from the index
|
||||||
func (t *TemporaryUploadRepository) RemoveFilesFromIndex(ctx context.Context, filenames ...string) error {
|
func (t *TemporaryUploadRepository) RemoveFilesFromIndex(ctx context.Context, filenames ...string) error {
|
||||||
objFmt, err := t.gitRepo.GetObjectFormat()
|
objFmt, err := t.gitRepo.GetObjectFormat()
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ type ChangeRepoFile struct {
|
|||||||
FromTreePath string
|
FromTreePath string
|
||||||
ContentReader io.ReadSeeker
|
ContentReader io.ReadSeeker
|
||||||
SHA string
|
SHA string
|
||||||
Options *RepoFileOptions
|
|
||||||
|
DeleteRecursively bool // when deleting, work as `git rm -r ...`
|
||||||
|
|
||||||
|
Options *RepoFileOptions // FIXME: need to refactor, internal usage only
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeRepoFilesOptions holds the repository files update options
|
// ChangeRepoFilesOptions holds the repository files update options
|
||||||
@@ -69,26 +72,6 @@ type RepoFileOptions struct {
|
|||||||
executable bool
|
executable bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrRepoFileDoesNotExist represents a "RepoFileDoesNotExist" kind of error.
|
|
||||||
type ErrRepoFileDoesNotExist struct {
|
|
||||||
Path string
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrRepoFileDoesNotExist checks if an error is a ErrRepoDoesNotExist.
|
|
||||||
func IsErrRepoFileDoesNotExist(err error) bool {
|
|
||||||
_, ok := err.(ErrRepoFileDoesNotExist)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrRepoFileDoesNotExist) Error() string {
|
|
||||||
return fmt.Sprintf("repository file does not exist [path: %s]", err.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrRepoFileDoesNotExist) Unwrap() error {
|
|
||||||
return util.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
type LazyReadSeeker interface {
|
type LazyReadSeeker interface {
|
||||||
io.ReadSeeker
|
io.ReadSeeker
|
||||||
io.Closer
|
io.Closer
|
||||||
@@ -217,24 +200,6 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range opts.Files {
|
|
||||||
if file.Operation == "delete" {
|
|
||||||
// Get the files in the index
|
|
||||||
filesInIndex, err := t.LsFiles(ctx, file.TreePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("DeleteRepoFile: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the file we want to delete in the index
|
|
||||||
inFilelist := slices.Contains(filesInIndex, file.TreePath)
|
|
||||||
if !inFilelist {
|
|
||||||
return nil, ErrRepoFileDoesNotExist{
|
|
||||||
Path: file.TreePath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasOldBranch {
|
if hasOldBranch {
|
||||||
// Get the commit of the original branch
|
// Get the commit of the original branch
|
||||||
commit, err := t.GetBranchCommit(opts.OldBranch)
|
commit, err := t.GetBranchCommit(opts.OldBranch)
|
||||||
@@ -272,9 +237,15 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
|||||||
addedLfsPointers = append(addedLfsPointers, *addedLfsPointer)
|
addedLfsPointers = append(addedLfsPointers, *addedLfsPointer)
|
||||||
}
|
}
|
||||||
case "delete":
|
case "delete":
|
||||||
|
if file.DeleteRecursively {
|
||||||
|
if err = t.RemoveRecursivelyFromIndex(ctx, file.TreePath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil {
|
if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath)
|
return nil, fmt.Errorf("invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
{{template "base/head" .}}
|
{{template "base/head" .}}
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository file editor delete">
|
<div role="main" aria-label="{{.Title}}" class="page-content repository file editor delete">
|
||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container fluid padded">
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
|
<div class="repo-view-container">
|
||||||
|
{{template "repo/view_file_tree" .}}
|
||||||
|
<div class="repo-view-content">
|
||||||
<form class="ui form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
|
<form class="ui form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
{{template "repo/editor/common_top" .}}
|
{{template "repo/editor/common_top" .}}
|
||||||
|
<div class="repo-editor-header">
|
||||||
|
{{/* although the UI isn't good enough, this header is necessary for the "left file tree view" toggle button, this button must exist */}}
|
||||||
|
{{template "repo/view_file_tree_toggle_button" .}}
|
||||||
|
{{/* then, to make the page looks overall good, add the breadcrumb here to make the toggle button can be shown in a text row, but not a single button*/}}
|
||||||
|
<div class="breadcrumb">
|
||||||
|
<a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
|
||||||
|
{{range $i, $v := .TreeNames}}
|
||||||
|
<div class="breadcrumb-divider">/</div>
|
||||||
|
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{template "repo/editor/commit_form" .}}
|
{{template "repo/editor/commit_form" .}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
{{template "base/head" .}}
|
{{template "base/head" .}}
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository file editor edit">
|
<div role="main" aria-label="{{.Title}}" class="page-content repository file editor edit">
|
||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container fluid padded">
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
|
<div class="repo-view-container">
|
||||||
|
{{template "repo/view_file_tree" .}}
|
||||||
|
<div class="repo-view-content">
|
||||||
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"
|
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"
|
||||||
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
|
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
|
||||||
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||||
@@ -10,6 +13,7 @@
|
|||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
{{template "repo/editor/common_top" .}}
|
{{template "repo/editor/common_top" .}}
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
|
{{template "repo/view_file_tree_toggle_button" .}}
|
||||||
{{template "repo/editor/common_breadcrumb" .}}
|
{{template "repo/editor/common_breadcrumb" .}}
|
||||||
</div>
|
</div>
|
||||||
{{if not .NotEditableReason}}
|
{{if not .NotEditableReason}}
|
||||||
@@ -49,5 +53,7 @@
|
|||||||
{{template "repo/editor/commit_form" .}}
|
{{template "repo/editor/commit_form" .}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
{{template "base/head" .}}
|
{{template "base/head" .}}
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository file editor upload">
|
<div role="main" aria-label="{{.Title}}" class="page-content repository file editor upload">
|
||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container fluid padded">
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
|
<div class="repo-view-container">
|
||||||
|
{{template "repo/view_file_tree" .}}
|
||||||
|
<div class="repo-view-content">
|
||||||
<form class="ui comment form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
|
<form class="ui comment form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
{{template "repo/editor/common_top" .}}
|
{{template "repo/editor/common_top" .}}
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
|
{{template "repo/view_file_tree_toggle_button" .}}
|
||||||
{{template "repo/editor/common_breadcrumb" .}}
|
{{template "repo/editor/common_breadcrumb" .}}
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -15,5 +19,7 @@
|
|||||||
{{template "repo/editor/commit_form" .}}
|
{{template "repo/editor/commit_form" .}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
{{template "base/head" .}}
|
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository">
|
|
||||||
{{template "repo/header" .}}
|
|
||||||
<div class="ui container">
|
|
||||||
<div class="tw-flex tw-items-center">
|
|
||||||
<a href="{{$.RepoLink}}">{{.RepoName}}</a>
|
|
||||||
<span class="tw-mx-2">/</span>
|
|
||||||
<div class="ui input tw-flex-1">
|
|
||||||
<input id="repo-file-find-input" type="text" autofocus data-url-data-link="{{.DataLink}}" data-url-tree-link="{{.TreeLink}}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table id="repo-find-file-table" class="ui single line fixed table">
|
|
||||||
<tbody>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div id="repo-find-file-no-result" class="ui row center tw-mt-8 tw-hidden">
|
|
||||||
<h3>{{ctx.Locale.Tr "repo.find_file.no_matching"}}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{template "base/footer" .}}
|
|
||||||
@@ -17,9 +17,7 @@
|
|||||||
{{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}}
|
{{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}}
|
||||||
|
|
||||||
<div class="repo-view-container">
|
<div class="repo-view-container">
|
||||||
<div class="tw-flex tw-flex-col repo-view-file-tree-container not-mobile {{if not .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}" {{if .IsSigned}}data-user-is-signed-in{{end}}>
|
|
||||||
{{template "repo/view_file_tree" .}}
|
{{template "repo/view_file_tree" .}}
|
||||||
</div>
|
|
||||||
<div class="repo-view-content">
|
<div class="repo-view-content">
|
||||||
{{template "repo/view_content" .}}
|
{{template "repo/view_content" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,11 +5,7 @@
|
|||||||
<div class="repo-button-row">
|
<div class="repo-button-row">
|
||||||
<div class="repo-button-row-left">
|
<div class="repo-button-row-left">
|
||||||
{{if not $isTreePathRoot}}
|
{{if not $isTreePathRoot}}
|
||||||
<button class="repo-view-file-tree-toggle-show ui compact basic button icon not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}"
|
{{template "repo/view_file_tree_toggle_button" .}}
|
||||||
data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show"
|
|
||||||
data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
|
|
||||||
{{svg "octicon-sidebar-collapse"}}
|
|
||||||
</button>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{template "repo/branch_dropdown" dict
|
{{template "repo/branch_dropdown" dict
|
||||||
@@ -37,31 +33,6 @@
|
|||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<!-- Show go to file if on home page -->
|
|
||||||
{{if $isTreePathRoot}}
|
|
||||||
<a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if and .RefFullName.IsBranch (not .IsViewFile)}}
|
|
||||||
<button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanEnableEditor}}disabled{{end}}>
|
|
||||||
{{ctx.Locale.Tr "repo.editor.add_file"}}
|
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
|
||||||
<div class="menu">
|
|
||||||
<a class="item" href="{{.RepoLink}}/_new/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
|
|
||||||
{{ctx.Locale.Tr "repo.editor.new_file"}}
|
|
||||||
</a>
|
|
||||||
{{if .RepositoryUploadEnabled}}
|
|
||||||
<a class="item" href="{{.RepoLink}}/_upload/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
|
|
||||||
{{ctx.Locale.Tr "repo.editor.upload_file"}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
<a class="item" href="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
|
|
||||||
{{ctx.Locale.Tr "repo.editor.patch"}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if and $isTreePathRoot .Repository.IsTemplate}}
|
{{if and $isTreePathRoot .Repository.IsTemplate}}
|
||||||
<a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}">
|
<a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}">
|
||||||
{{ctx.Locale.Tr "repo.use_template"}}
|
{{ctx.Locale.Tr "repo.use_template"}}
|
||||||
@@ -86,12 +57,65 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="repo-button-row-right">
|
<div class="repo-button-row-right">
|
||||||
|
<div class="repo-file-search-container"
|
||||||
|
data-global-init="initRepoFileSearch"
|
||||||
|
data-repo-link="{{.RepoLink}}"
|
||||||
|
data-current-ref-name-sub-url="{{.RefTypeNameSubURL}}"
|
||||||
|
data-tree-list-url="{{.RepoLink}}/tree-list/{{.RefTypeNameSubURL}}"
|
||||||
|
data-no-results-text="{{ctx.Locale.Tr "repo.find_file.no_matching"}}"
|
||||||
|
data-placeholder="{{ctx.Locale.Tr "repo.find_file.go_to_file"}}"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{{if .RefFullName.IsBranch}}
|
||||||
|
{{$addFilePath := .TreePath}}
|
||||||
|
{{if .IsViewFile}}
|
||||||
|
{{if gt (len .TreeNames) 1}}
|
||||||
|
{{$addFilePath = StringUtils.Join (slice .TreeNames 0 (Eval (len .TreeNames) "-" 1)) "/"}}
|
||||||
|
{{else}}
|
||||||
|
{{$addFilePath = ""}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
<button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanEnableEditor}}disabled{{end}}>
|
||||||
|
{{ctx.Locale.Tr "repo.editor.add_file"}}
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="menu">
|
||||||
|
<a class="item" href="{{.RepoLink}}/_new/{{.BranchName | PathEscapeSegments}}/{{$addFilePath | PathEscapeSegments}}">
|
||||||
|
{{svg "octicon-file-added" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.editor.new_file"}}
|
||||||
|
</a>
|
||||||
|
{{if .RepositoryUploadEnabled}}
|
||||||
|
<a class="item" href="{{.RepoLink}}/_upload/{{.BranchName | PathEscapeSegments}}/{{$addFilePath | PathEscapeSegments}}">
|
||||||
|
{{svg "octicon-upload" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.editor.upload_file"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
<a class="item" href="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}/{{$addFilePath | PathEscapeSegments}}">
|
||||||
|
{{svg "octicon-diff" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.editor.patch"}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{if and (not .IsViewFile) (not $isTreePathRoot)}}
|
||||||
|
<button class="ui dropdown basic compact jump button tw-px-3" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
|
||||||
|
{{svg "octicon-kebab-horizontal"}}
|
||||||
|
<div class="menu">
|
||||||
|
<a class="item" data-clipboard-text="{{.Repository.Link}}/src/commit/{{.CommitID}}/{{PathEscapeSegments .TreePath}}" data-clipboard-text-type="url">
|
||||||
|
{{svg "octicon-link" 16}}{{ctx.Locale.Tr "repo.file_copy_permalink"}}
|
||||||
|
</a>
|
||||||
|
{{if and (.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) (not .Repository.IsArchived) (not $isTreePathRoot)}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
<a class="item tw-text-danger" href="{{.RepoLink}}/_delete/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
|
||||||
|
{{svg "octicon-trash" 16}}{{ctx.Locale.Tr "repo.editor.delete_this_directory"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
<!-- Only show clone panel in repository home page -->
|
<!-- Only show clone panel in repository home page -->
|
||||||
{{if $isTreePathRoot}}
|
{{if $isTreePathRoot}}
|
||||||
{{template "repo/clone_panel" .}}
|
{{template "repo/clone_panel" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
|
{{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
|
||||||
<a class="ui button" href="{{.RepoLink}}/commits/{{.RefTypeNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
|
<a class="ui compact button" href="{{.RepoLink}}/commits/{{.RefTypeNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
|
||||||
{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
|
{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
<div class="flex-text-block repo-button-row">
|
<div class="repo-view-file-tree-container {{if not .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}">
|
||||||
<button class="ui compact basic icon button"
|
<div class="flex-text-block repo-button-row">
|
||||||
|
<button class="repo-view-file-tree-toggle ui button"
|
||||||
data-global-click="onRepoViewFileTreeToggle" data-toggle-action="hide"
|
data-global-click="onRepoViewFileTreeToggle" data-toggle-action="hide"
|
||||||
data-tooltip-content="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
|
data-tooltip-content="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
|
||||||
{{svg "octicon-sidebar-expand"}}
|
{{svg "octicon-sidebar-expand"}}
|
||||||
</button>
|
</button>
|
||||||
<b>{{ctx.Locale.Tr "files"}}</b>
|
<b>{{ctx.Locale.Tr "files"}}</b>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{/* TODO: Dynamically move components such as refSelector and createPR here */}}
|
{{/* TODO: Dynamically move components such as refSelector and createPR here */}}
|
||||||
<div id="view-file-tree" class="tw-overflow-auto tw-h-full is-loading"
|
<div id="view-file-tree" class="tw-overflow-auto tw-h-full is-loading"
|
||||||
data-repo-link="{{.RepoLink}}"
|
data-repo-link="{{.RepoLink}}"
|
||||||
data-tree-path="{{$.TreePath}}"
|
data-tree-path="{{$.TreePath}}"
|
||||||
data-current-ref-name-sub-url="{{.RefTypeNameSubURL}}"
|
data-current-ref-name-sub-url="{{.RefTypeNameSubURL}}"
|
||||||
></div>
|
></div>
|
||||||
|
</div>
|
||||||
|
|||||||
6
templates/repo/view_file_tree_toggle_button.tmpl
Normal file
6
templates/repo/view_file_tree_toggle_button.tmpl
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<button type="button"
|
||||||
|
class="repo-view-file-tree-toggle ui button not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}"
|
||||||
|
data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}"
|
||||||
|
>
|
||||||
|
{{svg "octicon-sidebar-collapse"}}
|
||||||
|
</button>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="repo-file-cell message loading-icon-2px">
|
<div class="repo-file-cell message commit-summary loading-icon-2px">
|
||||||
{{if $commit}}
|
{{if $commit}}
|
||||||
{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}}
|
{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}}
|
||||||
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.Message $commitLink $.Repository}}
|
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.Message $commitLink $.Repository}}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ func deleteFileInBranch(user *user_model.User, repo *repo_model.Repository, tree
|
|||||||
func createOrReplaceFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) error {
|
func createOrReplaceFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) error {
|
||||||
_, err := deleteFileInBranch(user, repo, treePath, branchName)
|
_, err := deleteFileInBranch(user, repo, treePath, branchName)
|
||||||
|
|
||||||
if err != nil && !files_service.IsErrRepoFileDoesNotExist(err) {
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,7 +13,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
files_service "code.gitea.io/gitea/services/repository/files"
|
files_service "code.gitea.io/gitea/services/repository/files"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getCreateRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions {
|
func getCreateRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions {
|
||||||
@@ -93,55 +94,6 @@ func getUpdateRepoFilesRenameOptions(repo *repo_model.Repository) *files_service
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDeleteRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions {
|
|
||||||
return &files_service.ChangeRepoFilesOptions{
|
|
||||||
Files: []*files_service.ChangeRepoFile{
|
|
||||||
{
|
|
||||||
Operation: "delete",
|
|
||||||
TreePath: "README.md",
|
|
||||||
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
LastCommitID: "",
|
|
||||||
OldBranch: repo.DefaultBranch,
|
|
||||||
NewBranch: repo.DefaultBranch,
|
|
||||||
Message: "Deletes README.md",
|
|
||||||
Author: &files_service.IdentityOptions{
|
|
||||||
GitUserName: "Bob Smith",
|
|
||||||
GitUserEmail: "bob@smith.com",
|
|
||||||
},
|
|
||||||
Committer: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExpectedFileResponseForRepoFilesDelete() *api.FileResponse {
|
|
||||||
// Just returns fields that don't change, i.e. fields with commit SHAs and dates can't be determined
|
|
||||||
return &api.FileResponse{
|
|
||||||
Content: nil,
|
|
||||||
Commit: &api.FileCommitResponse{
|
|
||||||
Author: &api.CommitUser{
|
|
||||||
Identity: api.Identity{
|
|
||||||
Name: "Bob Smith",
|
|
||||||
Email: "bob@smith.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Committer: &api.CommitUser{
|
|
||||||
Identity: api.Identity{
|
|
||||||
Name: "Bob Smith",
|
|
||||||
Email: "bob@smith.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Message: "Deletes README.md\n",
|
|
||||||
},
|
|
||||||
Verification: &api.PayloadCommitVerification{
|
|
||||||
Verified: false,
|
|
||||||
Reason: "gpg.error.not_signed_commit",
|
|
||||||
Signature: "",
|
|
||||||
Payload: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExpectedFileResponseForRepoFilesCreate(commitID string, lastCommit *git.Commit) *api.FileResponse {
|
func getExpectedFileResponseForRepoFilesCreate(commitID string, lastCommit *git.Commit) *api.FileResponse {
|
||||||
treePath := "new/file.txt"
|
treePath := "new/file.txt"
|
||||||
encoding := "base64"
|
encoding := "base64"
|
||||||
@@ -578,12 +530,7 @@ func TestChangeRepoFilesWithoutBranchNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestChangeRepoFilesForDelete(t *testing.T) {
|
func TestChangeRepoFilesForDelete(t *testing.T) {
|
||||||
onGiteaRun(t, testDeleteRepoFiles)
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
}
|
|
||||||
|
|
||||||
func testDeleteRepoFiles(t *testing.T, u *url.URL) {
|
|
||||||
// setup
|
|
||||||
unittest.PrepareTestEnv(t)
|
|
||||||
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
||||||
ctx.SetPathParam("id", "1")
|
ctx.SetPathParam("id", "1")
|
||||||
contexttest.LoadRepo(t, ctx, 1)
|
contexttest.LoadRepo(t, ctx, 1)
|
||||||
@@ -593,60 +540,78 @@ func testDeleteRepoFiles(t *testing.T, u *url.URL) {
|
|||||||
defer ctx.Repo.GitRepo.Close()
|
defer ctx.Repo.GitRepo.Close()
|
||||||
repo := ctx.Repo.Repository
|
repo := ctx.Repo.Repository
|
||||||
doer := ctx.Doer
|
doer := ctx.Doer
|
||||||
opts := getDeleteRepoFilesOptions(repo)
|
|
||||||
|
|
||||||
t.Run("Delete README.md file", func(t *testing.T) {
|
t.Run("Delete README.md by commit", func(t *testing.T) {
|
||||||
|
urlRaw := "/user2/repo1/raw/branch/branch2/README.md"
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusOK)
|
||||||
|
opts := &files_service.ChangeRepoFilesOptions{
|
||||||
|
OldBranch: "branch2",
|
||||||
|
LastCommitID: "985f0301dba5e7b34be866819cd15ad3d8f508ee",
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "delete",
|
||||||
|
TreePath: "README.md",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Message: "test message",
|
||||||
|
}
|
||||||
filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts)
|
filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expectedFileResponse := getExpectedFileResponseForRepoFilesDelete()
|
|
||||||
assert.NotNil(t, filesResponse)
|
assert.NotNil(t, filesResponse)
|
||||||
assert.Nil(t, filesResponse.Files[0])
|
assert.Nil(t, filesResponse.Files[0])
|
||||||
assert.Equal(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message)
|
assert.Equal(t, "test message\n", filesResponse.Commit.Message)
|
||||||
assert.Equal(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity)
|
MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusNotFound)
|
||||||
assert.Equal(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity)
|
|
||||||
assert.Equal(t, expectedFileResponse.Verification, filesResponse.Verification)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Verify README.md has been deleted", func(t *testing.T) {
|
t.Run("Delete README.md with options", func(t *testing.T) {
|
||||||
|
urlRaw := "/user2/repo1/raw/branch/master/README.md"
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusOK)
|
||||||
|
opts := &files_service.ChangeRepoFilesOptions{
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "delete",
|
||||||
|
TreePath: "README.md",
|
||||||
|
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OldBranch: repo.DefaultBranch,
|
||||||
|
NewBranch: repo.DefaultBranch,
|
||||||
|
Message: "Message for deleting README.md",
|
||||||
|
Author: &files_service.IdentityOptions{GitUserName: "Bob Smith", GitUserEmail: "bob@smith.com"},
|
||||||
|
}
|
||||||
filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts)
|
filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts)
|
||||||
assert.Nil(t, filesResponse)
|
require.NoError(t, err)
|
||||||
expectedError := "repository file does not exist [path: " + opts.Files[0].TreePath + "]"
|
require.NotNil(t, filesResponse)
|
||||||
assert.EqualError(t, err, expectedError)
|
assert.Nil(t, filesResponse.Files[0])
|
||||||
|
assert.Equal(t, "Message for deleting README.md\n", filesResponse.Commit.Message)
|
||||||
|
assert.Equal(t, api.Identity{Name: "Bob Smith", Email: "bob@smith.com"}, filesResponse.Commit.Author.Identity)
|
||||||
|
assert.Equal(t, api.Identity{Name: "Bob Smith", Email: "bob@smith.com"}, filesResponse.Commit.Committer.Identity)
|
||||||
|
assert.Equal(t, &api.PayloadCommitVerification{Reason: "gpg.error.not_signed_commit"}, filesResponse.Verification)
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusNotFound)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// Test opts with branch names removed, same results
|
t.Run("Delete directory", func(t *testing.T) {
|
||||||
func TestChangeRepoFilesForDeleteWithoutBranchNames(t *testing.T) {
|
urlRaw := "/user2/repo1/raw/branch/sub-home-md-img-check/docs/README.md"
|
||||||
onGiteaRun(t, testDeleteRepoFilesWithoutBranchNames)
|
MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusOK)
|
||||||
}
|
opts := &files_service.ChangeRepoFilesOptions{
|
||||||
|
OldBranch: "sub-home-md-img-check",
|
||||||
func testDeleteRepoFilesWithoutBranchNames(t *testing.T, u *url.URL) {
|
LastCommitID: "4649299398e4d39a5c09eb4f534df6f1e1eb87cc",
|
||||||
// setup
|
Files: []*files_service.ChangeRepoFile{
|
||||||
unittest.PrepareTestEnv(t)
|
{
|
||||||
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
Operation: "delete",
|
||||||
ctx.SetPathParam("id", "1")
|
TreePath: "docs",
|
||||||
contexttest.LoadRepo(t, ctx, 1)
|
DeleteRecursively: true,
|
||||||
contexttest.LoadRepoCommit(t, ctx)
|
},
|
||||||
contexttest.LoadUser(t, ctx, 2)
|
},
|
||||||
contexttest.LoadGitRepo(t, ctx)
|
Message: "test message",
|
||||||
defer ctx.Repo.GitRepo.Close()
|
}
|
||||||
|
|
||||||
repo := ctx.Repo.Repository
|
|
||||||
doer := ctx.Doer
|
|
||||||
opts := getDeleteRepoFilesOptions(repo)
|
|
||||||
opts.OldBranch = ""
|
|
||||||
opts.NewBranch = ""
|
|
||||||
|
|
||||||
t.Run("Delete README.md without Branch Name", func(t *testing.T) {
|
|
||||||
filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts)
|
filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expectedFileResponse := getExpectedFileResponseForRepoFilesDelete()
|
|
||||||
assert.NotNil(t, filesResponse)
|
assert.NotNil(t, filesResponse)
|
||||||
assert.Nil(t, filesResponse.Files[0])
|
assert.Nil(t, filesResponse.Files[0])
|
||||||
assert.Equal(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message)
|
assert.Equal(t, "test message\n", filesResponse.Commit.Message)
|
||||||
assert.Equal(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity)
|
MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusNotFound)
|
||||||
assert.Equal(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity)
|
})
|
||||||
assert.Equal(t, expectedFileResponse.Verification, filesResponse.Verification)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,3 @@
|
|||||||
.repository.file.editor .tab[data-tab="write"] {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository.file.editor .tab[data-tab="write"] .editor-toolbar {
|
|
||||||
border: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository.file.editor .tab[data-tab="write"] .CodeMirror {
|
|
||||||
border-left: 0;
|
|
||||||
border-right: 0;
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-editor-header {
|
|
||||||
display: flex;
|
|
||||||
margin: 1rem 0;
|
|
||||||
padding: 3px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar {
|
.editor-toolbar {
|
||||||
border-color: var(--color-secondary);
|
border-color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
animation: isloadingspin 1000ms infinite linear;
|
animation: isloadingspin 1000ms infinite linear;
|
||||||
border-width: 4px;
|
border-width: 3px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: var(--color-secondary) var(--color-secondary) var(--color-secondary-dark-8) var(--color-secondary-dark-8);
|
border-color: var(--color-secondary) var(--color-secondary) var(--color-secondary-dark-8) var(--color-secondary-dark-8);
|
||||||
border-radius: var(--border-radius-full);
|
border-radius: var(--border-radius-full);
|
||||||
|
|||||||
@@ -150,63 +150,68 @@ td .commit-summary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .header .icon {
|
.non-diff-file-content .header .icon {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .header .small.icon {
|
.non-diff-file-content .header .small.icon {
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .header .tiny.icon {
|
.non-diff-file-content .header .tiny.icon {
|
||||||
font-size: 0.5em;
|
font-size: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon {
|
.non-diff-file-content .header .file-actions .btn-octicon {
|
||||||
line-height: var(--line-height-default);
|
line-height: var(--line-height-default);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon:hover {
|
.non-diff-file-content .header .file-actions .btn-octicon:hover {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon-danger:hover {
|
.non-diff-file-content .header .file-actions .btn-octicon-danger:hover {
|
||||||
color: var(--color-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon.disabled {
|
.non-diff-file-content .header .file-actions .btn-octicon.disabled {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
opacity: var(--opacity-disabled);
|
opacity: var(--opacity-disabled);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .plain-text {
|
.non-diff-file-content .plain-text {
|
||||||
padding: 1em 2em;
|
padding: 1em 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .plain-text pre {
|
.non-diff-file-content .plain-text pre {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .csv {
|
.non-diff-file-content .csv {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content pre {
|
.non-diff-file-content pre {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .asciicast {
|
.non-diff-file-content .asciicast {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-editor-header {
|
.repo-editor-header {
|
||||||
|
display: flex;
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 3px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-editor-header input {
|
.repo-editor-header input {
|
||||||
@@ -216,17 +221,13 @@ td .commit-summary {
|
|||||||
margin-right: 5px !important;
|
margin-right: 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.editor .tabular.menu .svg {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository.file.editor .commit-form-wrapper {
|
.repository.file.editor .commit-form-wrapper {
|
||||||
padding-left: 48px;
|
padding-left: 58px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.editor .commit-form-wrapper .commit-avatar {
|
.repository.file.editor .commit-form-wrapper .commit-avatar {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: -48px;
|
margin-left: -58px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.editor .commit-form-wrapper .commit-form {
|
.repository.file.editor .commit-form-wrapper .commit-form {
|
||||||
@@ -1409,12 +1410,25 @@ td .commit-summary {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-button-row .ui.button {
|
.repo-button-row .ui.button,
|
||||||
|
.repo-view-container .ui.button.repo-view-file-tree-toggle {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-view-container .ui.button.repo-view-file-tree-toggle {
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-button-row .repo-file-search-container .ui.input {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-button-row .ui.dropdown > .menu {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
tbody.commit-list {
|
tbody.commit-list {
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
@@ -1483,6 +1497,12 @@ tbody.commit-list {
|
|||||||
line-height: initial;
|
line-height: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.commit-body a.commit code,
|
||||||
|
.commit-summary a.commit code {
|
||||||
|
/* these links are generated by the render: <a class="commit" href="...">...</a> */
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.git-notes.top {
|
.git-notes.top {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,9 @@
|
|||||||
gap: var(--page-spacing);
|
gap: var(--page-spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-view-container .repo-view-file-tree-container {
|
.repo-view-file-tree-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
flex: 0 0 15%;
|
flex: 0 0 15%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
@@ -65,6 +67,12 @@
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.repo-view-file-tree-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.repo-view-content {
|
.repo-view-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ gitea-theme-meta-info {
|
|||||||
--color-highlight-fg: #87651e;
|
--color-highlight-fg: #87651e;
|
||||||
--color-highlight-bg: #352c1c;
|
--color-highlight-bg: #352c1c;
|
||||||
--color-overlay-backdrop: #080808c0;
|
--color-overlay-backdrop: #080808c0;
|
||||||
|
--color-danger: var(--color-red);
|
||||||
accent-color: var(--color-accent);
|
accent-color: var(--color-accent);
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ gitea-theme-meta-info {
|
|||||||
--color-highlight-fg: #eed200;
|
--color-highlight-fg: #eed200;
|
||||||
--color-highlight-bg: #fffbdd;
|
--color-highlight-bg: #fffbdd;
|
||||||
--color-overlay-backdrop: #080808c0;
|
--color-overlay-backdrop: #080808c0;
|
||||||
|
--color-danger: var(--color-red);
|
||||||
accent-color: var(--color-accent);
|
accent-color: var(--color-accent);
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|||||||
230
web_src/js/components/RepoFileSearch.vue
Normal file
230
web_src/js/components/RepoFileSearch.vue
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, watch, nextTick, useTemplateRef, onMounted, onUnmounted } from 'vue';
|
||||||
|
import {generateElemId} from '../utils/dom.ts';
|
||||||
|
import { GET } from '../modules/fetch.ts';
|
||||||
|
import { filterRepoFilesWeighted } from '../features/repo-findfile.ts';
|
||||||
|
import { pathEscapeSegments } from '../utils/url.ts';
|
||||||
|
import { SvgIcon } from '../svg.ts';
|
||||||
|
import {throttle} from 'throttle-debounce';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
repoLink: { type: String, required: true },
|
||||||
|
currentRefNameSubURL: { type: String, required: true },
|
||||||
|
treeListUrl: { type: String, required: true },
|
||||||
|
noResultsText: { type: String, required: true },
|
||||||
|
placeholder: { type: String, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const refElemInput = useTemplateRef<HTMLInputElement>('searchInput');
|
||||||
|
const refElemPopup = useTemplateRef<HTMLElement>('searchPopup');
|
||||||
|
|
||||||
|
const searchQuery = ref('');
|
||||||
|
const allFiles = ref<string[]>([]);
|
||||||
|
const selectedIndex = ref(0);
|
||||||
|
const isLoadingFileList = ref(false);
|
||||||
|
const hasLoadedFileList = ref(false);
|
||||||
|
|
||||||
|
const showPopup = computed(() => searchQuery.value.length > 0);
|
||||||
|
|
||||||
|
const filteredFiles = computed(() => {
|
||||||
|
if (!searchQuery.value) return [];
|
||||||
|
return filterRepoFilesWeighted(allFiles.value, searchQuery.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const applySearchQuery = throttle(300, () => {
|
||||||
|
searchQuery.value = refElemInput.value.value;
|
||||||
|
selectedIndex.value = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSearchInput = () => {
|
||||||
|
loadFileListForSearch();
|
||||||
|
applySearchQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
clearSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!searchQuery.value || filteredFiles.value.length === 0) return;
|
||||||
|
|
||||||
|
const handleSelectedItem = (idx: number) => {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex.value = idx;
|
||||||
|
const el = refElemPopup.value.querySelector(`.file-search-results > :nth-child(${idx+1} of .item)`);
|
||||||
|
el?.scrollIntoView({ block: 'nearest', behavior: 'instant' });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
handleSelectedItem(Math.min(selectedIndex.value + 1, filteredFiles.value.length - 1));
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
handleSelectedItem(Math.max(selectedIndex.value - 1, 0))
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
const selectedFile = filteredFiles.value[selectedIndex.value];
|
||||||
|
if (selectedFile) {
|
||||||
|
handleSearchResultClick(selectedFile.matchResult.join(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
searchQuery.value = '';
|
||||||
|
refElemInput.value.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (!searchQuery.value) return;
|
||||||
|
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const clickInside = refElemInput.value.contains(target) || refElemPopup.value.contains(target);
|
||||||
|
if (!clickInside) clearSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFileListForSearch = async () => {
|
||||||
|
if (hasLoadedFileList.value || isLoadingFileList.value) return;
|
||||||
|
|
||||||
|
isLoadingFileList.value = true;
|
||||||
|
try {
|
||||||
|
const response = await GET(props.treeListUrl);
|
||||||
|
allFiles.value = await response.json();
|
||||||
|
hasLoadedFileList.value = true;
|
||||||
|
} finally {
|
||||||
|
isLoadingFileList.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleSearchResultClick(filePath: string) {
|
||||||
|
clearSearch();
|
||||||
|
window.location.href = `${props.repoLink}/src/${pathEscapeSegments(props.currentRefNameSubURL)}/${pathEscapeSegments(filePath)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
if (!showPopup.value) return;
|
||||||
|
|
||||||
|
const rectInput = refElemInput.value.getBoundingClientRect();
|
||||||
|
const rectPopup = refElemPopup.value.getBoundingClientRect();
|
||||||
|
const docElem = document.documentElement;
|
||||||
|
const style = refElemPopup.value.style;
|
||||||
|
style.top = `${docElem.scrollTop + rectInput.bottom + 4}px`;
|
||||||
|
if (rectInput.x + rectPopup.width < docElem.clientWidth) {
|
||||||
|
// enough space to align left with the input
|
||||||
|
style.left = `${docElem.scrollLeft + rectInput.x}px`;
|
||||||
|
} else {
|
||||||
|
// no enough space, align right from the viewport right edge minus page margin
|
||||||
|
const leftPos = docElem.scrollLeft + docElem.getBoundingClientRect().width - rectPopup.width;
|
||||||
|
style.left = `calc(${leftPos}px - var(--page-margin-x))`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const searchPopupId = generateElemId('file-search-popup-');
|
||||||
|
refElemPopup.value.setAttribute('id', searchPopupId);
|
||||||
|
refElemInput.value.setAttribute('aria-controls', searchPopupId);
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
window.addEventListener('resize', updatePosition);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
window.removeEventListener('resize', updatePosition);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Position search results below the input
|
||||||
|
watch([searchQuery, filteredFiles], async () => {
|
||||||
|
if (searchQuery.value) {
|
||||||
|
await nextTick();
|
||||||
|
updatePosition();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="ui small input">
|
||||||
|
<input
|
||||||
|
ref="searchInput" :placeholder="placeholder" autocomplete="off"
|
||||||
|
role="combobox" aria-autocomplete="list" :aria-expanded="searchQuery ? 'true' : 'false'"
|
||||||
|
@input="handleSearchInput" @keydown="handleKeyDown"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-show="showPopup" ref="searchPopup" class="file-search-popup">
|
||||||
|
<!-- always create the popup by v-show above to avoid null ref, only create the popup content if the popup should be displayed to save memory -->
|
||||||
|
<template v-if="showPopup">
|
||||||
|
<div v-if="filteredFiles.length" role="listbox" class="file-search-results flex-items-block">
|
||||||
|
<div
|
||||||
|
v-for="(result, idx) in filteredFiles" :key="result.matchResult.join('')"
|
||||||
|
:class="['item', { 'selected': idx === selectedIndex }]"
|
||||||
|
role="option" :aria-selected="idx === selectedIndex" @click="handleSearchResultClick(result.matchResult.join(''))"
|
||||||
|
@mouseenter="selectedIndex = idx" :title="result.matchResult.join('')"
|
||||||
|
>
|
||||||
|
<SvgIcon name="octicon-file" class="file-icon"/>
|
||||||
|
<span class="full-path">
|
||||||
|
<span v-for="(part, index) in result.matchResult" :key="index">{{ part }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isLoadingFileList">
|
||||||
|
<div class="is-loading"/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="tw-p-4">
|
||||||
|
{{ props.noResultsText }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.file-search-popup {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--color-box-body);
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
width: max-content;
|
||||||
|
max-height: min(calc(100vw - 20px), 300px);
|
||||||
|
max-width: min(calc(100vw - 40px), 600px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-search-popup .is-loading {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-search-results .item {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-search-results .item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-search-results .item:hover,
|
||||||
|
.file-search-results .item.selected {
|
||||||
|
background-color: var(--color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-search-results .item .file-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-search-results .item .full-path {
|
||||||
|
flex: 1;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-search-results .item .full-path :nth-child(even) {
|
||||||
|
color: var(--color-red);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
import {svg} from '../svg.ts';
|
import {createApp} from 'vue';
|
||||||
import {toggleElem} from '../utils/dom.ts';
|
import RepoFileSearch from '../components/RepoFileSearch.vue';
|
||||||
import {pathEscapeSegments} from '../utils/url.ts';
|
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||||
import {GET} from '../modules/fetch.ts';
|
|
||||||
|
|
||||||
const threshold = 50;
|
const threshold = 50;
|
||||||
let files: Array<string> = [];
|
|
||||||
let repoFindFileInput: HTMLInputElement;
|
|
||||||
let repoFindFileTableBody: HTMLElement;
|
|
||||||
let repoFindFileNoResult: HTMLElement;
|
|
||||||
|
|
||||||
// return the case-insensitive sub-match result as an array: [unmatched, matched, unmatched, matched, ...]
|
// return the case-insensitive sub-match result as an array: [unmatched, matched, unmatched, matched, ...]
|
||||||
// res[even] is unmatched, res[odd] is matched, see unit tests for examples
|
// res[even] is unmatched, res[odd] is matched, see unit tests for examples
|
||||||
@@ -73,48 +68,14 @@ export function filterRepoFilesWeighted(files: Array<string>, filter: string) {
|
|||||||
return filterResult;
|
return filterResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterRepoFiles(filter: string) {
|
export function initRepoFileSearch() {
|
||||||
const treeLink = repoFindFileInput.getAttribute('data-url-tree-link');
|
registerGlobalInitFunc('initRepoFileSearch', (el) => {
|
||||||
repoFindFileTableBody.innerHTML = '';
|
createApp(RepoFileSearch, {
|
||||||
|
repoLink: el.getAttribute('data-repo-link'),
|
||||||
const filterResult = filterRepoFilesWeighted(files, filter);
|
currentRefNameSubURL: el.getAttribute('data-current-ref-name-sub-url'),
|
||||||
|
treeListUrl: el.getAttribute('data-tree-list-url'),
|
||||||
toggleElem(repoFindFileNoResult, !filterResult.length);
|
noResultsText: el.getAttribute('data-no-results-text'),
|
||||||
for (const r of filterResult) {
|
placeholder: el.getAttribute('data-placeholder'),
|
||||||
const row = document.createElement('tr');
|
}).mount(el);
|
||||||
const cell = document.createElement('td');
|
});
|
||||||
const a = document.createElement('a');
|
|
||||||
a.setAttribute('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`);
|
|
||||||
a.innerHTML = svg('octicon-file', 16, 'tw-mr-2');
|
|
||||||
row.append(cell);
|
|
||||||
cell.append(a);
|
|
||||||
for (const [index, part] of r.matchResult.entries()) {
|
|
||||||
const span = document.createElement('span');
|
|
||||||
// safely escape by using textContent
|
|
||||||
span.textContent = part;
|
|
||||||
span.title = span.textContent;
|
|
||||||
// if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz']
|
|
||||||
// the matchResult[odd] is matched and highlighted to red.
|
|
||||||
if (index % 2 === 1) span.classList.add('ui', 'text', 'red');
|
|
||||||
a.append(span);
|
|
||||||
}
|
|
||||||
repoFindFileTableBody.append(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRepoFiles() {
|
|
||||||
const response = await GET(repoFindFileInput.getAttribute('data-url-data-link'));
|
|
||||||
files = await response.json();
|
|
||||||
filterRepoFiles(repoFindFileInput.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initFindFileInRepo() {
|
|
||||||
repoFindFileInput = document.querySelector('#repo-file-find-input');
|
|
||||||
if (!repoFindFileInput) return;
|
|
||||||
|
|
||||||
repoFindFileTableBody = document.querySelector('#repo-find-file-table tbody');
|
|
||||||
repoFindFileNoResult = document.querySelector('#repo-find-file-no-result');
|
|
||||||
repoFindFileInput.addEventListener('input', () => filterRepoFiles(repoFindFileInput.value));
|
|
||||||
|
|
||||||
loadRepoFiles();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import {registerGlobalEventFunc} from '../modules/observer.ts';
|
|||||||
|
|
||||||
const {appSubUrl} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
|
|
||||||
|
function isUserSignedIn() {
|
||||||
|
return Boolean(document.querySelector('#navbar .user-menu'));
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleSidebar(btn: HTMLElement) {
|
async function toggleSidebar(btn: HTMLElement) {
|
||||||
const elToggleShow = document.querySelector('.repo-view-file-tree-toggle-show');
|
const elToggleShow = document.querySelector('.repo-view-file-tree-toggle[data-toggle-action="show"]');
|
||||||
const elFileTreeContainer = document.querySelector('.repo-view-file-tree-container');
|
const elFileTreeContainer = document.querySelector('.repo-view-file-tree-container');
|
||||||
const shouldShow = btn.getAttribute('data-toggle-action') === 'show';
|
const shouldShow = btn.getAttribute('data-toggle-action') === 'show';
|
||||||
toggleElem(elFileTreeContainer, shouldShow);
|
toggleElem(elFileTreeContainer, shouldShow);
|
||||||
@@ -15,7 +19,7 @@ async function toggleSidebar(btn: HTMLElement) {
|
|||||||
|
|
||||||
// FIXME: need to remove "full height" style from parent element
|
// FIXME: need to remove "full height" style from parent element
|
||||||
|
|
||||||
if (!elFileTreeContainer.hasAttribute('data-user-is-signed-in')) return;
|
if (!isUserSignedIn()) return;
|
||||||
await POST(`${appSubUrl}/user/settings/update_preferences`, {
|
await POST(`${appSubUrl}/user/settings/update_preferences`, {
|
||||||
data: {codeViewShowFileTree: shouldShow},
|
data: {codeViewShowFileTree: shouldShow},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {initMarkupAnchors} from './markup/anchors.ts';
|
|||||||
import {initNotificationCount} from './features/notification.ts';
|
import {initNotificationCount} from './features/notification.ts';
|
||||||
import {initRepoIssueContentHistory} from './features/repo-issue-content.ts';
|
import {initRepoIssueContentHistory} from './features/repo-issue-content.ts';
|
||||||
import {initStopwatch} from './features/stopwatch.ts';
|
import {initStopwatch} from './features/stopwatch.ts';
|
||||||
import {initFindFileInRepo} from './features/repo-findfile.ts';
|
import {initRepoFileSearch} from './features/repo-findfile.ts';
|
||||||
import {initMarkupContent} from './markup/content.ts';
|
import {initMarkupContent} from './markup/content.ts';
|
||||||
import {initRepoFileView} from './features/file-view.ts';
|
import {initRepoFileView} from './features/file-view.ts';
|
||||||
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
|
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
|
||||||
@@ -101,7 +101,7 @@ const initPerformanceTracer = callInitFunctions([
|
|||||||
initSshKeyFormParser,
|
initSshKeyFormParser,
|
||||||
initStopwatch,
|
initStopwatch,
|
||||||
initTableSort,
|
initTableSort,
|
||||||
initFindFileInRepo,
|
initRepoFileSearch,
|
||||||
initCopyContent,
|
initCopyContent,
|
||||||
|
|
||||||
initAdminCommon,
|
initAdminCommon,
|
||||||
|
|||||||
Reference in New Issue
Block a user