compassmock/src/components/files/file-breadcrumb.tsx
Nicholai 017b0797c7
feat(files): Google Drive integration (#49)
* feat(files): wire file browser to Google Drive API

replace mock file data with real Google Drive integration
via domain-wide delegation (service account impersonation).

- add google drive REST API v3 library (JWT auth, drive
  client, rate limiting, token cache)
- add schema: google_auth, google_starred_files tables,
  users.googleEmail column
- add 16+ server actions for full CRUD (browse, upload,
  download, create folders, rename, move, trash/restore)
- add download proxy route with google-native file export
- add folder-by-ID route (/dashboard/files/folder/[id])
- refactor use-files hook to fetch from server actions
  with mock data fallback when disconnected
- update all file browser components (upload, rename,
  move, context menu, breadcrumb, drag-drop, nav)
- add settings UI: connection, shared drive picker,
  user email mapping
- extract shared AES-GCM crypto from netsuite module
- dual permission: compass RBAC + google workspace ACLs

* docs(files): add Google Drive integration guide

covers architecture decisions, permission model,
setup instructions, and known limitations.

---------

Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
2026-02-06 22:18:25 -07:00

161 lines
4.2 KiB
TypeScript
Executable File

"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
import { IconChevronRight } from "@tabler/icons-react"
import { useFiles } from "@/hooks/use-files"
import { getDriveFileInfo } from "@/app/actions/google-drive"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
type BreadcrumbSegment = {
name: string
folderId: string | null
}
export function FileBreadcrumb({
path,
folderId,
}: {
path?: string[]
folderId?: string
}) {
const { state } = useFiles()
const [segments, setSegments] = useState<
BreadcrumbSegment[]
>([])
// for google drive mode: resolve folder ancestry
useEffect(() => {
if (state.isConnected !== true || !folderId) {
setSegments([])
return
}
let cancelled = false
async function resolve() {
const trail: BreadcrumbSegment[] = []
let currentId: string | null = folderId ?? null
// walk up the parents chain (max 10 deep to prevent infinite loops)
for (let depth = 0; depth < 10 && currentId; depth++) {
try {
const result = await getDriveFileInfo(currentId)
if (!result.success || cancelled) break
trail.unshift({
name: result.file.name,
folderId: currentId,
})
currentId = result.file.parentId
} catch {
break
}
}
if (!cancelled) {
setSegments(trail)
}
}
resolve()
return () => {
cancelled = true
}
}, [folderId, state.isConnected])
// mock data mode: use path array
if (state.isConnected !== true) {
const effectivePath = path ?? []
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
{effectivePath.length === 0 ? (
<BreadcrumbPage>My Files</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link href="/dashboard/files">
My Files
</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
{effectivePath.map((segment, i) => {
const isLast = i === effectivePath.length - 1
const href = `/dashboard/files/${effectivePath.slice(0, i + 1).join("/")}`
return (
<span key={segment} className="contents">
<BreadcrumbSeparator>
<IconChevronRight size={14} />
</BreadcrumbSeparator>
<BreadcrumbItem>
{isLast ? (
<BreadcrumbPage>
{segment}
</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link href={href}>{segment}</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
</span>
)
})}
</BreadcrumbList>
</Breadcrumb>
)
}
// google drive mode: use resolved segments
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
{segments.length === 0 && !folderId ? (
<BreadcrumbPage>My Files</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link href="/dashboard/files">My Files</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
{segments.map((seg, i) => {
const isLast = i === segments.length - 1
return (
<span
key={seg.folderId ?? i}
className="contents"
>
<BreadcrumbSeparator>
<IconChevronRight size={14} />
</BreadcrumbSeparator>
<BreadcrumbItem>
{isLast ? (
<BreadcrumbPage>{seg.name}</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link
href={`/dashboard/files/folder/${seg.folderId}`}
>
{seg.name}
</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
</span>
)
})}
</BreadcrumbList>
</Breadcrumb>
)
}