Skip to content

feat(nav): back/forward navigation with tree auto-reveal#47

Open
DanR978 wants to merge 2 commits into
ace-technologies-inc:mainfrom
DanR978:feat/nav-history
Open

feat(nav): back/forward navigation with tree auto-reveal#47
DanR978 wants to merge 2 commits into
ace-technologies-inc:mainfrom
DanR978:feat/nav-history

Conversation

@DanR978

@DanR978 DanR978 commented Jul 10, 2026

Copy link
Copy Markdown

What this does

Adds Back and Forward buttons to the toolbar. They step through the objects you
have selected, the same way a browser steps through pages you have visited.

The sidebar tree now follows the selection. When something is selected, the tree
opens the folders above it and scrolls it into view.

Objects get selected from the relationship graph, from search results, and from
the tree itself. All of those go into the history now, and all of them scroll
the tree.

History (explorer.ts, Toolbar.tsx)

selectItem() pushes the selection onto a list, capped at 50 entries. Selecting
the same node twice does not add a second entry, and going back and then
selecting something new drops the forward entries, like a browser does.

goBack() and goForward() set the state directly instead of calling
selectItem(). If they called it, going back would push that entry on again and
you could never leave the end of the list.

A node is only visible if the folders above it are open, so back and forward
re-open that path. Search and the relationship graph already do the same thing
before they select something.

History is kept in memory only, and reset() clears it on disconnect, so it
cannot carry over to another server.

The two side buttons on a mouse (X1 and X2) work too. That part is Electron only,
since in the web build those buttons belong to the browser.

Scrolling (TreeView.tsx, VirtualObjectRows.tsx)

TreeView owns this. An obj: id names an object, not a spot in the tree, and
the same object can be rendered both under Namespaces > ObjectType and in the
flat Objects list. So the id alone does not say which row to scroll to.

It looks for rows in the DOM with the selected class. If one of them is already
on screen it does nothing. Otherwise it centers the first one. If none exist in
the DOM, it asks the virtualized Objects list to scroll by index instead. That
works because all rows are the same height, so the virtualizer knows where a row
is even when it has not been rendered.

Both paths only scroll up and down, never sideways.

Scrolling only runs when the selection changes, so expanding another node, or the
30 second background refresh, will not move the tree on you.

Testing

npm run typecheck passes. (npm run lint fails on main too, since ESLint is
installed without a config file.)

I ran the store directly to check the history rules: order, back and forward
stopping at the ends, forward entries dropped after a new selection, no
duplicates, the 50 entry cap, reset() clearing it, the right folders re-opening
for each id type, and an object that lists itself as its own parent not looping
forever.

For the scrolling I read the @tanstack/virtual-core source to confirm that
scrollToIndex only changes the vertical scroll, and that every row has a
position even when it has not been rendered.

Known limitation (already on main)

Rows in the flat Objects list use obj:${elementId} as their id no matter where
they sit, and an object shows up both at the top level and under its composition
parent. Selecting one highlights all of the copies. This PR does not fix that,
but it handles it: if any copy is on screen, nothing scrolls. Giving node ids a
path would be a good follow up.

Risk

No new dependencies and no API changes. Scrolling does nothing when the node is
already visible, and the history does nothing until you press a button.

DanR978 added 2 commits July 9, 2026 23:32
Adds a bounded selection-history stack to the explorer store and two arrow
buttons to the toolbar, so navigating away from an object -- by clicking a
relationship-graph node, picking a search result, or clicking a tree row --
can be undone.

selectItem() pushes onto the stack, de-duping a repeated selection of the
current node and truncating the forward entries on a new navigation, as a
browser does. goBack/goForward restore a selection by setting state directly
rather than calling selectItem, which would push the entry back on and trap
the cursor at the end of the stack.

A node is only visible once the folders and ancestors above it are expanded,
so goBack/goForward re-expand that path -- the same expansion SearchModal and
RelationshipGraph perform before they select. Without it, going back to a node
inside a branch the user has since collapsed would update the detail pane and
leave the tree inert.

History is session-scoped (the explorer store is not persisted) and cleared by
reset() on disconnect, so it never leaks across servers.

Also handles the X1/X2 side buttons found on most mice. Electron only: in the
web build those buttons drive the browser's own history, and taking them over
would strand the SPA.
Back/forward (and search, and relationship-graph clicks) expand the path to the
selected node, but nothing brought it on screen -- a node could land highlighted
below the fold. Center it when no copy of it is already visible.

Reveal is owned entirely by TreeView. An `obj:` id names an object, not a tree:
objects are rendered both under Namespaces -> ObjectType as plain DOM and in the
windowed Objects list, so the id prefix cannot say which row to scroll to. So ask
the DOM first -- whichever copies are mounted carry the selected style -- and fall
back to the virtualized list only when no DOM row exists, via an imperative
handle. That fallback works because uniform row heights give the virtualizer a
measurement for every index, mounted or not.

The same object can be mounted more than once (the flat list shows it at top
level and again under its composition parent). If any copy is on screen, do
nothing; otherwise center the first.

Both paths scroll vertically only -- scrollToIndex writes scrollTop and never
scrollLeft, and the DOM path adjusts scrollTop directly rather than using
scrollIntoView, which would drag a horizontally-scrolled tree sideways.

Reveal is keyed on the selection changing, guarded by a single ref, so expanding
an unrelated node or the 30s background poll (which replaces allObjects) never
scrolls the tree away from where the user left it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant