+
{{template "base/alert" .}}
{{if .Repository.IsArchived}}
@@ -16,112 +17,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" .}}
-
- ('.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..22f7978f3053d
--- /dev/null
+++ b/web_src/js/features/repo-view-file-tree-sidebar.ts
@@ -0,0 +1,127 @@
+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 {initMarkupContent} from '../markup/content.ts';
+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 refTypeNameSubURL = fileTree.getAttribute('data-current-ref-type-name-sub-url');
+ const response = await GET(`${apiBaseUrl}/tree/${refTypeNameSubURL}/${item ? item.path : ''}?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,
+ sub_module_url: i.sub_module_url,
+ 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'));
+ });
+ initMarkupContent();
+ 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);
+
+ window.addEventListener('popstate', () => {
+ selectedItem.value = extractPath(window.location.href);
+ loadContent();
+ });
+}
+
+function extractPath(url) {
+ // Create a URL object
+ const urlObj = new URL(url);
+
+ // Get the pathname part
+ const path = urlObj.pathname;
+
+ // Define a regular expression to match "/{param1}/{param2}/src/{branch}/{main}/"
+ const regex = /^\/[^/]+\/[^/]+\/src\/[^/]+\/[^/]+\//;
+
+ // Use RegExp#exec() method to match the path
+ const match = regex.exec(path);
+ if (match) {
+ return path.substring(match[0].length);
+ }
+
+ // If the path does not match, return the original path
+ return path;
+}
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index b89e5960470e1..25c6a1648521e 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -32,6 +32,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';
@@ -191,6 +192,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');
diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts
index b193afb255993..0208ff0e98ca0 100644
--- a/web_src/js/svg.ts
+++ b/web_src/js/svg.ts
@@ -29,6 +29,7 @@ import octiconFile from '../../public/assets/img/svg/octicon-file.svg';
import octiconFileDirectoryFill from '../../public/assets/img/svg/octicon-file-directory-fill.svg';
import octiconFileDirectoryOpenFill from '../../public/assets/img/svg/octicon-file-directory-open-fill.svg';
import octiconFileSubmodule from '../../public/assets/img/svg/octicon-file-submodule.svg';
+import octiconFileSymlinkFile from '../../public/assets/img/svg/octicon-file-symlink-file.svg';
import octiconFilter from '../../public/assets/img/svg/octicon-filter.svg';
import octiconGear from '../../public/assets/img/svg/octicon-gear.svg';
import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.svg';
@@ -107,6 +108,7 @@ const svgs = {
'octicon-file-directory-fill': octiconFileDirectoryFill,
'octicon-file-directory-open-fill': octiconFileDirectoryOpenFill,
'octicon-file-submodule': octiconFileSubmodule,
+ 'octicon-file-symlink-file': octiconFileSymlinkFile,
'octicon-filter': octiconFilter,
'octicon-gear': octiconGear,
'octicon-git-branch': octiconGitBranch,
- {{- /* for repo home (default branch) and /owner/repo/src/{RefType}/{RefShortName} */ -}}
- {{- template "repo/branch_dropdown" dict
- "Repository" .Repository
- "ShowTabBranches" true
- "ShowTabTags" true
- "CurrentRefType" .RefFullName.RefType
- "CurrentRefShortName" .RefFullName.ShortName
- "CurrentTreePath" .TreePath
- "RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}"
- "AllowCreateNewRef" .CanCreateBranch
- "ShowViewAllRefsEntry" true
- -}}
- {{if and .CanCompareOrPull .RefFullName.IsBranch (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 .RefFullName.IsBranch (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 .TreeNames}}
+
- {{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..cef3b4bdf2993
--- /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/{RefType}/{RefShortName}/{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..291ceb50cdca7
--- /dev/null
+++ b/templates/repo/home_content.tmpl
@@ -0,0 +1,98 @@
+{{$isTreePathRoot := not .TreeNames}}
+{{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}}
+
+{{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..48b8edfaa5235
--- /dev/null
+++ b/templates/repo/view_file_tree_sidebar.tmpl
@@ -0,0 +1,18 @@
+
+ {{if not $isTreePathRoot}}
+
+ {{end}}
+ {{template "repo/home_branch_dropdown" (dict "ctxData" .)}}
+ {{if and .CanCompareOrPull .RefFullName.IsBranch (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 .RefFullName.IsBranch (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 (len .TreeNames) "-" 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 96551979ea21a..2a3764820be2c 100644
--- a/web_src/css/repo/home.css
+++ b/web_src/css/repo/home.css
@@ -49,6 +49,68 @@
}
}
+.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;
+ position: sticky;
+ top: 14px;
+ z-index: 8;
+}
+
+.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..cbfe68ac8e573
--- /dev/null
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts
index 7aebdd8dd5be5..284c590fca62d 100644
--- a/web_src/js/features/common-button.ts
+++ b/web_src/js/features/common-button.ts
@@ -160,7 +160,11 @@ export function initGlobalButtons(): void {
// 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(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));
+ initTargetButtons(document);
+}
+
+export function initTargetButtons(target: ParentNode): void {
+ 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 4bc9281a35e4f..0bc14c3bff5af 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 8994a57f4a8af..f0a2f878d84f6 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