mirror of https://github.com/go-gitea/gitea.git
Merge 442cf3bc92 into 1da1e644ed
commit
ea61ebf24f
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -280,6 +280,8 @@ func EditFile(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
prepareHomeTreeSideBarSwitch(ctx)
|
||||
|
||||
// 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))
|
||||
|
||||
|
|
@ -384,7 +386,7 @@ func DeleteFile(ctx *context.Context) {
|
|||
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,33 +394,80 @@ func DeleteFilePost(ctx *context.Context) {
|
|||
}
|
||||
|
||||
treePath := ctx.Repo.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{
|
||||
|
||||
// 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 filesToDelete []*files_service.ChangeRepoFile
|
||||
var commitMessage string
|
||||
|
||||
if entry.IsDir() {
|
||||
// Get all files in the directory recursively
|
||||
tree, err := ctx.Repo.Commit.SubTree(treePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("SubTree", err)
|
||||
return
|
||||
}
|
||||
|
||||
entries, err := tree.ListEntriesRecursiveFast()
|
||||
if err != nil {
|
||||
ctx.ServerError("ListEntriesRecursiveFast", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create delete operations for all files in the directory
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && !e.IsSubModule() {
|
||||
filesToDelete = append(filesToDelete, &files_service.ChangeRepoFile{
|
||||
Operation: "delete",
|
||||
TreePath: treePath + "/" + e.Name(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete_directory", treePath))
|
||||
} else {
|
||||
// Single file deletion
|
||||
filesToDelete = []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "delete",
|
||||
TreePath: treePath,
|
||||
},
|
||||
},
|
||||
Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
|
||||
Signoff: parsed.form.Signoff,
|
||||
Author: parsed.GitCommitter,
|
||||
Committer: parsed.GitCommitter,
|
||||
}
|
||||
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: filesToDelete,
|
||||
Message: commitMessage,
|
||||
Signoff: parsed.form.Signoff,
|
||||
Author: parsed.GitCommitter,
|
||||
Committer: parsed.GitCommitter,
|
||||
})
|
||||
if err != nil {
|
||||
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||
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)
|
||||
}
|
||||
|
||||
func UploadFile(ctx *context.Context) {
|
||||
ctx.Data["PageIsUpload"] = true
|
||||
prepareHomeTreeSideBarSwitch(ctx)
|
||||
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
|
||||
opts := prepareEditorCommitFormOptions(ctx, "_upload")
|
||||
if ctx.Written() {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
)
|
||||
|
||||
func NewDiffPatch(ctx *context.Context) {
|
||||
prepareHomeTreeSideBarSwitch(ctx)
|
||||
prepareEditorCommitFormOptions(ctx, "_diffpatch")
|
||||
if ctx.Written() {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,15 +1,25 @@
|
|||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository file editor edit">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui container fluid padded">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"
|
||||
<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" .}}
|
||||
</div>
|
||||
<div class="repo-view-content">
|
||||
<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-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||
>
|
||||
{{.CsrfTokenHtml}}
|
||||
{{template "repo/editor/common_top" .}}
|
||||
<div class="repo-editor-header">
|
||||
<div class="repo-editor-header tw-flex tw-items-center tw-gap-2">
|
||||
<button type="button" class="repo-view-file-tree-toggle-show ui compact basic button icon 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>
|
||||
{{template "repo/editor/common_breadcrumb" .}}
|
||||
</div>
|
||||
{{if not .NotEditableReason}}
|
||||
|
|
@ -47,7 +57,9 @@
|
|||
</div>
|
||||
{{end}}
|
||||
{{template "repo/editor/commit_form" .}}
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,27 @@
|
|||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository file editor edit">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui container fluid padded">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"
|
||||
<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" .}}
|
||||
</div>
|
||||
<div class="repo-view-content">
|
||||
<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-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||
>
|
||||
{{.CsrfTokenHtml}}
|
||||
{{template "repo/editor/common_top" .}}
|
||||
<div class="repo-editor-header">
|
||||
<div class="repo-editor-header tw-flex tw-items-center tw-gap-2">
|
||||
<button type="button" class="repo-view-file-tree-toggle-show ui compact basic button icon 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>
|
||||
<div class="breadcrumb">
|
||||
{{ctx.Locale.Tr "repo.editor.patching"}}
|
||||
<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a>
|
||||
<div class="breadcrumb-divider">:</div>
|
||||
<a class="section" href="{{$.BranchLink}}">{{.BranchName}}</a>
|
||||
<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
|
||||
<input type="hidden" name="tree_path" value="__dummy_for_EditRepoFileForm.TreePath(Required)__">
|
||||
|
|
@ -33,7 +41,9 @@
|
|||
</div>
|
||||
</div>
|
||||
{{template "repo/editor/commit_form" .}}
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,31 @@
|
|||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository file editor upload">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui container fluid padded">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui comment form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
|
||||
<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" .}}
|
||||
</div>
|
||||
<div class="repo-view-content">
|
||||
<form class="ui comment form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
|
||||
{{.CsrfTokenHtml}}
|
||||
{{template "repo/editor/common_top" .}}
|
||||
<div class="repo-editor-header">
|
||||
<div class="repo-editor-header tw-flex tw-items-center tw-gap-2">
|
||||
<button type="button" class="repo-view-file-tree-toggle-show ui compact basic button icon 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>
|
||||
{{template "repo/editor/common_breadcrumb" .}}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{template "repo/upload" .}}
|
||||
</div>
|
||||
{{template "repo/editor/commit_form" .}}
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
|
|
|
|||
|
|
@ -42,26 +42,6 @@
|
|||
<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}}
|
||||
<a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}">
|
||||
{{ctx.Locale.Tr "repo.use_template"}}
|
||||
|
|
@ -86,6 +66,52 @@
|
|||
</div>
|
||||
|
||||
<div class="repo-button-row-right">
|
||||
{{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 not .IsViewFile}}
|
||||
<button class="ui dropdown basic compact jump button icon repo-file-actions-dropdown" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
|
||||
{{svg "octicon-kebab-horizontal"}}
|
||||
<div class="menu">
|
||||
<a class="item" data-clipboard-text="{{.TreePath}}">
|
||||
{{svg "octicon-copy" 16 "tw-mr-2"}}{{ctx.Locale.Tr "copy_path"}}
|
||||
</a>
|
||||
<a class="item" data-clipboard-text="{{AppUrl}}{{StringUtils.TrimPrefix .Repository.Link "/"}}/src/commit/{{.CommitID}}/{{PathEscapeSegments .TreePath}}">
|
||||
{{svg "octicon-link" 16 "tw-mr-2"}}{{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 danger" href="{{.RepoLink}}/_delete/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
|
||||
{{svg "octicon-trash" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.editor.delete_this_directory"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<!-- Only show clone panel in repository home page -->
|
||||
{{if $isTreePathRoot}}
|
||||
{{template "repo/clone_panel" .}}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,14 @@
|
|||
<b>{{ctx.Locale.Tr "files"}}</b>
|
||||
</div>
|
||||
|
||||
<div class="ui small input tw-w-full tw-px-2 tw-pb-2">
|
||||
<input id="file-tree-search" type="text" placeholder="{{ctx.Locale.Tr "repo.find_file.go_to_file"}}" autocomplete="off">
|
||||
</div>
|
||||
|
||||
{{/* 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-y-auto tw-overflow-x-visible tw-h-full is-loading"
|
||||
data-repo-link="{{.RepoLink}}"
|
||||
data-tree-path="{{$.TreePath}}"
|
||||
data-current-ref-name-sub-url="{{.RefTypeNameSubURL}}"
|
||||
data-tree-list-url="{{.RepoLink}}/tree-list/{{.RefTypeNameSubURL}}"
|
||||
></div>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
@import "./repo/issue-list.css";
|
||||
@import "./repo/list-header.css";
|
||||
@import "./repo/file-view.css";
|
||||
@import "./repo/file-actions.css";
|
||||
@import "./repo/wiki.css";
|
||||
@import "./repo/header.css";
|
||||
@import "./repo/home.css";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/* Repository file actions dropdown and centered content */
|
||||
.ui.dropdown.repo-add-file > .menu {
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
|
||||
.ui.dropdown.repo-file-actions-dropdown > .menu {
|
||||
margin-top: 4px !important;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.repo-file-actions-dropdown .menu .item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.repo-file-actions-dropdown .menu .divider {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger,
|
||||
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger svg {
|
||||
color: var(--color-red) !important;
|
||||
}
|
||||
|
||||
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger:hover,
|
||||
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger:hover svg {
|
||||
color: var(--color-red) !important;
|
||||
background: var(--color-red-badge-hover-bg) !important;
|
||||
}
|
||||
|
|
@ -1,9 +1,17 @@
|
|||
<script lang="ts" setup>
|
||||
import ViewFileTreeItem from './ViewFileTreeItem.vue';
|
||||
import {onMounted, useTemplateRef} from 'vue';
|
||||
import {onMounted, onUnmounted, useTemplateRef, ref, computed, watch, nextTick} from 'vue';
|
||||
import {createViewFileTreeStore} from './ViewFileTreeStore.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {filterRepoFilesWeighted} from '../features/repo-findfile.ts';
|
||||
import {pathEscapeSegments} from '../utils/url.ts';
|
||||
import {svg} from '../svg.ts';
|
||||
|
||||
const elRoot = useTemplateRef('elRoot');
|
||||
const searchResults = useTemplateRef('searchResults');
|
||||
const searchQuery = ref('');
|
||||
const allFiles = ref<string[]>([]);
|
||||
const selectedIndex = ref(0);
|
||||
|
||||
const props = defineProps({
|
||||
repoLink: {type: String, required: true},
|
||||
|
|
@ -12,27 +20,244 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
const store = createViewFileTreeStore(props);
|
||||
|
||||
const filteredFiles = computed(() => {
|
||||
if (!searchQuery.value) return [];
|
||||
return filterRepoFilesWeighted(allFiles.value, searchQuery.value);
|
||||
});
|
||||
|
||||
const treeLink = computed(() => `${props.repoLink}/src/${props.currentRefNameSubURL}`);
|
||||
|
||||
let searchInputElement: HTMLInputElement | null = null;
|
||||
|
||||
const handleSearchInput = (e: Event) => {
|
||||
searchQuery.value = (e.target as HTMLInputElement).value;
|
||||
selectedIndex.value = 0;
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && searchQuery.value) {
|
||||
e.preventDefault();
|
||||
clearSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!searchQuery.value || filteredFiles.value.length === 0) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
selectedIndex.value = Math.min(selectedIndex.value + 1, filteredFiles.value.length - 1);
|
||||
scrollSelectedIntoView();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
selectedIndex.value = Math.max(selectedIndex.value - 1, 0);
|
||||
scrollSelectedIntoView();
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const selectedFile = filteredFiles.value[selectedIndex.value];
|
||||
if (selectedFile) {
|
||||
handleSearchResultClick(selectedFile.matchResult.join(''));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
searchQuery.value = '';
|
||||
if (searchInputElement) searchInputElement.value = '';
|
||||
};
|
||||
|
||||
const scrollSelectedIntoView = () => {
|
||||
nextTick(() => {
|
||||
const resultsEl = searchResults.value;
|
||||
if (!resultsEl) return;
|
||||
|
||||
const selectedEl = resultsEl.querySelector('.file-tree-search-result-item.selected');
|
||||
if (selectedEl) {
|
||||
selectedEl.scrollIntoView({block: 'nearest', behavior: 'smooth'});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (!searchQuery.value) return;
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
const resultsEl = searchResults.value;
|
||||
|
||||
if (searchInputElement && !searchInputElement.contains(target) &&
|
||||
resultsEl && !resultsEl.contains(target)) {
|
||||
clearSearch();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
store.rootFiles = await store.loadChildren('', props.treePath);
|
||||
elRoot.value.closest('.is-loading')?.classList?.remove('is-loading');
|
||||
|
||||
// Load all files for search
|
||||
const treeListUrl = elRoot.value.closest('#view-file-tree')?.getAttribute('data-tree-list-url');
|
||||
if (treeListUrl) {
|
||||
const response = await GET(treeListUrl);
|
||||
allFiles.value = await response.json();
|
||||
}
|
||||
|
||||
searchInputElement = document.querySelector('#file-tree-search');
|
||||
if (searchInputElement) {
|
||||
searchInputElement.addEventListener('input', handleSearchInput);
|
||||
searchInputElement.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
|
||||
window.addEventListener('popstate', (e) => {
|
||||
store.selectedItem = e.state?.treePath || '';
|
||||
if (e.state?.url) store.loadViewContent(e.state.url);
|
||||
});
|
||||
});
|
||||
|
||||
// Position search results below the input
|
||||
watch(searchQuery, async () => {
|
||||
if (searchQuery.value && searchInputElement) {
|
||||
await nextTick();
|
||||
const resultsEl = searchResults.value;
|
||||
if (resultsEl) {
|
||||
const rect = searchInputElement.getBoundingClientRect();
|
||||
resultsEl.style.top = `${rect.bottom + 4}px`;
|
||||
resultsEl.style.left = `${rect.left}px`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (searchInputElement) {
|
||||
searchInputElement.removeEventListener('input', handleSearchInput);
|
||||
searchInputElement.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
function handleSearchResultClick(filePath: string) {
|
||||
clearSearch();
|
||||
window.location.href = `${treeLink.value}/${pathEscapeSegments(filePath)}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="view-file-tree-items" ref="elRoot">
|
||||
<ViewFileTreeItem v-for="item in store.rootFiles" :key="item.name" :item="item" :store="store"/>
|
||||
<div ref="elRoot" class="file-tree-root">
|
||||
<Teleport to="body">
|
||||
<div v-if="searchQuery && filteredFiles.length > 0" ref="searchResults" class="file-tree-search-results">
|
||||
<div
|
||||
v-for="(result, idx) in filteredFiles"
|
||||
:key="result.matchResult.join('')"
|
||||
:class="['file-tree-search-result-item', {'selected': idx === selectedIndex}]"
|
||||
@click="handleSearchResultClick(result.matchResult.join(''))"
|
||||
@mouseenter="selectedIndex = idx"
|
||||
:title="result.matchResult.join('')"
|
||||
>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="svg('octicon-file', 16)"/>
|
||||
<span class="file-tree-search-result-path">
|
||||
<span
|
||||
v-for="(part, index) in result.matchResult"
|
||||
:key="index"
|
||||
:class="{'search-match': index % 2 === 1}"
|
||||
>{{ part }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="searchQuery && filteredFiles.length === 0" ref="searchResults" class="file-tree-search-results file-tree-search-no-results">
|
||||
<div class="file-tree-no-results-content">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="svg('octicon-search', 24)"/>
|
||||
<span>No matching files found</span>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
<div class="view-file-tree-items">
|
||||
<ViewFileTreeItem v-for="item in store.rootFiles" :key="item.name" :item="item" :store="store"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-tree-root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view-file-tree-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.file-tree-search-results {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background: var(--color-box-body);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
min-width: 300px;
|
||||
width: max-content;
|
||||
max-width: 600px;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.file-tree-search-result-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
.file-tree-search-result-item > span:first-child {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.file-tree-search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.file-tree-search-result-item:hover,
|
||||
.file-tree-search-result-item.selected {
|
||||
background-color: var(--color-hover);
|
||||
}
|
||||
|
||||
.file-tree-search-result-path {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.search-match {
|
||||
color: var(--color-red);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.file-tree-search-no-results {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.file-tree-no-results-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
color: var(--color-text-light-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-tree-no-results-content > span:first-child {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue