{{template "repo/header" .}}
@@ -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)}} -
+
+ {{if $hasTreeSidebar}} +
{{template "repo/view_file_tree_sidebar" .}}
+ {{end}} +
{{template "repo/sub_menu" .}}
+ {{if $hasTreeSidebar}} + + {{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}} - {{svg "octicon-git-pull-request"}} @@ -60,7 +74,7 @@ {{end}} {{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} - + Files +
+ +
+
+
+ {{svg "octicon-sync" 16 "job-status-rotate"}} +
+
diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css index 65005e2263..20964bcb6a 100644 --- a/web_src/css/repo/home.css +++ b/web_src/css/repo/home.css @@ -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; diff --git a/web_src/js/components/ViewFileTree.vue b/web_src/js/components/ViewFileTree.vue new file mode 100644 index 0000000000..3337f6e9e5 --- /dev/null +++ b/web_src/js/components/ViewFileTree.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue new file mode 100644 index 0000000000..db2e888fdd --- /dev/null +++ b/web_src/js/components/ViewFileTreeItem.vue @@ -0,0 +1,120 @@ + + + + diff --git a/web_src/js/features/repo-view-file-tree-sidebar.ts b/web_src/js/features/repo-view-file-tree-sidebar.ts new file mode 100644 index 0000000000..d1404e3781 --- /dev/null +++ b/web_src/js/features/repo-view-file-tree-sidebar.ts @@ -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); +} diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 51d8c96fbd..4602e22ffa 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -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,