diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b5b90b31a53cb..6712250924361 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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.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_directory = Delete Directory 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.directory_delete_success = Directory "%s" has been deleted. +editor.delete_directory = Delete directory '%s' 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.or = or diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index ec34d54d2248b..27a0827a10f20 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -610,10 +610,6 @@ func handleChangeRepoFilesError(ctx *context.APIContext, err error) { ctx.APIError(http.StatusUnprocessableEntity, err) return } - if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { - ctx.APIError(http.StatusNotFound, err) - return - } if errors.Is(err, util.ErrNotExist) { ctx.APIError(http.StatusNotFound, err) return diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index e304633f95238..0eebff6aa88b8 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -10,7 +10,6 @@ import ( "net/url" "path" "strconv" - "strings" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -42,8 +41,8 @@ type blameRow struct { // RefBlame render blame page func RefBlame(ctx *context.Context) { - ctx.Data["PageIsViewCode"] = true ctx.Data["IsBlame"] = true + prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL()) // Get current entry user currently looking at. if ctx.Repo.TreePath == "" { @@ -56,17 +55,6 @@ func RefBlame(ctx *context.Context) { 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() fileSize := blob.Size() ctx.Data["FileSize"] = fileSize diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 8c630cb35f0a6..983249a6d247e 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -41,7 +41,12 @@ const ( 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) if cleanedTreePath != ctx.Repo.TreePath { 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 prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath)) - prepareEditorCommitFormOptions(ctx, editorAction) + prepareEditorPage(ctx, editorAction) if ctx.Written() { return } @@ -376,15 +381,16 @@ func EditFilePost(ctx *context.Context) { // DeleteFile render delete file page func DeleteFile(ctx *context.Context) { - prepareEditorCommitFormOptions(ctx, "_delete") + prepareEditorPage(ctx, "_delete") if ctx.Written() { return } ctx.Data["PageIsDelete"] = true + prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath) ctx.HTML(http.StatusOK, tplDeleteFile) } -// DeleteFilePost response for deleting file +// DeleteFilePost response for deleting file or directory func DeleteFilePost(ctx *context.Context) { parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx) if ctx.Written() { @@ -392,17 +398,37 @@ func DeleteFilePost(ctx *context.Context) { } 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, OldBranch: parsed.OldBranchName, NewBranch: parsed.NewBranchName, Files: []*files_service.ChangeRepoFile{ { - Operation: "delete", - TreePath: treePath, + Operation: "delete", + TreePath: treePath, + DeleteRecursively: true, }, }, - Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)), + Message: commitMessage, Signoff: parsed.form.Signoff, Author: parsed.GitCommitter, Committer: parsed.GitCommitter, @@ -412,7 +438,11 @@ func DeleteFilePost(ctx *context.Context) { return } - ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) + 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)) + } redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath) redirectForCommitChoice(ctx, parsed, redirectTreePath) } @@ -420,7 +450,7 @@ func DeleteFilePost(ctx *context.Context) { func UploadFile(ctx *context.Context) { ctx.Data["PageIsUpload"] = true prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath) - opts := prepareEditorCommitFormOptions(ctx, "_upload") + opts := prepareEditorPage(ctx, "_upload") if ctx.Written() { return } diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go index aad7b4129c795..357c6f3a21aa2 100644 --- a/routers/web/repo/editor_apply_patch.go +++ b/routers/web/repo/editor_apply_patch.go @@ -14,7 +14,7 @@ import ( ) func NewDiffPatch(ctx *context.Context) { - prepareEditorCommitFormOptions(ctx, "_diffpatch") + prepareEditorPage(ctx, "_diffpatch") if ctx.Written() { return } diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go index 099814a9fa9bb..32e3c58e8748e 100644 --- a/routers/web/repo/editor_cherry_pick.go +++ b/routers/web/repo/editor_cherry_pick.go @@ -16,7 +16,7 @@ import ( ) func CherryPick(ctx *context.Context) { - prepareEditorCommitFormOptions(ctx, "_cherrypick") + prepareEditorPage(ctx, "_cherrypick") if ctx.Written() { return } diff --git a/routers/web/repo/find.go b/routers/web/repo/find.go deleted file mode 100644 index 3a3a7610e772b..0000000000000 --- a/routers/web/repo/find.go +++ /dev/null @@ -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) -} diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 09ac33cff43f0..8e85cc327858f 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -245,27 +245,17 @@ func LastCommit(ctx *context.Context) { 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) if ctx.Written() { 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) } @@ -289,7 +279,9 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri 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. 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) 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 prepareDirectoryFileIcons(ctx, files) for _, f := range files { @@ -334,16 +341,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri if !loadLatestCommitData(ctx, latestCommit) { 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 } diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 17043055e5ff9..00d30bedef5ed 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -362,6 +362,32 @@ func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) b 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 func Home(ctx *context.Context) { if handleRepoHomeFeed(ctx) { @@ -383,8 +409,7 @@ func Home(ctx *context.Context) { title += ": " + ctx.Repo.Repository.Description } ctx.Data["Title"] = title - ctx.Data["PageIsViewCode"] = true - ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show New File / Upload File buttons + prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL()) if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { // empty or broken repositories need to be handled differently @@ -405,26 +430,6 @@ func Home(ctx *context.Context) { 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 isTreePathRoot := ctx.Repo.TreePath == "" @@ -455,7 +460,7 @@ func Home(ctx *context.Context) { if isViewHomeOnlyContent(ctx) { ctx.HTML(http.StatusOK, tplRepoViewContent) - } else if len(treeNames) != 0 { + } else if ctx.Repo.TreePath != "" { ctx.HTML(http.StatusOK, tplRepoView) } else { ctx.HTML(http.StatusOK, tplRepoHome) diff --git a/routers/web/web.go b/routers/web/web.go index dd1b391c68983..89a570dce0773 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -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.Group("/{username}/{reponame}", func() { - m.Get("/find/*", repo.FindFiles) m.Group("/tree-list", func() { m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeList) m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList) diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index feb4811bb02ac..731f23855d28a 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -135,6 +135,14 @@ func (t *TemporaryUploadRepository) LsFiles(ctx context.Context, filenames ...st 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 func (t *TemporaryUploadRepository) RemoveFilesFromIndex(ctx context.Context, filenames ...string) error { objFmt, err := t.gitRepo.GetObjectFormat() diff --git a/services/repository/files/update.go b/services/repository/files/update.go index b07055d57aaa6..4830f711fc284 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -46,7 +46,10 @@ type ChangeRepoFile struct { FromTreePath string ContentReader io.ReadSeeker 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 @@ -69,26 +72,6 @@ type RepoFileOptions struct { 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 { io.ReadSeeker 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 { // Get the commit of the original branch commit, err := t.GetBranchCommit(opts.OldBranch) @@ -272,8 +237,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use addedLfsPointers = append(addedLfsPointers, *addedLfsPointer) } case "delete": - if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil { - return nil, err + 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 { + return nil, err + } } default: return nil, fmt.Errorf("invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath) diff --git a/templates/repo/editor/delete.tmpl b/templates/repo/editor/delete.tmpl index bf6143f1cb91a..70769326a7c28 100644 --- a/templates/repo/editor/delete.tmpl +++ b/templates/repo/editor/delete.tmpl @@ -1,13 +1,30 @@ {{template "base/head" .}}
{{template "repo/header" .}} -
+
{{template "base/alert" .}} -
- {{.CsrfTokenHtml}} - {{template "repo/editor/common_top" .}} - {{template "repo/editor/commit_form" .}} -
+
+ {{template "repo/view_file_tree" .}} +
+
+ {{.CsrfTokenHtml}} + {{template "repo/editor/common_top" .}} +
+ {{/* 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*/}} + +
+ {{template "repo/editor/commit_form" .}} +
+
+
{{template "base/footer" .}} diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index 0911d02e1f423..e6b9c557700ab 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -1,53 +1,59 @@ {{template "base/head" .}}
{{template "repo/header" .}} -
+
{{template "base/alert" .}} -
+ {{template "repo/view_file_tree" .}} +
+ - {{.CsrfTokenHtml}} - {{template "repo/editor/common_top" .}} -
- {{template "repo/editor/common_breadcrumb" .}} -
- {{if not .NotEditableReason}} -
-
- + > + {{.CsrfTokenHtml}} + {{template "repo/editor/common_top" .}} +
+ {{template "repo/view_file_tree_toggle_button" .}} + {{template "repo/editor/common_breadcrumb" .}}
-
-
- -
-
-
- {{ctx.Locale.Tr "loading"}} + {{if not .NotEditableReason}} + -
-
+ {{else}} +
+
+

{{.NotEditableReason}}

+

{{ctx.Locale.Tr "repo.editor.file_not_editable_hint"}}

+
-
-
- {{else}} -
-
-

{{.NotEditableReason}}

-

{{ctx.Locale.Tr "repo.editor.file_not_editable_hint"}}

-
-
- {{end}} - {{template "repo/editor/commit_form" .}} - + {{end}} + {{template "repo/editor/commit_form" .}} + +
+
{{template "base/footer" .}} diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl index 3e36c77b3b924..847d6df88d757 100644 --- a/templates/repo/editor/upload.tmpl +++ b/templates/repo/editor/upload.tmpl @@ -1,19 +1,25 @@ {{template "base/head" .}}
{{template "repo/header" .}} -
+
{{template "base/alert" .}} -
- {{.CsrfTokenHtml}} - {{template "repo/editor/common_top" .}} -
- {{template "repo/editor/common_breadcrumb" .}} +
+ {{template "repo/view_file_tree" .}} +
+ + {{.CsrfTokenHtml}} + {{template "repo/editor/common_top" .}} +
+ {{template "repo/view_file_tree_toggle_button" .}} + {{template "repo/editor/common_breadcrumb" .}} +
+
+ {{template "repo/upload" .}} +
+ {{template "repo/editor/commit_form" .}} +
-
- {{template "repo/upload" .}} -
- {{template "repo/editor/commit_form" .}} - +
{{template "base/footer" .}} diff --git a/templates/repo/find/files.tmpl b/templates/repo/find/files.tmpl deleted file mode 100644 index ce242796bec80..0000000000000 --- a/templates/repo/find/files.tmpl +++ /dev/null @@ -1,21 +0,0 @@ -{{template "base/head" .}} -
- {{template "repo/header" .}} -
-
- {{.RepoName}} - / -
- -
-
- - - -
-
-

{{ctx.Locale.Tr "repo.find_file.no_matching"}}

-
-
-
-{{template "base/footer" .}} diff --git a/templates/repo/view.tmpl b/templates/repo/view.tmpl index f99fe2f57ab37..99f2a7da7ee49 100644 --- a/templates/repo/view.tmpl +++ b/templates/repo/view.tmpl @@ -17,9 +17,7 @@ {{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}}
-
- {{template "repo/view_file_tree" .}} -
+ {{template "repo/view_file_tree" .}}
{{template "repo/view_content" .}}
diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 66e4fffcb9b19..b31648fbbe7b4 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -5,11 +5,7 @@
{{if not $isTreePathRoot}} - + {{template "repo/view_file_tree_toggle_button" .}} {{end}} {{template "repo/branch_dropdown" dict @@ -37,31 +33,6 @@ {{end}} - - {{if $isTreePathRoot}} - {{ctx.Locale.Tr "repo.find_file.go_to_file"}} - {{end}} - - {{if and .RefFullName.IsBranch (not .IsViewFile)}} - - {{end}} - {{if and $isTreePathRoot .Repository.IsTemplate}} {{ctx.Locale.Tr "repo.use_template"}} @@ -86,12 +57,65 @@
+
+ + {{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}} +
+ + {{if and (not .IsViewFile) (not $isTreePathRoot)}} + + {{end}} + {{end}} {{if $isTreePathRoot}} {{template "repo/clone_panel" .}} {{end}} {{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}} - + {{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}} {{end}} diff --git a/templates/repo/view_file_tree.tmpl b/templates/repo/view_file_tree.tmpl index 8aed05f346940..f79fcc22aa34b 100644 --- a/templates/repo/view_file_tree.tmpl +++ b/templates/repo/view_file_tree.tmpl @@ -1,15 +1,17 @@ -
- - {{ctx.Locale.Tr "files"}} -
+
+
+ + {{ctx.Locale.Tr "files"}} +
-{{/* TODO: Dynamically move components such as refSelector and createPR here */}} -
+ {{/* TODO: Dynamically move components such as refSelector and createPR here */}} +
+
diff --git a/templates/repo/view_file_tree_toggle_button.tmpl b/templates/repo/view_file_tree_toggle_button.tmpl new file mode 100644 index 0000000000000..3d6ea928edd06 --- /dev/null +++ b/templates/repo/view_file_tree_toggle_button.tmpl @@ -0,0 +1,6 @@ + diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 145494aa1a974..61443ac465220 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -47,7 +47,7 @@ {{end}} {{end}}
-
+
{{if $commit}} {{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}} {{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.Message $commitLink $.Repository}} diff --git a/tests/integration/api_repo_file_helpers.go b/tests/integration/api_repo_file_helpers.go index f8d6fc803f192..9a4c4486646d2 100644 --- a/tests/integration/api_repo_file_helpers.go +++ b/tests/integration/api_repo_file_helpers.go @@ -5,6 +5,7 @@ package integration import ( "context" + "errors" "strings" "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 { _, err := deleteFileInBranch(user, repo, treePath, branchName) - if err != nil && !files_service.IsErrRepoFileDoesNotExist(err) { + if err != nil && !errors.Is(err, util.ErrNotExist) { return err } diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index 6821f8bf61175..6fd42401c5259 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -5,6 +5,7 @@ package integration import ( "fmt" + "net/http" "net/url" "path" "strings" @@ -12,7 +13,6 @@ import ( "time" 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/gitrepo" "code.gitea.io/gitea/modules/setting" @@ -22,6 +22,7 @@ import ( files_service "code.gitea.io/gitea/services/repository/files" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) 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 { treePath := "new/file.txt" encoding := "base64" @@ -578,75 +530,88 @@ func TestChangeRepoFilesWithoutBranchNames(t *testing.T) { } func TestChangeRepoFilesForDelete(t *testing.T) { - onGiteaRun(t, testDeleteRepoFiles) -} - -func testDeleteRepoFiles(t *testing.T, u *url.URL) { - // setup - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - repo := ctx.Repo.Repository - doer := ctx.Doer - opts := getDeleteRepoFilesOptions(repo) - - t.Run("Delete README.md file", func(t *testing.T) { - filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts) - assert.NoError(t, err) - expectedFileResponse := getExpectedFileResponseForRepoFilesDelete() - assert.NotNil(t, filesResponse) - assert.Nil(t, filesResponse.Files[0]) - assert.Equal(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message) - assert.Equal(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity) - assert.Equal(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity) - assert.Equal(t, expectedFileResponse.Verification, filesResponse.Verification) - }) + onGiteaRun(t, func(t *testing.T, u *url.URL) { + ctx, _ := contexttest.MockContext(t, "user2/repo1") + ctx.SetPathParam("id", "1") + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + contexttest.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository + doer := ctx.Doer - t.Run("Verify README.md has been deleted", func(t *testing.T) { - filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts) - assert.Nil(t, filesResponse) - expectedError := "repository file does not exist [path: " + opts.Files[0].TreePath + "]" - assert.EqualError(t, err, expectedError) - }) -} + 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) + require.NoError(t, err) + assert.NotNil(t, filesResponse) + assert.Nil(t, filesResponse.Files[0]) + assert.Equal(t, "test message\n", filesResponse.Commit.Message) + MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusNotFound) + }) -// Test opts with branch names removed, same results -func TestChangeRepoFilesForDeleteWithoutBranchNames(t *testing.T) { - onGiteaRun(t, testDeleteRepoFilesWithoutBranchNames) -} + 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) + require.NoError(t, err) + require.NotNil(t, filesResponse) + 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) + }) -func testDeleteRepoFilesWithoutBranchNames(t *testing.T, u *url.URL) { - // setup - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - 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) - assert.NoError(t, err) - expectedFileResponse := getExpectedFileResponseForRepoFilesDelete() - assert.NotNil(t, filesResponse) - assert.Nil(t, filesResponse.Files[0]) - assert.Equal(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message) - assert.Equal(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity) - assert.Equal(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity) - assert.Equal(t, expectedFileResponse.Verification, filesResponse.Verification) + t.Run("Delete directory", func(t *testing.T) { + urlRaw := "/user2/repo1/raw/branch/sub-home-md-img-check/docs/README.md" + MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusOK) + opts := &files_service.ChangeRepoFilesOptions{ + OldBranch: "sub-home-md-img-check", + LastCommitID: "4649299398e4d39a5c09eb4f534df6f1e1eb87cc", + Files: []*files_service.ChangeRepoFile{ + { + Operation: "delete", + TreePath: "docs", + DeleteRecursively: true, + }, + }, + Message: "test message", + } + filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts) + require.NoError(t, err) + assert.NotNil(t, filesResponse) + assert.Nil(t, filesResponse.Files[0]) + assert.Equal(t, "test message\n", filesResponse.Commit.Message) + MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusNotFound) + }) }) } diff --git a/web_src/css/editor/fileeditor.css b/web_src/css/editor/fileeditor.css index 698efffc9925e..12ae97a1094a3 100644 --- a/web_src/css/editor/fileeditor.css +++ b/web_src/css/editor/fileeditor.css @@ -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 { border-color: var(--color-secondary); } diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css index 779339c46b39d..aedf53569a95a 100644 --- a/web_src/css/modules/animations.css +++ b/web_src/css/modules/animations.css @@ -28,7 +28,7 @@ aspect-ratio: 1; transform: translate(-50%, -50%); animation: isloadingspin 1000ms infinite linear; - border-width: 4px; + border-width: 3px; border-style: solid; border-color: var(--color-secondary) var(--color-secondary) var(--color-secondary-dark-8) var(--color-secondary-dark-8); border-radius: var(--border-radius-full); diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 9b70e0e6dbaa1..0bf37ca083807 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -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; } -.repository.file.list .non-diff-file-content .header .small.icon { +.non-diff-file-content .header .small.icon { 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; } -.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); padding: 8px; vertical-align: middle; 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); } -.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); } -.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; opacity: var(--opacity-disabled); cursor: default; } -.repository.file.list .non-diff-file-content .plain-text { +.non-diff-file-content .plain-text { padding: 1em 2em; } -.repository.file.list .non-diff-file-content .plain-text pre { +.non-diff-file-content .plain-text pre { overflow-wrap: anywhere; white-space: pre-wrap; } -.repository.file.list .non-diff-file-content .csv { +.non-diff-file-content .csv { overflow-x: auto; padding: 0 !important; } -.repository.file.list .non-diff-file-content pre { +.non-diff-file-content pre { overflow: auto; } -.repository.file.list .non-diff-file-content .asciicast { +.non-diff-file-content .asciicast { padding: 0 !important; } .repo-editor-header { + display: flex; + margin: 1rem 0; + padding: 3px 0; width: 100%; + gap: 0.5em; + align-items: center; } .repo-editor-header input { @@ -216,17 +221,13 @@ td .commit-summary { margin-right: 5px !important; } -.repository.file.editor .tabular.menu .svg { - margin-right: 5px; -} - .repository.file.editor .commit-form-wrapper { - padding-left: 48px; + padding-left: 58px; } .repository.file.editor .commit-form-wrapper .commit-avatar { float: left; - margin-left: -48px; + margin-left: -58px; } .repository.file.editor .commit-form-wrapper .commit-form { @@ -1409,12 +1410,25 @@ td .commit-summary { 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; margin: 0; 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 { vertical-align: baseline; } @@ -1483,6 +1497,12 @@ tbody.commit-list { line-height: initial; } +.commit-body a.commit code, +.commit-summary a.commit code { + /* these links are generated by the render: ... */ + background: inherit; +} + .git-notes.top { text-align: left; } diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css index ee371f1b1c982..60bf1f17f941b 100644 --- a/web_src/css/repo/home.css +++ b/web_src/css/repo/home.css @@ -54,7 +54,9 @@ 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%; min-width: 0; max-height: 100vh; @@ -65,6 +67,12 @@ overflow-y: hidden; } +@media (max-width: 767.98px) { + .repo-view-file-tree-container { + display: none; + } +} + .repo-view-content { flex: 1; min-width: 0; diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index 1718a1f06b723..f89752dc79125 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -244,6 +244,7 @@ gitea-theme-meta-info { --color-highlight-fg: #87651e; --color-highlight-bg: #352c1c; --color-overlay-backdrop: #080808c0; + --color-danger: var(--color-red); accent-color: var(--color-accent); color-scheme: dark; } diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index db54f5e5fbfd6..1261ef8be03db 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -244,6 +244,7 @@ gitea-theme-meta-info { --color-highlight-fg: #eed200; --color-highlight-bg: #fffbdd; --color-overlay-backdrop: #080808c0; + --color-danger: var(--color-red); accent-color: var(--color-accent); color-scheme: light; } diff --git a/web_src/js/components/RepoFileSearch.vue b/web_src/js/components/RepoFileSearch.vue new file mode 100644 index 0000000000000..cbc1d50656d69 --- /dev/null +++ b/web_src/js/components/RepoFileSearch.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/web_src/js/features/repo-findfile.ts b/web_src/js/features/repo-findfile.ts index 59c827126fb66..7a35a3c7ffebf 100644 --- a/web_src/js/features/repo-findfile.ts +++ b/web_src/js/features/repo-findfile.ts @@ -1,13 +1,8 @@ -import {svg} from '../svg.ts'; -import {toggleElem} from '../utils/dom.ts'; -import {pathEscapeSegments} from '../utils/url.ts'; -import {GET} from '../modules/fetch.ts'; +import {createApp} from 'vue'; +import RepoFileSearch from '../components/RepoFileSearch.vue'; +import {registerGlobalInitFunc} from '../modules/observer.ts'; const threshold = 50; -let files: Array = []; -let repoFindFileInput: HTMLInputElement; -let repoFindFileTableBody: HTMLElement; -let repoFindFileNoResult: HTMLElement; // 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 @@ -73,48 +68,14 @@ export function filterRepoFilesWeighted(files: Array, filter: string) { return filterResult; } -function filterRepoFiles(filter: string) { - const treeLink = repoFindFileInput.getAttribute('data-url-tree-link'); - repoFindFileTableBody.innerHTML = ''; - - const filterResult = filterRepoFilesWeighted(files, filter); - - toggleElem(repoFindFileNoResult, !filterResult.length); - for (const r of filterResult) { - const row = document.createElement('tr'); - 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(); +export function initRepoFileSearch() { + registerGlobalInitFunc('initRepoFileSearch', (el) => { + createApp(RepoFileSearch, { + repoLink: el.getAttribute('data-repo-link'), + currentRefNameSubURL: el.getAttribute('data-current-ref-name-sub-url'), + treeListUrl: el.getAttribute('data-tree-list-url'), + noResultsText: el.getAttribute('data-no-results-text'), + placeholder: el.getAttribute('data-placeholder'), + }).mount(el); + }); } diff --git a/web_src/js/features/repo-view-file-tree.ts b/web_src/js/features/repo-view-file-tree.ts index f52b64cc51d19..98ffdb8a86aed 100644 --- a/web_src/js/features/repo-view-file-tree.ts +++ b/web_src/js/features/repo-view-file-tree.ts @@ -6,8 +6,12 @@ import {registerGlobalEventFunc} from '../modules/observer.ts'; const {appSubUrl} = window.config; +function isUserSignedIn() { + return Boolean(document.querySelector('#navbar .user-menu')); +} + 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 shouldShow = btn.getAttribute('data-toggle-action') === 'show'; toggleElem(elFileTreeContainer, shouldShow); @@ -15,7 +19,7 @@ async function toggleSidebar(btn: HTMLElement) { // 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`, { data: {codeViewShowFileTree: shouldShow}, }); diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index df56c85c868c6..8f5d4ecb152f6 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -16,7 +16,7 @@ import {initMarkupAnchors} from './markup/anchors.ts'; import {initNotificationCount} from './features/notification.ts'; import {initRepoIssueContentHistory} from './features/repo-issue-content.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 {initRepoFileView} from './features/file-view.ts'; import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; @@ -101,7 +101,7 @@ const initPerformanceTracer = callInitFunctions([ initSshKeyFormParser, initStopwatch, initTableSort, - initFindFileInRepo, + initRepoFileSearch, initCopyContent, initAdminCommon,