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.
This commit is contained in:
98
ui/src/components/BreadcrumbNav.tsx
Normal file
98
ui/src/components/BreadcrumbNav.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user