fix: open the navigation picker among the current page's siblings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-10-09 07:47:53 -07:00
co-authored by Claude Opus 5.5
parent 43aab55bfb
commit 3c74ce0847
6 changed files with 38 additions and 21 deletions
@@ -4,8 +4,8 @@ The chevron after the breadcrumb's last segment: it opens the same picker a sess
uses — the workspace pages, then the folders with their flows, scripts and apps — and picking a
row navigates the app there.
It lands where the page is: an item's folder with the item highlighted, or a page highlighted at
the root, so moving to a sibling is one step rather than a drill from the top.
It lands among the current page's siblings: an item opens in its folder with the item
highlighted, a workspace page inside Pages with that page highlighted.
-->
<script lang="ts">
import { ChevronDown } from 'lucide-svelte'
@@ -34,8 +34,7 @@ the root, so moving to a sibling is one step rather than a drill from the top.
let isOpen = $state(false)
// The tree files a path under its `f/<folder>` or `u/<user>` scope, then one branch per
// subfolder, each keyed by its full path — so the item's own folder is its path minus the name.
// The item's own folder is its path minus the name.
const initialScope = $derived.by<Scope>(() => {
const parts = item?.path?.split('/') ?? []
return parts.length >= 3 ? { kind: 'all', dir: parts.slice(0, -1).join('/') } : undefined
@@ -43,6 +43,7 @@ section or the flat layout.
PREVIEW_PAGES,
artifactKey,
isArtifactKey,
isPageKey,
pageHref,
pageKey,
previewModeFor,
@@ -243,16 +244,18 @@ section or the flat layout.
)
])
// An artifact highlight lives under the 'artifacts' branch, which the legacy
// {kind, dir} scope can't express — open the picker inside that branch so the
// active artifact is actually visible and highlighted (not the first root row).
// Keyed on the highlight's shape alone: the branch itself may not have
// materialized yet (artifacts hydrate from IndexedDB after tabs restore), and
// the drill entries fill in reactively once it does.
// A page or artifact highlight lives under its own branch, which the legacy
// {kind, dir} scope can't express — open the picker inside that branch, so the
// reader standing on one sees its siblings with it highlighted. Keyed on the
// highlight's shape alone: the artifacts branch may not have materialized yet
// (artifacts hydrate from IndexedDB after tabs restore), and the drill entries
// fill in reactively once it does.
const computedInitialScope = untrack(() =>
initialHighlight && isArtifactKey(initialHighlight)
? ['artifacts']
: legacyScopeToPath(initialScope, kinds, 'flat')
: initialHighlight && isPageKey(initialHighlight)
? ['pages']
: legacyScopeToPath(initialScope, kinds, 'flat')
)
// The flat root has no branch to carry a per-kind loading flag — surface
@@ -141,6 +141,7 @@ export function triggerLabelForPath(path: string): string | undefined {
}
export const pageKey = (path: string) => `page:${path}`
export const isPageKey = (key: string) => key.startsWith('page:')
export const pageHref = (path: string) => `${base}${path}`
/** Strip the deployment base prefix (and any query/hash) from a preview path
@@ -1,6 +1,7 @@
import {
ASSETS_PATH,
AUDIT_LOGS_PATH,
isPageKey,
FOLDERS_PATH,
GROUPS_PATH,
isPageItemListPath,
@@ -29,6 +30,7 @@ import {
// Re-exported so the preview code that already reads locations through this module keeps
// one import, while a caller needing only a path can reach for the leaf instead.
export {
isPageKey,
pageItemListPath,
pageItemUrl,
pageKey,
@@ -7,7 +7,7 @@ import {
type WorkspaceItem,
type WorkspaceItemKind
} from './workspacePicker'
import type { DrillBranch, DrillLeaf, DrillNode } from './drillPicker'
import { resolveScope, type DrillBranch, type DrillLeaf, type DrillNode } from './drillPicker'
const item = (
kind: WorkspaceItemKind,
@@ -522,6 +522,17 @@ describe('legacyScopeToPath', () => {
])
})
it('walks every ancestor folder, so a nested dir resolves in the built tree', () => {
const loaded = { flow: [item('flow', 'f/a/b/c')], script: [] }
for (const layout of ['flat', 'by-kind'] as const) {
const tree = buildWorkspaceTree({ loaded, kinds: ['flow', 'script'], loadingKind: {}, layout })
const path = legacyScopeToPath({ kind: 'flow', dir: 'f/a/b' }, ['flow', 'script'], layout)
expect(resolveScope(tree, path)?.children.map((c) => c.key)).toEqual([
leafKeyFor('flow', 'f/a/b/c')
])
}
})
it('flat: returns [] without a dir', () => {
expect(legacyScopeToPath({ kind: 'all' }, ['flow', 'script'], 'flat')).toEqual([])
})
+10 -9
View File
@@ -271,19 +271,20 @@ export function legacyScopeToPath(
layout: 'by-kind' | 'flat' = 'by-kind'
): string[] {
if (!scope) return []
// A subfolder is a child of its parent's branch, not a root entry, so a scope into
// `f/a/b` must walk `f/a` first or it resolves to nothing.
const dirChain = (kind: WorkspaceItemKind | 'all') => {
if (!scope.dir) return []
const parts = scope.dir.split('/')
return parts.slice(1).map((_, i) => dirKey(kind, parts.slice(0, i + 2).join('/')))
}
// Flat layout: no kind branches at all — scope dirs live at root and are
// always keyed under the cross-kind 'all' namespace.
if (layout === 'flat') {
return scope.dir ? [dirKey('all', scope.dir)] : []
}
if (layout === 'flat') return dirChain('all')
// Single-kind mode: there's no kind branch at root; scope's `kind` is
// implicit. Only the dir (if any) makes it to the path.
if (kinds.length === 1) {
return scope.dir ? [dirKey(scope.kind, scope.dir)] : []
}
const path: string[] = [kindKey(scope.kind)]
if (scope.dir) path.push(dirKey(scope.kind, scope.dir))
return path
if (kinds.length === 1) return dirChain(scope.kind)
return [kindKey(scope.kind), ...dirChain(scope.kind)]
}
/** Return `absolutePath` shortened to its segment relative to the deepest