From b4622fef0fa5bf38f74f76573fbf931b907d368e Mon Sep 17 00:00:00 2001 From: aadrian Date: Sat, 3 Oct 2026 13:27:32 +0200 Subject: [PATCH 1/3] Fix the file tree in the collapsed sidebar, stop caching it in localStorage and highlight the current file. --- scripts/components/Directory.js | 54 +++++++++++++------ scripts/components/File.js | 8 ++- scripts/components/FileNodes.js | 42 ++++++++++++--- scripts/components/Root.js | 8 ++- scripts/components/scroll.js | 22 ++++++++ .../explorer/assets/plugin-explorer.css | 16 ++++++ 6 files changed, 124 insertions(+), 26 deletions(-) create mode 100644 scripts/components/scroll.js diff --git a/scripts/components/Directory.js b/scripts/components/Directory.js index 9d18f98..69ad299 100644 --- a/scripts/components/Directory.js +++ b/scripts/components/Directory.js @@ -3,19 +3,22 @@ import ReactMixin from 'react-mixin'; import LocalStorageMixin from 'react-localstorage'; import request from 'superagent'; import FileNodes from './FileNodes'; +import { stopAutoScroll } from './scroll'; export default class Directory extends React.Component { static get propTypes() { return { url: PropTypes.string, - name: PropTypes.string + name: PropTypes.string, + current: PropTypes.bool }; } static get getDefaultProps() { return { url: '', - name: '' + name: '', + current: false }; } @@ -27,11 +30,40 @@ export default class Directory extends React.Component { }; } + // also runs when the expanded flag is restored from localStorage + componentDidUpdate(prevProps, prevState) { + if (this.state.expanded && !prevState.expanded) { + request + .get(this.getExploreUrl()) + .end((err, res) => { + if (err || !this.state.expanded) { + return; + } + try { + this.setState({ children: JSON.parse(res.text) }); + } catch (e) { + return; + } + }); + } + } + getLocalStorageKey() { return this.props.url; } - toggleFolder(path) { + // persist only the expanded flag; children are always loaded from the server + // eslint-disable-next-line class-methods-use-this + getStateFilterKeys() { + return ['expanded']; + } + + getExploreUrl() { + return this.props.url; + } + + toggleFolder() { + stopAutoScroll(); if (this.state.expanded) { this.setState({ expanded: false, @@ -39,26 +71,14 @@ export default class Directory extends React.Component { }); } else { this.setState({ expanded: true }); - request - .get(path) - .end((err, res) => { - if (err) { - return; - } - try { - this.setState({ children: JSON.parse(res.text) }); - } catch (e) { - return; - } - }); } } render() { const arrow = this.state.expanded ? 'octicon octicon-chevron-down' : 'octicon octicon-chevron-right'; return ( -
  • - - Files + Files
    diff --git a/scripts/components/scroll.js b/scripts/components/scroll.js new file mode 100644 index 0000000..b1af2f3 --- /dev/null +++ b/scripts/components/scroll.js @@ -0,0 +1,22 @@ +// Keeps the current file or folder visible while the tree restores its expanded folders +// after a page load. Stops once the user expands or collapses a folder, so the tree +// doesn't jump back to it. +let enabled = true; + +export function stopAutoScroll() { + enabled = false; +} + +// scroll only the tree (not the page) +export function scrollToCurrent(element) { + const tree = enabled && element && element.closest('.file-tree'); + const current = tree && tree.querySelector('li.current'); + if (!current) { + return; + } + const item = current.firstElementChild.getBoundingClientRect(); + const view = tree.getBoundingClientRect(); + if (item.top < view.top || item.bottom > view.bottom) { + tree.scrollTop += (item.top - view.top) - ((view.height - item.height) / 2); + } +} diff --git a/src/main/resources/explorer/assets/plugin-explorer.css b/src/main/resources/explorer/assets/plugin-explorer.css index 876b132..f48f264 100644 --- a/src/main/resources/explorer/assets/plugin-explorer.css +++ b/src/main/resources/explorer/assets/plugin-explorer.css @@ -78,3 +78,19 @@ li.active .file-tree { .file-node > a:hover { opacity: 0.6; } +/* the file or folder shown on the current page */ +.file-tree li.current > a, +.file-tree li.current > .folder-expander { + font-weight: bold; +} +.file-tree li.current > a { + background-color: #e8f1f8; +} +/* collapsed (mini) sidebar: show only the icon, like the core menu items */ +@media (min-width: 768px) { + .sidebar-mini.sidebar-collapse .tree-node > .root-expander, + .sidebar-mini.sidebar-collapse .tree-node > .file-tree, + .sidebar-mini.sidebar-collapse .submenu-files > span { + display: none; + } +} From 17288c095e4c763cc209647e8c1d2ba3d8fcace3 Mon Sep 17 00:00:00 2001 From: aadrian Date: Sat, 3 Oct 2026 13:28:31 +0200 Subject: [PATCH 2/3] Fix the ESLint errors. --- scripts/components/Directory.js | 1 - scripts/components/File.js | 1 - scripts/components/FileNodes.js | 7 +++++-- scripts/components/Root.js | 1 - 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/components/Directory.js b/scripts/components/Directory.js index 69ad299..ac1d1bb 100644 --- a/scripts/components/Directory.js +++ b/scripts/components/Directory.js @@ -6,7 +6,6 @@ import FileNodes from './FileNodes'; import { stopAutoScroll } from './scroll'; export default class Directory extends React.Component { - static get propTypes() { return { url: PropTypes.string, diff --git a/scripts/components/File.js b/scripts/components/File.js index d5bcff0..5651633 100644 --- a/scripts/components/File.js +++ b/scripts/components/File.js @@ -1,7 +1,6 @@ import React, { PropTypes } from 'react'; export default class File extends React.Component { - static get propTypes() { return { url: PropTypes.string.isRequired, diff --git a/scripts/components/FileNodes.js b/scripts/components/FileNodes.js index 09157b6..2aa79cb 100644 --- a/scripts/components/FileNodes.js +++ b/scripts/components/FileNodes.js @@ -16,10 +16,13 @@ function normalizePath(path) { } export default class FileNodes extends React.Component { - static get propTypes() { return { - data: PropTypes.array.isRequired, + data: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + isDirectory: PropTypes.bool.isRequired, + })).isRequired, }; } diff --git a/scripts/components/Root.js b/scripts/components/Root.js index 182649a..fcc7396 100644 --- a/scripts/components/Root.js +++ b/scripts/components/Root.js @@ -3,7 +3,6 @@ import FileNodes from './FileNodes'; import Directory from './Directory'; export default class Root extends Directory { - componentWillMount() { const logo = document.querySelector('header.main-header a.logo'); if (!logo) { From 807d4d4b0c945a56eb9c498901c7cc83985e6fcb Mon Sep 17 00:00:00 2001 From: aadrian Date: Sun, 4 Oct 2026 12:22:48 +0200 Subject: [PATCH 3/3] Adapt to the upstream edge-case handling and tests. --- scripts/components/Directory.js | 2 +- test/components/Directory.spec.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/components/Directory.js b/scripts/components/Directory.js index ac1d1bb..3006192 100644 --- a/scripts/components/Directory.js +++ b/scripts/components/Directory.js @@ -41,7 +41,7 @@ export default class Directory extends React.Component { try { this.setState({ children: JSON.parse(res.text) }); } catch (e) { - return; + // not JSON, e.g. a sign-in page after the session expired } }); } diff --git a/test/components/Directory.spec.js b/test/components/Directory.spec.js index 6723080..fa608a6 100644 --- a/test/components/Directory.spec.js +++ b/test/components/Directory.spec.js @@ -34,9 +34,11 @@ describe('Directory', () => { }); it('collapses without issuing a network call on second toggle', () => { - sinon.stub(request, 'get'); + sinon.stub(request, 'get').returns({ end: () => {} }); const wrapper = shallow(); + // expanding loads the children, like restoring the expanded flag from localStorage wrapper.setState({ expanded: true, children: [{ name: 'x', url: '/x', isDirectory: false }] }); + request.get.resetHistory(); wrapper.instance().toggleFolder('/owner/repo/explore/main/src'); wrapper.update();