compassmock/src/components/files/file-new-folder-dialog.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

124 lines
3.0 KiB
TypeScript
Executable File

"use client"
import { useState } from "react"
import { IconFolderPlus, IconLoader2 } from "@tabler/icons-react"
import { useFiles } from "@/hooks/use-files"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { toast } from "sonner"
export function FileNewFolderDialog({
open,
onOpenChange,
currentPath,
parentId,
}: {
open: boolean
onOpenChange: (open: boolean) => void
currentPath: string[]
parentId: string | null
}) {
const [name, setName] = useState("")
const [loading, setLoading] = useState(false)
const { createFolder, state, dispatch } = useFiles()
const handleCreate = async () => {
const trimmed = name.trim()
if (!trimmed) return
setLoading(true)
try {
if (state.isConnected === true) {
const ok = await createFolder(
trimmed,
parentId ?? undefined
)
if (ok) {
toast.success(`Folder "${trimmed}" created`)
} else {
toast.error("Failed to create folder")
}
} else {
// mock mode: local dispatch
dispatch({
type: "OPTIMISTIC_ADD_FOLDER",
payload: {
id: `folder-${Date.now()}`,
name: trimmed,
type: "folder",
size: 0,
path: currentPath,
createdAt: new Date().toISOString(),
modifiedAt: new Date().toISOString(),
owner: { name: "You" },
starred: false,
shared: false,
trashed: false,
parentId,
},
})
toast.success(`Folder "${trimmed}" created`)
}
setName("")
onOpenChange(false)
} finally {
setLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<IconFolderPlus size={18} />
New folder
</DialogTitle>
</DialogHeader>
<div className="py-2">
<Input
placeholder="Folder name"
value={name}
onChange={e => setName(e.target.value)}
onKeyDown={e =>
e.key === "Enter" && handleCreate()
}
autoFocus
disabled={loading}
/>
</div>
<DialogFooter>
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
disabled={loading}
>
Cancel
</Button>
<Button
onClick={handleCreate}
disabled={!name.trim() || loading}
>
{loading && (
<IconLoader2
size={16}
className="mr-2 animate-spin"
/>
)}
Create
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}