Files
windmill/frontend/src/lib/components/BreadcrumbNavPicker.svelte
T
9653318c17 feat: add a navigation picker after the breadcrumb's last segment (#11649)
* feat: add a navigation picker after the breadcrumb's last segment

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* style: size the navigation chevron like the workspace chevron

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* style: keep one-line picker lists at one-line row height

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: show the navigation chevron only once the item's path is known

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: highlight a draft in the navigation picker by its storage path

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: key the picker's current item on the header path outside editors

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: count navigation picker picks in feature_usage

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: shrink the page header in stages and let the bar fold page actions

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: fold the runs header through the bar and wrap its filter to a second row

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: truncate the run id and fold run actions into one menu on a narrow bar

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: keep an item's summary and drop its path on a narrow page header

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: show the full summary and path read-only when they cannot be edited

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: keep secondary detail actions in menus and the star beside the summary

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: show secondary detail actions as buttons while the bar has room for them

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 346bd5417537b76591465e63b9d15bd081e9228c

This commit updates the EE repository reference after PR #868 was merged in windmill-ee-private.

Previous ee-repo-ref: 581b18cac39a4bee2671fd1aa86f91b9e702e8f5

New ee-repo-ref: 346bd5417537b76591465e63b9d15bd081e9228c

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-10-14 15:51:12 +02:00

123 lines
4.5 KiB
Svelte

<!--
@component
The chevron after the breadcrumb's last segment: it opens the same picker a session's preview tab
uses — the workspace pages, then the folders with their flows, scripts and apps — and picking a
row navigates the app there.
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'
import { goto } from '$app/navigation'
import { page } from '$app/state'
import Popover from '$lib/components/meltComponents/Popover.svelte'
import PreviewRouterPicker, { type Scope } from './sessions/PreviewRouterPicker.svelte'
import { pageKey, parsePreviewItemRoute, type PreviewTarget } from './sessions/previewRouter'
import { editPathFor, leafKeyFor, viewPathFor, type WorkspaceItem } from './workspacePicker'
import { logFeatureUsage } from '$lib/utils/featureUsage'
import type { PageHeaderItem } from './pageHeaderRegistry.svelte'
let {
item,
pagePath,
actingWorkspaceId,
iconSize = 14
}: {
/** The item the page is about, to land in its folder. */
item?: PageHeaderItem
/** The route of a workspace page, to highlight it among the pages. */
pagePath?: string
/** Load from, and navigate within, this workspace rather than the one the app points at. */
actingWorkspaceId?: string
iconSize?: number
} = $props()
let isOpen = $state(false)
// Two paths for one item, which differ on a draft: the header names it by the path it will
// deploy to, which is the folder the tree files it under, while its row is keyed by where it
// is stored, which is the path in an editor's URL. A viewer's URL is no help: a script page
// can be addressed by a version hash, so there the header's path is the row's.
const route = $derived(item?.path ? parsePreviewItemRoute(page.url.pathname) : null)
const currentItem = $derived.by<WorkspaceItem | undefined>(() => {
const kind = route?.kind ?? item?.kind
const path = route?.mode === 'edit' ? route.itemPath : item?.path
if (!kind || !path) return undefined
return { path, kind, raw_app: route?.raw_app, summary: item?.summary ?? '' }
})
// 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
})
const initialHighlight = $derived(
currentItem
? leafKeyFor(currentItem.kind, currentItem.path)
: pagePath
? pageKey(pagePath)
: undefined
)
// A page acting on another workspace than the app's (a session in a fork) keeps acting on it:
// the route reads `?workspace=` before the store.
function withWorkspace(href: string): string {
if (!actingWorkspaceId) return href
const sep = href.includes('?') ? '&' : '?'
return `${href}${sep}workspace=${encodeURIComponent(actingWorkspaceId)}`
}
function hrefFor(target: PreviewTarget): string | undefined {
if (target.type === 'page') return target.href
if (target.type === 'item')
return target.mode === 'view' ? viewPathFor(target.item) : editPathFor(target.item)
return undefined
}
/** The whole vocabulary of the `header_nav` / `pick` counter: what kind of destination was
* picked, never which one. */
type PickKey = 'page' | 'item_view' | 'item_edit'
function pickKey(target: PreviewTarget): PickKey | undefined {
if (target.type === 'page') return 'page'
if (target.type === 'item') return target.mode === 'view' ? 'item_view' : 'item_edit'
return undefined
}
function pick(target: PreviewTarget) {
isOpen = false
const href = hrefFor(target)
if (!href) return
const key = pickKey(target)
if (key) logFeatureUsage('header_nav', 'pick', { key, workspace: actingWorkspaceId })
goto(withWorkspace(href))
}
</script>
<Popover
placement="bottom-start"
usePointerDownOutside
excludeSelectors=".drawer"
disableFocusTrap
closeOnOtherPopoverOpen
bind:isOpen
openFocus="[data-workspace-picker-search]"
contentClasses="flex flex-col overflow-hidden"
class="shrink-0 flex items-center h-9 px-1.5 rounded text-tertiary hover:bg-surface-hover hover:text-primary transition-colors"
triggerAttrs={{ 'aria-label': 'Navigate to', title: 'Navigate to' }}
>
{#snippet trigger()}
<ChevronDown size={iconSize} class="flex-shrink-0" />
{/snippet}
{#snippet content()}
<PreviewRouterPicker
{initialScope}
{initialHighlight}
{currentItem}
workspaceId={actingWorkspaceId}
onPick={pick}
/>
{/snippet}
</Popover>