This repository has been archived on 2026-03-27. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
drive/ui/src/components/BreadcrumbNav.tsx
Sienna Meridian Satterwhite 58237d9e44 Initial commit — Drive, an S3 file browser with WOPI editing
Lightweight replacement for the upstream La Suite Numérique drive
(Django/Celery/Next.js) built as a single Deno binary.

Server (Deno + Hono):
- S3 file operations via AWS SigV4 (no SDK) with pre-signed URLs
- WOPI host for Collabora Online (CheckFileInfo, GetFile, PutFile, locks)
- Ory Kratos session auth + CSRF protection
- Ory Keto permission model (OPL namespaces, not yet wired to routes)
- PostgreSQL metadata with recursive folder sizes
- S3 backfill API for registering files uploaded outside the UI
- OpenTelemetry tracing + metrics (opt-in via OTEL_ENABLED)

Frontend (React 19 + Cunningham v4 + react-aria):
- File browser with GridList, keyboard nav, multi-select
- Collabora editor iframe (full-screen, form POST, postMessage)
- Profile menu, waffle menu, drag-drop upload, asset type badges
- La Suite integration service theming (runtime CSS)

Testing (549 tests):
- 235 server unit tests (Deno) — 90%+ coverage
- 278 UI unit tests (Vitest) — 90%+ coverage
- 11 E2E tests (Playwright)
- 12 integration service tests (Playwright)
- 13 WOPI integration tests (Playwright + Docker Compose + Collabora)

MIT licensed.
2026-03-25 18:28:37 +00:00

99 lines
2.7 KiB
TypeScript

import { useNavigate } from 'react-router-dom'
import { useFile } from '../api/files'
interface BreadcrumbNavProps {
folderId?: string
}
interface BreadcrumbSegment {
id: string | null
name: string
}
function useBreadcrumbs(folderId?: string): BreadcrumbSegment[] {
const { data: folder } = useFile(folderId)
const crumbs: BreadcrumbSegment[] = [{ id: null, name: 'My Files' }]
if (folder) {
if (folder.parent_id) {
crumbs.push({ id: folder.parent_id, name: '...' })
}
crumbs.push({ id: folder.id, name: folder.filename })
}
return crumbs
}
export default function BreadcrumbNav({ folderId }: BreadcrumbNavProps) {
const navigate = useNavigate()
const breadcrumbs = useBreadcrumbs(folderId)
return (
<nav
aria-label="Breadcrumb"
style={{
display: 'flex',
alignItems: 'center',
gap: 2,
fontSize: 14,
}}
>
{breadcrumbs.map((crumb, index) => {
const isLast = index === breadcrumbs.length - 1
return (
<span key={crumb.id ?? 'root'} style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{index > 0 && (
<span className="material-icons" aria-hidden="true" style={{
fontSize: 16,
color: 'var(--c--theme--colors--greyscale-400)',
userSelect: 'none',
}}>
chevron_right
</span>
)}
{isLast ? (
<span style={{
fontWeight: 600,
color: 'var(--c--theme--colors--greyscale-800)',
padding: '4px 6px',
}}>
{crumb.name}
</span>
) : (
<button
onClick={() => {
if (crumb.id === null) {
navigate('/explorer')
} else {
navigate(`/explorer/${crumb.id}`)
}
}}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '4px 6px',
borderRadius: 4,
color: 'var(--c--theme--colors--greyscale-500)',
fontWeight: 500,
fontSize: 14,
transition: 'color 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = 'var(--c--theme--colors--primary-400)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = 'var(--c--theme--colors--greyscale-500)'
}}
>
{crumb.name}
</button>
)}
</span>
)
})}
</nav>
)
}