Skip to content

Add directory tree UI (#156) and fix directory-filter performance#231

Merged
YusukeHirao merged 10 commits into
devfrom
feature/156-directory-tree-ui
Jul 24, 2026
Merged

Add directory tree UI (#156) and fix directory-filter performance#231
YusukeHirao merged 10 commits into
devfrom
feature/156-directory-tree-ui

Conversation

@YusukeHirao

Copy link
Copy Markdown
Member

Summary

  • Add folder icons (📁/📂), a larger SVG expand chevron moved after the label/count, and hide the toggle on a host's root row (always expanded)
  • Add "collapse to depth" (numeric input + button) and "sort order" (path / most pages first / fewest pages first, select + button) controls above the directory tree
  • Move all directory-tree UI state (expand/collapse overrides, collapse depth, sort order) into the URL, matching every other list view's convention — selecting a node navigates to the Pages view, which previously reset all of this state on the back button
  • Show a one-line notice above the Pages table when a directory filter is active, since it has no control of its own there (unlike the other filters shown in the filter bar)

Performance fix (found via manual testing against a real ~450k-page archive)

  • The directory tree's node-click hand-off to /pages?directory=... exposed a pre-existing performance cliff: the legacy directory filter is a substring LIKE '%dir%' against url_refs.url, which can't seek an index. Against the real archive, a directory-filtered request did not return within 2 minutes.
  • Fix: viewer_pages.path_sort_key was already stored for exactly this range-scan use case but had no index and no reader. Added the vp_path index and a directory option to listViewerPages/applyViewerPagesFilters — a >=/< range bounded by a maximal-codepoint sentinel, not a LIKE, so it matches the whole subtree while still seeking the index.
  • Bumped VIEWER_READ_MODEL_SCHEMA_VERSION to 18; existing archives fall back to the legacy path until their read model rebuilds (nitpicker viewer-build, or the included scripts/migrate-to-0.14.mjs).
  • Measured: the same request that previously did not return within 2 minutes now completes in under 100ms.

Test plan

  • yarn lint — 0 errors
  • yarn build — all 13 packages
  • yarn test — full unit suite, 486 files / 3432 tests passed
  • yarn workspace @nitpicker/viewer test:e2e:directory-tree — 12/12 passed
  • yarn workspace @nitpicker/viewer test:e2e — 49/49 passed (regression check on the shared fixture)
  • Verified against a real ~450k-page archive: directory-filtered /api/pages request went from >2min (no response) to ~84ms
  • Manual browser check of folder icons, collapse-to-depth, sort order, back-button state persistence, and the Pages-view filter notice

…stant

Move the hardcoded initial-load depth cutoff out of get-directory-tree.ts
into its own dependency-free module and publish it via a browser-safe
sub-export, so the viewer frontend's default-expanded-by-depth policy reads
the same value instead of duplicating it as an independent literal.
Implement the frontend for issue #156's directory tree view — the backend
read model and API landed in #107 with no frontend surface yet. Renders the
initial depth <= 3 payload as an already-expanded tree per root host,
expands boundary nodes on demand via /api/directory-tree/children (no
hardcoded depth cutoff — driven purely by whether a node's children are
already known), and shows a selected directory's direct pages in a paginated
side panel reusing the existing DataTable virtual mode.

Adds a dedicated E2E fixture that builds the viewer read model explicitly
(the shared fixture doesn't), so directory-tree.spec.ts runs against real
data without touching the other six views' fast-path/legacy dispatch in the
shared-fixture suite.
…odel

Add direct_html_page_count/descendant_html_page_count alongside the existing
directPageCount/descendantPageCount columns, classified via
classifyContentType at build time. The unfiltered counts include every
crawled resource under a directory (images, PDFs, etc.), which is misleading
when the viewer's tree UI labels the number "pages" — the new columns give
callers an accurate HTML-only count without an extra query.

Bumps VIEWER_READ_MODEL_SCHEMA_VERSION to 17.
…s to /pages

Drop the in-place direct-pages panel (DirectoryPagesPanel,
useDirectoryTreePagesInfinite) in favor of navigating to /pages?directory=
<path>&contentTypeCategory=html on node select. The Pages view's directory
filter already covers the whole subtree with full sort/filter/pagination,
and contentTypeCategory=html keeps the destination to actual pages, not
every crawled resource under that path (images, PDFs, etc.) — wires a
one-line directory param read into pages-view.tsx to support it.

Redesigns the tree's visual style: page-level scroll instead of a boxed
pane, badges pulled closer to node names as a pill (no more far-apart
justify-content: space-between), and indent guide lines replacing the
even/odd row shading that would have visibly shifted on every expand/
collapse. Badges now read descendantHtmlPageCount instead of the
unfiltered descendantPageCount, matching the query-layer change.
Show a folder icon (open/closed based on expanded state) before the
directory name, move the expand/collapse control after the name and page
count instead of leading the row, and render it as an SVG chevron (larger,
rotates on expand) instead of a plain text glyph. A host's root row no
longer shows the toggle at all — it is always expanded, since collapsing
the one row every other row nests under would hide the whole tree behind a
single click.
path_sort_key was already stored for exactly this range-scan use case but had
no index and no reader. Add the vp_path index and a directory option to
listViewerPages/applyViewerPagesFilters — a >=/< range on path_sort_key
bounded by a maximal-codepoint sentinel, not a LIKE, so it matches the whole
subtree while still seeking the index regardless of collation settings.

Bump VIEWER_READ_MODEL_SCHEMA_VERSION to 18 for the new index; existing
archives fall back to the legacy path until their read model rebuilds.

Measured against a 451k-page archive: a directory-filtered /api/pages
request that previously did not return within 2 minutes (the legacy path's
substring LIKE against url_refs.url can't seek an index) now completes in
under 100ms.
…ory tree

Add a depth input + button that resets every node back to the plain
depth < N default, discarding expand/collapse overrides entirely rather than
computing which individual nodes to flip. Add a sort-order select + button
(path / most pages first / fewest pages first) that reorders siblings at
every level by descendantHtmlPageCount, computed client-side with no extra
request.

Move all of this view's state (expand/collapse overrides, collapse depth,
sort order) into the URL via useUrlFilter, matching every other list view's
convention. Selecting a node navigates away to the Pages view, which
unmounts this component — a plain useState reset to its initial value on the
back button, discarding whatever the user had expanded or configured.

Format the page-count badge with toLocaleString() for thousands separators,
matching the convention already used elsewhere in this view layer.
Rebuilds the viewer read model for archives predating
viewer_directory_nodes.direct_html_page_count / descendant_html_page_count
(schema version < 17). No write-model change accompanies this bump, so this
is a plain buildViewerReadModel rebuild, not a data migration in the
migrate-to-0.13.mjs sense — recorded here the same way migrate-to-0.10.mjs /
migrate-to-0.13.mjs record their own one-off schema-bump fixes.
The directory tree navigates to /pages?directory=... but that filter has no
control of its own on the Pages view (unlike urlPattern/status/etc., which
show in the filter bar) — nothing on screen said which directory the list
was scoped to. Add a one-line notice above the table when the filter is
present, independent of column sort state.
@YusukeHirao
YusukeHirao merged commit e3a7c51 into dev Jul 24, 2026
4 checks passed
@YusukeHirao
YusukeHirao deleted the feature/156-directory-tree-ui branch July 24, 2026 07:31
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