+
{{template "base/alert" .}}
{{if .Repository.IsArchived}}
@@ -16,123 +22,13 @@
{{template "repo/code/recently_pushed_new_branches" .}}
- {{$treeNamesLen := len .TreeNames}}
- {{$isTreePathRoot := eq $treeNamesLen 0}}
- {{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}}
-
-
- {{template "repo/sub_menu" .}}
-
-
- {{- /* for repo home (default branch) and /owner/repo/src/branch/the-name */ -}}
- {{- $branchDropdownCurrentRefType := "branch" -}}
- {{- $branchDropdownCurrentRefShortName := .BranchName -}}
- {{- if .IsViewTag -}}
- {{- /* for /owner/repo/src/tag/the-name */ -}}
- {{- $branchDropdownCurrentRefType = "tag" -}}
- {{- $branchDropdownCurrentRefShortName = .TagName -}}
- {{- else if .IsViewCommit -}}
- {{- /* for /owner/repo/src/commit/000000 */ -}}
- {{- $branchDropdownCurrentRefType = "commit" -}}
- {{- $branchDropdownCurrentRefShortName = ShortSha .CommitID -}}
- {{- 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}}
-
- {{svg "octicon-git-pull-request"}}
-
- {{end}}
-
-
- {{if $isTreePathRoot}}
- {{ctx.Locale.Tr "repo.find_file.go_to_file"}}
- {{end}}
-
- {{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
-
- {{end}}
-
- {{if and $isTreePathRoot .Repository.IsTemplate}}
-
- {{ctx.Locale.Tr "repo.use_template"}}
-
- {{end}}
-
- {{if not $isTreePathRoot}}
- {{$treeNameIdxLast := Eval $treeNamesLen "-" 1}}
-
- {{StringUtils.EllipsisString .Repository.Name 30}}
- {{- range $i, $v := .TreeNames -}}
- /
- {{- if eq $i $treeNameIdxLast -}}
- {{$v}}
-
- {{- else -}}
- {{$p := index $.Paths $i}}{{$v}}
- {{- end -}}
- {{- end -}}
-
- {{end}}
-
+
+ {{if $hasTreeSidebar}}
+
- {{if .IsViewFile}}
- {{template "repo/view_file" .}}
- {{else if .IsBlame}}
- {{template "repo/blame" .}}
- {{else}}{{/* IsViewDirectory */}}
- {{if $isTreePathRoot}}
- {{template "repo/code/upstream_diverging_info" .}}
- {{end}}
- {{template "repo/view_list" .}}
- {{if and .ReadmeExist (or .IsMarkup .IsPlainText)}}
- {{template "repo/view_file" .}}
- {{end}}
- {{end}}
+ {{template "repo/view_file_tree_sidebar" .}}
+ {{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}}
-
-
+ {{template "repo/home_content" .}}
{{if $showSidebar}}
diff --git a/templates/repo/home_branch_dropdown.tmpl b/templates/repo/home_branch_dropdown.tmpl
new file mode 100644
index 0000000000000..f720100c7d273
--- /dev/null
+++ b/templates/repo/home_branch_dropdown.tmpl
@@ -0,0 +1,12 @@
+{{template "repo/branch_dropdown" dict
+ "Repository" .ctxData.Repository
+ "ShowTabBranches" true
+ "ShowTabTags" true
+ "CurrentRefType" .ctxData.RefFullName.RefType
+ "CurrentRefShortName" .ctxData.RefFullName.ShortName
+ "CurrentTreePath" .ctxData.TreePath
+ "RefLinkTemplate" "{RepoLink}/src/{RefTypeNameSubURL}/{TreePath}"
+ "AllowCreateNewRef" .ctxData.CanCreateBranch
+ "ShowViewAllRefsEntry" true
+ "ContainerClasses" .containerClasses
+}}
diff --git a/templates/repo/home_content.tmpl b/templates/repo/home_content.tmpl
new file mode 100644
index 0000000000000..d8e1890d43473
--- /dev/null
+++ b/templates/repo/home_content.tmpl
@@ -0,0 +1,101 @@
+{{$treeNamesLen := len .TreeNames}}
+{{$isTreePathRoot := eq $treeNamesLen 0}}
+{{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}}
+{{$hasTreeSidebar := not $isTreePathRoot}}
+{{$showTreeSidebar := .RepoPreferences.ShowFileViewTreeSidebar}}
+
+{{template "repo/sub_menu" .}}
+
+
+{{if .IsViewFile}}
+ {{template "repo/view_file" .}}
+{{else if .IsBlame}}
+ {{template "repo/blame" .}}
+{{else}}{{/* IsViewDirectory */}}
+ {{if $isTreePathRoot}}
+ {{template "repo/code/upstream_diverging_info" .}}
+ {{end}}
+ {{template "repo/view_list" .}}
+ {{if and .ReadmeExist (or .IsMarkup .IsPlainText)}}
+ {{template "repo/view_file" .}}
+ {{end}}
+{{end}}
diff --git a/templates/repo/view_file_tree_sidebar.tmpl b/templates/repo/view_file_tree_sidebar.tmpl
new file mode 100644
index 0000000000000..c2ee6336c393b
--- /dev/null
+++ b/templates/repo/view_file_tree_sidebar.tmpl
@@ -0,0 +1,17 @@
+
+ {{if $hasTreeSidebar}}
+
+ {{end}}
+ {{template "repo/home_branch_dropdown" (dict "ctxData" .)}}
+ {{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}}
+
+ {{svg "octicon-git-pull-request"}}
+
+ {{end}}
+
+
+ {{if $isTreePathRoot}}
+ {{ctx.Locale.Tr "repo.find_file.go_to_file"}}
+ {{end}}
+
+ {{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
+
+ {{end}}
+
+ {{if and $isTreePathRoot .Repository.IsTemplate}}
+
+ {{ctx.Locale.Tr "repo.use_template"}}
+
+ {{end}}
+
+ {{if not $isTreePathRoot}}
+ {{$treeNameIdxLast := Eval $treeNamesLen "-" 1}}
+
+ {{StringUtils.EllipsisString .Repository.Name 30}}
+ {{- range $i, $v := .TreeNames -}}
+ /
+ {{- if eq $i $treeNameIdxLast -}}
+ {{$v}}
+
+ {{- else -}}
+ {{$p := index $.Paths $i}}{{$v}}
+ {{- end -}}
+ {{- 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}}
+
+
+
+
+
+ Files
+
+
+
+
+
diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css
index 65005e2263537..db171e7974b29 100644
--- a/web_src/css/repo/home.css
+++ b/web_src/css/repo/home.css
@@ -50,6 +50,65 @@
}
}
+.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;
+ margin-left: 1rem;
+}
+
+#view-file-tree.is-loading {
+ aspect-ratio: 5.415; /* the size is about 790 x 145 */
+}
+
+.repo-grid-tree-sidebar .repo-view-file-tree-sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ max-height: 100vh;
+ overflow: hidden;
+}
+
+.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;
+}
+
+.repo-grid-tree-sidebar .view-file-tree-sidebar-bottom {
+ flex: 1;
+ overflow: auto;
+}
+
+.repo-grid-tree-sidebar .repo-button-row {
+ margin-top: 0 !important;
+}
+
+@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 0000000000000..3337f6e9e553e
--- /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 0000000000000..cb94c1f7f1d06
--- /dev/null
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts
index 3162557b9b21f..c0cd8ee359db9 100644
--- a/web_src/js/features/common-button.ts
+++ b/web_src/js/features/common-button.ts
@@ -155,12 +155,16 @@ function onShowModalClick(e) {
}
export function initGlobalButtons(): void {
+ initTargetButtons(document as ParentNode);
+}
+
+export function initTargetButtons(target: ParentNode): void {
// There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form.
// However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission.
// There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
- addDelegatedEventListener(document, 'click', 'form button.ui.cancel.button', (_ /* el */, e) => e.preventDefault());
+ queryElems(target, 'form button.ui.cancel.button', (el) => el.addEventListener('click', (e) => e.preventDefault()));
- queryElems(document, '.show-panel', (el) => el.addEventListener('click', onShowPanelClick));
- queryElems(document, '.hide-panel', (el) => el.addEventListener('click', onHidePanelClick));
- queryElems(document, '.show-modal', (el) => el.addEventListener('click', onShowModalClick));
+ queryElems(target, '.show-panel', (el) => el.addEventListener('click', onShowPanelClick));
+ queryElems(target, '.hide-panel', (el) => el.addEventListener('click', onHidePanelClick));
+ queryElems(target, '.show-modal', (el) => el.addEventListener('click', onShowModalClick));
}
diff --git a/web_src/js/features/common-page.ts b/web_src/js/features/common-page.ts
index 56c5915b6dbf8..058702785d072 100644
--- a/web_src/js/features/common-page.ts
+++ b/web_src/js/features/common-page.ts
@@ -28,8 +28,13 @@ export function initFootLanguageMenu() {
}
export function initGlobalDropdown() {
+ initTargetDropdown(document.body);
+}
+
+export function initTargetDropdown(target: Element) {
// Semantic UI modules.
- const $uiDropdowns = fomanticQuery('.ui.dropdown');
+ const $target = fomanticQuery(target);
+ const $uiDropdowns = $target.find('.ui.dropdown');
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
$uiDropdowns.filter(':not(.custom)').dropdown({hideDividers: 'empty'});
diff --git a/web_src/js/features/copycontent.ts b/web_src/js/features/copycontent.ts
index af867463b2998..554c9b7f47535 100644
--- a/web_src/js/features/copycontent.ts
+++ b/web_src/js/features/copycontent.ts
@@ -6,7 +6,11 @@ import {GET} from '../modules/fetch.ts';
const {i18n} = window.config;
export function initCopyContent() {
- const btn = document.querySelector('#copy-content');
+ initTargetCopyContent(document);
+}
+
+export function initTargetCopyContent(target: ParentNode) {
+ const btn = target.querySelector('#copy-content');
if (!btn || btn.classList.contains('disabled')) return;
btn.addEventListener('click', async () => {
diff --git a/web_src/js/features/repo-commit.ts b/web_src/js/features/repo-commit.ts
index 56493443d9068..3d19905934a35 100644
--- a/web_src/js/features/repo-commit.ts
+++ b/web_src/js/features/repo-commit.ts
@@ -2,7 +2,11 @@ import {createTippy} from '../modules/tippy.ts';
import {toggleElem} from '../utils/dom.ts';
export function initRepoEllipsisButton() {
- for (const button of document.querySelectorAll('.js-toggle-commit-body')) {
+ initTargetRepoEllipsisButton(document);
+}
+
+export function initTargetRepoEllipsisButton(target: ParentNode) {
+ for (const button of target.querySelectorAll('.js-toggle-commit-body')) {
button.addEventListener('click', function (e) {
e.preventDefault();
const expanded = this.getAttribute('aria-expanded') === 'true';
diff --git a/web_src/js/features/repo-legacy.ts b/web_src/js/features/repo-legacy.ts
index 33f02be865eff..cf51585c57f2c 100644
--- a/web_src/js/features/repo-legacy.ts
+++ b/web_src/js/features/repo-legacy.ts
@@ -21,12 +21,6 @@ import {initRepoNew} from './repo-new.ts';
import {createApp} from 'vue';
import RepoBranchTagSelector from '../components/RepoBranchTagSelector.vue';
-function initRepoBranchTagSelector(selector: string) {
- for (const elRoot of document.querySelectorAll(selector)) {
- createApp(RepoBranchTagSelector, {elRoot}).mount(elRoot);
- }
-}
-
export function initBranchSelectorTabs() {
const elSelectBranches = document.querySelectorAll('.ui.dropdown.select-branch');
for (const elSelectBranch of elSelectBranches) {
@@ -39,11 +33,17 @@ export function initBranchSelectorTabs() {
}
}
+export function initTargetRepoBranchTagSelector(target: ParentNode, selector: string = '.js-branch-tag-selector') {
+ for (const elRoot of target.querySelectorAll(selector)) {
+ createApp(RepoBranchTagSelector, {elRoot}).mount(elRoot);
+ }
+}
+
export function initRepository() {
const pageContent = document.querySelector('.page-content.repository');
if (!pageContent) return;
- initRepoBranchTagSelector('.js-branch-tag-selector');
+ initTargetRepoBranchTagSelector(document);
initRepoCommentFormAndSidebar();
// Labels
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 0000000000000..7fc97137134ce
--- /dev/null
+++ b/web_src/js/features/repo-view-file-tree-sidebar.ts
@@ -0,0 +1,100 @@
+import {createApp, ref} from 'vue';
+import {toggleElem} from '../utils/dom.ts';
+import {GET, PUT} from '../modules/fetch.ts';
+import ViewFileTree from '../components/ViewFileTree.vue';
+import {initTargetRepoBranchTagSelector} from './repo-legacy.ts';
+import {initTargetDropdown} from './common-page.ts';
+import {initTargetRepoEllipsisButton} from './repo-commit.ts';
+import {initTargetPdfViewer} from '../render/pdf.ts';
+import {initTargetButtons} from './common-button.ts';
+import {initTargetCopyContent} from './copycontent.ts';
+
+async function toggleSidebar(visibility, isSigned) {
+ const sidebarEl = document.querySelector('.repo-view-file-tree-sidebar');
+ const showBtnEl = document.querySelector('.show-tree-sidebar-button');
+ 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);
+
+ if (!isSigned) return;
+
+ // save to session
+ await PUT('/repo/preferences', {
+ data: {
+ show_file_view_tree_sidebar: visibility,
+ },
+ });
+}
+
+async function loadChildren(item, recursive?: boolean) {
+ const fileTree = document.querySelector('#view-file-tree');
+ const apiBaseUrl = fileTree.getAttribute('data-api-base-url');
+ const refType = fileTree.getAttribute('data-current-ref-type');
+ const refName = fileTree.getAttribute('data-current-ref-short-name');
+ const response = await GET(`${apiBaseUrl}/tree/${item ? item.path : ''}?ref_type=${refType}&ref_name=${refName}&recursive=${recursive ?? false}`);
+ const json = await response.json();
+ if (json instanceof Array) {
+ return json.map((i) => ({
+ name: i.name,
+ type: i.type,
+ path: i.path,
+ children: i.children,
+ }));
+ }
+ return null;
+}
+
+async function loadContent() {
+ // load content by path (content based on home_content.tmpl)
+ const response = await GET(`${window.location.href}?only_content=true`);
+ const contentEl = document.querySelector('.repo-home-filelist');
+ contentEl.innerHTML = await response.text();
+ reloadContentScript(contentEl);
+}
+
+function reloadContentScript(contentEl: Element) {
+ contentEl.querySelector('.show-tree-sidebar-button').addEventListener('click', () => {
+ toggleSidebar(true, document.querySelector('.repo-view-file-tree-sidebar').hasAttribute('data-is-signed'));
+ });
+ initTargetButtons(contentEl);
+ initTargetDropdown(contentEl);
+ initTargetPdfViewer(contentEl);
+ initTargetRepoBranchTagSelector(contentEl);
+ initTargetRepoEllipsisButton(contentEl);
+ initTargetCopyContent(contentEl);
+}
+
+export async function initViewFileTreeSidebar() {
+ const sidebarElement = document.querySelector('.repo-view-file-tree-sidebar');
+ if (!sidebarElement) return;
+
+ const isSigned = sidebarElement.hasAttribute('data-is-signed');
+
+ document.querySelector('.hide-tree-sidebar-button').addEventListener('click', () => {
+ toggleSidebar(false, isSigned);
+ });
+ document.querySelector('.repo-home-filelist .show-tree-sidebar-button').addEventListener('click', () => {
+ toggleSidebar(true, isSigned);
+ });
+
+ const fileTree = document.querySelector('#view-file-tree');
+ const baseUrl = fileTree.getAttribute('data-api-base-url');
+ const treePath = fileTree.getAttribute('data-tree-path');
+ const refType = fileTree.getAttribute('data-current-ref-type');
+ const refName = fileTree.getAttribute('data-current-ref-short-name');
+ const refString = (refType ? (`/${refType}`) : '') + (refName ? (`/${refName}`) : '');
+
+ const selectedItem = ref(treePath);
+
+ const files = await loadChildren({path: treePath}, true);
+
+ fileTree.classList.remove('is-loading');
+ const fileTreeView = createApp(ViewFileTree, {files, selectedItem, loadChildren, loadContent: (item) => {
+ window.history.pushState(null, null, `${baseUrl}/src${refString}/${item.path}`);
+ selectedItem.value = item.path;
+ loadContent();
+ }});
+ fileTreeView.mount(fileTree);
+}
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index 022be033da25f..59bdf87c85171 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 {initRepoCodeView} from './features/repo-code.ts';
import {initSshKeyFormParser} from './features/sshkey-helper.ts';
@@ -192,6 +193,7 @@ onDomReady(() => {
initRepoRelease,
initRepoReleaseNew,
initRepoTopicBar,
+ initViewFileTreeSidebar,
initRepoWikiForm,
initRepository,
initRepositoryActionView,
diff --git a/web_src/js/render/pdf.ts b/web_src/js/render/pdf.ts
index f31f161e6e8e2..5bed6f7bab842 100644
--- a/web_src/js/render/pdf.ts
+++ b/web_src/js/render/pdf.ts
@@ -1,7 +1,11 @@
import {htmlEscape} from 'escape-goat';
export async function initPdfViewer() {
- const els = document.querySelectorAll('.pdf-content');
+ initTargetPdfViewer(document);
+}
+
+export async function initTargetPdfViewer(target: ParentNode) {
+ const els = target.querySelectorAll('.pdf-content');
if (!els.length) return;
const pdfobject = await import(/* webpackChunkName: "pdfobject" */'pdfobject');