mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-30 07:34:45 +01:00 
			
		
		
		
	Merge branch 'add-file-tree-to-file-view-page' of github.com:kerwin612/gitea into add-file-tree-to-file-view-page
This commit is contained in:
		
						commit
						64b4cf4f54
					
				| @ -46,6 +46,11 @@ func RefBlame(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// ctx.Data["RepoPreferences"] = ctx.Session.Get("repoPreferences") | ||||
| 	ctx.Data["RepoPreferences"] = &preferencesForm{ | ||||
| 		ShowFileViewTreeSidebar: true, | ||||
| 	} | ||||
| 
 | ||||
| 	branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() | ||||
| 	treeLink := branchLink | ||||
| 	rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() | ||||
|  | ||||
							
								
								
									
										45
									
								
								routers/web/repo/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								routers/web/repo/file.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| // Copyright 2014 The Gogs Authors. All rights reserved. | ||||
| // Copyright 2018 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/unit" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/services/context" | ||||
| 	files_service "code.gitea.io/gitea/services/repository/files" | ||||
| ) | ||||
| 
 | ||||
| // canReadFiles returns true if repository is readable and user has proper access level. | ||||
| func canReadFiles(r *context.Repository) bool { | ||||
| 	return r.Permission.CanRead(unit.TypeCode) | ||||
| } | ||||
| 
 | ||||
| // GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir | ||||
| func GetContents(ctx *context.Context) { | ||||
| 	if !canReadFiles(ctx.Repo) { | ||||
| 		ctx.NotFound("Invalid FilePath", nil) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	treePath := ctx.PathParam("*") | ||||
| 	ref := ctx.FormTrim("ref") | ||||
| 
 | ||||
| 	if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, treePath, ref); err != nil { | ||||
| 		if git.IsErrNotExist(err) { | ||||
| 			ctx.NotFound("GetContentsOrList", err) | ||||
| 			return | ||||
| 		} | ||||
| 		ctx.ServerError("Repo.GitRepo.GetCommit", err) | ||||
| 	} else { | ||||
| 		ctx.JSON(http.StatusOK, fileList) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetContentsList Get the metadata of all the entries of the root dir | ||||
| func GetContentsList(ctx *context.Context) { | ||||
| 	GetContents(ctx) | ||||
| } | ||||
| @ -22,6 +22,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/cache" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/optional" | ||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | ||||
| @ -758,3 +759,20 @@ func PrepareBranchList(ctx *context.Context) { | ||||
| 	} | ||||
| 	ctx.Data["Branches"] = brs | ||||
| } | ||||
| 
 | ||||
| type preferencesForm struct { | ||||
| 	ShowFileViewTreeSidebar bool `json:"show_file_view_tree_sidebar"` | ||||
| } | ||||
| 
 | ||||
| func UpdatePreferences(ctx *context.Context) { | ||||
| 	form := &preferencesForm{} | ||||
| 	if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { | ||||
| 		ctx.ServerError("DecodePreferencesForm", err) | ||||
| 		return | ||||
| 	} | ||||
| 	// if err := ctx.Session.Set("repoPreferences", form); err != nil { | ||||
| 	// 	ctx.ServerError("Session.Set", err) | ||||
| 	// 	return | ||||
| 	// } | ||||
| 	ctx.JSONOK() | ||||
| } | ||||
|  | ||||
| @ -305,6 +305,11 @@ func Home(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// ctx.Data["RepoPreferences"] = ctx.Session.Get("repoPreferences") | ||||
| 	ctx.Data["RepoPreferences"] = &preferencesForm{ | ||||
| 		ShowFileViewTreeSidebar: true, | ||||
| 	} | ||||
| 
 | ||||
| 	title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name | ||||
| 	if len(ctx.Repo.Repository.Description) > 0 { | ||||
| 		title += ": " + ctx.Repo.Repository.Description | ||||
|  | ||||
| @ -987,6 +987,7 @@ func registerRoutes(m *web.Router) { | ||||
| 		m.Get("/migrate", repo.Migrate) | ||||
| 		m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost) | ||||
| 		m.Get("/search", repo.SearchRepo) | ||||
| 		m.Put("/preferences", repo.UpdatePreferences) | ||||
| 	}, reqSignIn) | ||||
| 	// end "/repo": create, migrate, search | ||||
| 
 | ||||
| @ -1161,6 +1162,10 @@ func registerRoutes(m *web.Router) { | ||||
| 			m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.TreeList) | ||||
| 			m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.TreeList) | ||||
| 		}) | ||||
| 		m.Group("/contents", func() { | ||||
| 			m.Get("", repo.GetContentsList) | ||||
| 			m.Get("/*", repo.GetContents) | ||||
| 		}) | ||||
| 		m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff) | ||||
| 		m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists). | ||||
| 			Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff). | ||||
|  | ||||
| @ -1,4 +1,11 @@ | ||||
| {{template "base/head" .}} | ||||
| {{$treeNamesLen := len .TreeNames}} | ||||
| {{$isTreePathRoot := eq $treeNamesLen 0}} | ||||
| {{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}} | ||||
| {{$hasTreeSidebar := not $isTreePathRoot}} | ||||
| {{$showTreeSidebar := .RepoPreferences.ShowFileViewTreeSidebar}} | ||||
| {{$hideTreeSidebar := not $showTreeSidebar}} | ||||
| {{$hasAndShowTreeSidebar := and $hasTreeSidebar $showTreeSidebar}} | ||||
| <div role="main" aria-label="{{.Title}}" class="page-content repository file list {{if .IsBlame}}blame{{end}}"> | ||||
| 	{{template "repo/header" .}} | ||||
| 	<div class="ui container {{if .IsBlame}}fluid padded{{end}}"> | ||||
| @ -16,14 +23,20 @@ | ||||
| 
 | ||||
| 		{{template "repo/code/recently_pushed_new_branches" .}} | ||||
| 
 | ||||
| 		{{$treeNamesLen := len .TreeNames}} | ||||
| 		{{$isTreePathRoot := eq $treeNamesLen 0}} | ||||
| 		{{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}} | ||||
| 		<div class="{{Iif $showSidebar "repo-grid-filelist-sidebar" "repo-grid-filelist-only"}}"> | ||||
| 		<div class="{{Iif $showSidebar "repo-grid-filelist-sidebar" (Iif $showTreeSidebar "repo-grid-tree-sidebar" "repo-grid-filelist-only")}}"> | ||||
| 			{{if $hasTreeSidebar}} | ||||
| 				<div class="repo-view-file-tree-sidebar not-mobile {{if $hideTreeSidebar}}tw-hidden{{end}}">{{template "repo/view_file_tree_sidebar" .}}</div> | ||||
| 			{{end}} | ||||
| 
 | ||||
| 			<div class="repo-home-filelist"> | ||||
| 				{{template "repo/sub_menu" .}} | ||||
| 				<div class="repo-button-row"> | ||||
| 					<div class="repo-button-row-left"> | ||||
| 						{{if $hasTreeSidebar}} | ||||
| 							<button class="show-tree-sidebar-button ui compact basic button icon not-mobile {{if $showTreeSidebar}}tw-hidden{{end}}" title="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}"> | ||||
| 								{{svg "octicon-sidebar-collapse" 20 "icon"}} | ||||
| 							</button> | ||||
| 						{{end}} | ||||
| 						{{$branchDropdownCurrentRefType := "branch"}} | ||||
| 						{{$branchDropdownCurrentRefShortName := .BranchName}} | ||||
| 						{{if .IsViewTag}} | ||||
| @ -40,6 +53,7 @@ | ||||
| 							"RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}" | ||||
| 							"AllowCreateNewRef" .CanCreateBranch | ||||
| 							"ShowViewAllRefsEntry" true | ||||
| 							"ContainerClasses" (Iif $hasAndShowTreeSidebar "tw-hidden" "") | ||||
| 						}} | ||||
| 						{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}} | ||||
| 							{{$cmpBranch := ""}} | ||||
| @ -48,7 +62,7 @@ | ||||
| 							{{end}} | ||||
| 							{{$cmpBranch = print $cmpBranch (.BranchName|PathEscapeSegments)}} | ||||
| 							{{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}} | ||||
| 							<a id="new-pull-request" role="button" class="ui compact basic button" href="{{$compareLink}}" | ||||
| 							<a id="new-pull-request" role="button" class="ui compact basic button {{if $hasAndShowTreeSidebar}}tw-hidden{{end}}" href="{{$compareLink}}" | ||||
| 								data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}"> | ||||
| 								{{svg "octicon-git-pull-request"}} | ||||
| 							</a> | ||||
| @ -60,7 +74,7 @@ | ||||
| 						{{end}} | ||||
| 
 | ||||
| 						{{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} | ||||
| 							<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}> | ||||
| 							<button class="add-file-dropdown ui dropdown basic compact jump button {{if $hasAndShowTreeSidebar}}tw-hidden{{end}}"{{if not .Repository.CanEnableEditor}} disabled{{end}}> | ||||
| 								{{ctx.Locale.Tr "repo.editor.add_file"}} | ||||
| 								{{svg "octicon-triangle-down" 14 "dropdown icon"}} | ||||
| 								<div class="menu"> | ||||
|  | ||||
							
								
								
									
										65
									
								
								templates/repo/view_file_tree_sidebar.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								templates/repo/view_file_tree_sidebar.tmpl
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| <div class="view-file-tree-sidebar-top"> | ||||
| 	<div class="sidebar-header"> | ||||
| 		<button class="hide-tree-sidebar-button ui compact basic button icon" title="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}"> | ||||
| 			{{svg "octicon-sidebar-expand" 20 "icon"}} | ||||
| 		</button> | ||||
| 		<b> Files</b> | ||||
| 	</div> | ||||
| 	<div class="sidebar-ref"> | ||||
| 		{{$branchDropdownCurrentRefType := "branch"}} | ||||
| 		{{$branchDropdownCurrentRefShortName := .BranchName}} | ||||
| 		{{if .IsViewTag}} | ||||
| 			{{$branchDropdownCurrentRefType = "tag"}} | ||||
| 			{{$branchDropdownCurrentRefShortName = .TagName}} | ||||
| 		{{end}} | ||||
| 		{{template "repo/branch_dropdown" dict | ||||
| 			"Repository" .Repository | ||||
| 			"ShowTabBranches" true | ||||
| 			"ShowTabTags" true | ||||
| 			"CurrentRefType" $branchDropdownCurrentRefType | ||||
| 			"CurrentRefShortName" $branchDropdownCurrentRefShortName | ||||
| 			"CurrentTreePath" .TreePath | ||||
| 			"RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}" | ||||
| 			"AllowCreateNewRef" .CanCreateBranch | ||||
| 			"ShowViewAllRefsEntry" true | ||||
| 		}} | ||||
| 
 | ||||
| 		{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}} | ||||
| 			{{$cmpBranch := ""}} | ||||
| 			{{if ne .Repository.ID .BaseRepo.ID}} | ||||
| 				{{$cmpBranch = printf "%s/%s:" (.Repository.OwnerName|PathEscape) (.Repository.Name|PathEscape)}} | ||||
| 			{{end}} | ||||
| 			{{$cmpBranch = print $cmpBranch (.BranchName|PathEscapeSegments)}} | ||||
| 			{{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}} | ||||
| 			<a role="button" class="ui compact basic button" href="{{$compareLink}}" | ||||
| 				data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}"> | ||||
| 				{{svg "octicon-git-pull-request"}} | ||||
| 			</a> | ||||
| 		{{end}} | ||||
| 
 | ||||
| 		{{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} | ||||
| 			<button class="ui dropdown basic compact jump button"{{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}} | ||||
| 	</div> | ||||
| </div> | ||||
| <div class="view-file-tree-sidebar-bottom"> | ||||
| 	<div id="view-file-tree" class="center" data-api-base-url="{{.RepoLink}}" data-tree-path="{{$.TreePath}}"> | ||||
| 		{{svg "octicon-sync" 16 "job-status-rotate"}} | ||||
| 	</div> | ||||
| </div> | ||||
| @ -50,6 +50,49 @@ | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .repo-grid-tree-sidebar { | ||||
|   display: grid; | ||||
|   grid-template-columns: 300px auto; | ||||
|   grid-template-rows: auto auto 1fr; | ||||
| } | ||||
| 
 | ||||
| .repo-grid-tree-sidebar .repo-home-filelist { | ||||
|   min-width: 0; | ||||
|   grid-column: 2; | ||||
|   grid-row: 1 / 4; | ||||
| } | ||||
| 
 | ||||
| .repo-grid-tree-sidebar .repo-view-file-tree-sidebar { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 0.25em; | ||||
| } | ||||
| 
 | ||||
| .repo-grid-tree-sidebar .view-file-tree-sidebar-top { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 0.25em; | ||||
| } | ||||
| 
 | ||||
| .repo-grid-tree-sidebar .view-file-tree-sidebar-top .button { | ||||
|   padding: 6px 10px !important; | ||||
|   height: 30px; | ||||
|   flex-shrink: 0; | ||||
|   margin: 0; | ||||
| } | ||||
| 
 | ||||
| .repo-grid-tree-sidebar .view-file-tree-sidebar-top .sidebar-ref { | ||||
|   display: flex; | ||||
|   gap: 0.25em; | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 767.98px) { | ||||
|   .repo-grid-tree-sidebar { | ||||
|     grid-template-columns: auto; | ||||
|     grid-template-rows: auto auto auto; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .language-stats { | ||||
|   display: flex; | ||||
|   gap: 2px; | ||||
|  | ||||
							
								
								
									
										26
									
								
								web_src/js/components/ViewFileTree.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								web_src/js/components/ViewFileTree.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| <script lang="ts" setup> | ||||
| import ViewFileTreeItem from './ViewFileTreeItem.vue'; | ||||
| 
 | ||||
| defineProps<{ | ||||
|   files: any, | ||||
|   selectedItem: any, | ||||
|   loadChildren: any, | ||||
|   loadContent: any; | ||||
| }>(); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div class="view-file-tree-items"> | ||||
|     <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often --> | ||||
|     <ViewFileTreeItem v-for="item in files" :key="item.name" :item="item" :selected-item="selectedItem" :load-content="loadContent" :load-children="loadChildren"/> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| .view-file-tree-items { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 1px; | ||||
|   margin-right: .5rem; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										120
									
								
								web_src/js/components/ViewFileTreeItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								web_src/js/components/ViewFileTreeItem.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | ||||
| <script lang="ts" setup> | ||||
| import {SvgIcon} from '../svg.ts'; | ||||
| import {ref} from 'vue'; | ||||
| 
 | ||||
| type Item = { | ||||
|   name: string; | ||||
|   path: string; | ||||
|   htmlUrl: string; | ||||
|   isFile: boolean; | ||||
|   children?: Item[]; | ||||
| }; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   item: Item, | ||||
|   loadContent: any; | ||||
|   loadChildren: any; | ||||
|   selectedItem?: any; | ||||
| }>(); | ||||
| 
 | ||||
| const isLoading = ref(false); | ||||
| const children = ref(props.item.children); | ||||
| const collapsed = ref(!props.item.children); | ||||
| 
 | ||||
| const doLoadChildren = async () => { | ||||
|   collapsed.value = !collapsed.value; | ||||
|   if (!collapsed.value && props.loadChildren) { | ||||
|     isLoading.value = true; | ||||
|     const _children = await props.loadChildren(props.item); | ||||
|     children.value = _children; | ||||
|     isLoading.value = false; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const doLoadDirContent = () => { | ||||
|   doLoadChildren(); | ||||
|   props.loadContent(props.item); | ||||
| }; | ||||
| 
 | ||||
| const doLoadFileContent = () => { | ||||
|   props.loadContent(props.item); | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <!--title instead of tooltip above as the tooltip needs too much work with the current methods, i.e. not being loaded or staying open for "too long"--> | ||||
|   <div | ||||
|     v-if="item.isFile" class="item-file" | ||||
|     :class="{'selected': selectedItem.value === item.path}" | ||||
|     :title="item.name" | ||||
|     @click.stop="doLoadFileContent" | ||||
|   > | ||||
|     <!-- file --> | ||||
|     <SvgIcon name="octicon-file"/> | ||||
|     <span class="gt-ellipsis tw-flex-1">{{ item.name }}</span> | ||||
|   </div> | ||||
|   <div | ||||
|     v-else class="item-directory" | ||||
|     :class="{'selected': selectedItem.value === item.path}" | ||||
|     :title="item.name" | ||||
|     @click.stop="doLoadDirContent" | ||||
|   > | ||||
|     <!-- directory --> | ||||
|     <SvgIcon v-if="isLoading" name="octicon-sync" class="job-status-rotate"/> | ||||
|     <SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop="doLoadChildren"/> | ||||
|     <SvgIcon class="text primary" :name="collapsed ? 'octicon-file-directory-fill' : 'octicon-file-directory-open-fill'"/> | ||||
|     <span class="gt-ellipsis">{{ item.name }}</span> | ||||
|   </div> | ||||
| 
 | ||||
|   <div v-if="children?.length" v-show="!collapsed" class="sub-items"> | ||||
|     <ViewFileTreeItem v-for="childItem in children" :key="childItem.name" :item="childItem" :selected-item="selectedItem" :load-content="loadContent" :load-children="loadChildren"/> | ||||
|   </div> | ||||
| </template> | ||||
| <style scoped> | ||||
| a, a:hover { | ||||
|   text-decoration: none; | ||||
|   color: var(--color-text); | ||||
| } | ||||
| 
 | ||||
| .sub-items { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 1px; | ||||
|   margin-left: 13px; | ||||
|   border-left: 1px solid var(--color-secondary); | ||||
| } | ||||
| 
 | ||||
| .sub-items .item-file { | ||||
|   padding-left: 18px; | ||||
| } | ||||
| 
 | ||||
| .item-directory.selected, .item-file.selected { | ||||
|   color: var(--color-text); | ||||
|   background: var(--color-active); | ||||
|   border-radius: 4px; | ||||
| } | ||||
| 
 | ||||
| .item-file.viewed { | ||||
|   color: var(--color-text-light-3); | ||||
| } | ||||
| 
 | ||||
| .item-directory { | ||||
|   user-select: none; | ||||
| } | ||||
| 
 | ||||
| .item-file, | ||||
| .item-directory { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 0.25em; | ||||
|   padding: 6px; | ||||
| } | ||||
| 
 | ||||
| .item-file:hover, | ||||
| .item-directory:hover { | ||||
|   color: var(--color-text); | ||||
|   background: var(--color-hover); | ||||
|   border-radius: 4px; | ||||
|   cursor: pointer; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										96
									
								
								web_src/js/features/repo-view-file-tree-sidebar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								web_src/js/features/repo-view-file-tree-sidebar.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| import {createApp, ref} from 'vue'; | ||||
| import {toggleElem} from '../utils/dom.ts'; | ||||
| import {GET, PUT} from '../modules/fetch.ts'; | ||||
| import ViewFileTree from '../components/ViewFileTree.vue'; | ||||
| 
 | ||||
| async function toggleSidebar(visibility) { | ||||
|   const sidebarEl = document.querySelector('.repo-view-file-tree-sidebar'); | ||||
|   const showBtnEl = document.querySelector('.show-tree-sidebar-button'); | ||||
|   const refSelectorEl = document.querySelector('.repo-home-filelist .js-branch-tag-selector'); | ||||
|   const newPrBtnEl = document.querySelector('.repo-home-filelist #new-pull-request'); | ||||
|   const addFileEl = document.querySelector('.repo-home-filelist .add-file-dropdown'); | ||||
|   const containerClassList = sidebarEl.parentElement.classList; | ||||
|   containerClassList.toggle('repo-grid-tree-sidebar', visibility); | ||||
|   containerClassList.toggle('repo-grid-filelist-only', !visibility); | ||||
|   toggleElem(sidebarEl, visibility); | ||||
|   toggleElem(showBtnEl, !visibility); | ||||
|   toggleElem(refSelectorEl, !visibility); | ||||
|   toggleElem(newPrBtnEl, !visibility); | ||||
|   if (addFileEl) { | ||||
|     toggleElem(addFileEl, !visibility); | ||||
|   } | ||||
| 
 | ||||
|   // save to session
 | ||||
|   await PUT('/repo/preferences', { | ||||
|     data: { | ||||
|       show_file_view_tree_sidebar: visibility, | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function loadChildren(item?) { | ||||
|   const el = document.querySelector('#view-file-tree'); | ||||
|   const apiBaseUrl = el.getAttribute('data-api-base-url'); | ||||
|   const response = await GET(`${apiBaseUrl}/contents/${item ? item.path : ''}`); | ||||
|   const json = await response.json(); | ||||
|   if (json instanceof Array) { | ||||
|     return json.map((i) => ({ | ||||
|       name: i.name, | ||||
|       isFile: i.type === 'file', | ||||
|       htmlUrl: i.html_url, | ||||
|       path: i.path, | ||||
|     })); | ||||
|   } | ||||
|   return null; | ||||
| } | ||||
| 
 | ||||
| async function loadRecursive(treePath) { | ||||
|   let root = null; | ||||
|   let parent = null; | ||||
|   let parentPath = ''; | ||||
|   for (const i of (`/${treePath}`).split('/')) { | ||||
|     const path = `${parentPath}${parentPath ? '/' : ''}${i}`; | ||||
|     const result = await loadChildren({path}); | ||||
|     if (root === null) { | ||||
|       root = result; | ||||
|       parent = root; | ||||
|     } else { | ||||
|       parent = parent.find((item) => item.path === path); | ||||
|       parent.children = result; | ||||
|       parent = result; | ||||
|     } | ||||
|     parentPath = path; | ||||
|   } | ||||
|   return root; | ||||
| } | ||||
| 
 | ||||
| async function loadContent(item) { | ||||
|   document.querySelector('.repo-home-filelist').innerHTML = `load content of ${item.path}`; | ||||
| } | ||||
| 
 | ||||
| export async function initViewFileTreeSidebar() { | ||||
|   const sidebarElement = document.querySelector('.repo-view-file-tree-sidebar'); | ||||
|   if (!sidebarElement) return; | ||||
| 
 | ||||
|   document.querySelector('.show-tree-sidebar-button').addEventListener('click', () => { | ||||
|     toggleSidebar(true); | ||||
|   }); | ||||
| 
 | ||||
|   document.querySelector('.hide-tree-sidebar-button').addEventListener('click', () => { | ||||
|     toggleSidebar(false); | ||||
|   }); | ||||
| 
 | ||||
|   const fileTree = document.querySelector('#view-file-tree'); | ||||
|   const treePath = fileTree.getAttribute('data-tree-path'); | ||||
|   const selectedItem = ref(treePath); | ||||
| 
 | ||||
|   const files = await loadRecursive(treePath); | ||||
| 
 | ||||
|   fileTree.classList.remove('center'); | ||||
|   const fileTreeView = createApp(ViewFileTree, {files, selectedItem, loadChildren, loadContent: (item) => { | ||||
|     window.history.pushState(null, null, item.htmlUrl); | ||||
|     selectedItem.value = item.path; | ||||
|     loadContent(item); | ||||
|   }}); | ||||
|   fileTreeView.mount(fileTree); | ||||
| } | ||||
| @ -33,6 +33,7 @@ import { | ||||
| } from './features/repo-issue.ts'; | ||||
| import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; | ||||
| import {initRepoTopicBar} from './features/repo-home.ts'; | ||||
| import {initViewFileTreeSidebar} from './features/repo-view-file-tree-sidebar.ts'; | ||||
| import {initAdminCommon} from './features/admin/common.ts'; | ||||
| import {initRepoTemplateSearch} from './features/repo-template.ts'; | ||||
| import {initRepoCodeView} from './features/repo-code.ts'; | ||||
| @ -195,6 +196,7 @@ onDomReady(() => { | ||||
|     initRepoReleaseNew, | ||||
|     initRepoTemplateSearch, | ||||
|     initRepoTopicBar, | ||||
|     initViewFileTreeSidebar, | ||||
|     initRepoWikiForm, | ||||
|     initRepository, | ||||
|     initRepositoryActionView, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user