Theme 3: download a product as a project (zip export)
New GET /api/orgboard/products/{id}/export streams a zip of the product's
delivered work: PRODUCT.md (identity), each team's artifacts written as real
source files when the artifact is a single fenced code block (App.tsx,
schema.sql, …) or markdown otherwise, plus a README manifest. Gated on
board-view permission. The Delivery dashboard gets a Download project button
that fetches the file with the auth header and saves it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Gauge } from 'lucide-react'
|
||||
import { Download, Gauge } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { AppShell } from '@/components/AppShell'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -59,6 +60,7 @@ export function DeliveryPage() {
|
||||
const [productId, setProductId] = useState<string | null>(() => localStorage.getItem('teamup.delivery.product'))
|
||||
const [teams, setTeams] = useState<TeamProgress[]>([])
|
||||
const [analytics, setAnalytics] = useState<Analytics | null>(null)
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!organizationId) return
|
||||
@@ -122,6 +124,33 @@ export function DeliveryPage() {
|
||||
|
||||
const product = products.find((p) => p.id === productId) ?? null
|
||||
|
||||
// Download the product as a zip of its delivered artifacts. The export endpoint streams a file, so we
|
||||
// fetch it with the auth header (the api helper only does JSON), then trigger a browser download.
|
||||
const downloadProject = useCallback(async () => {
|
||||
if (!productId) return
|
||||
setDownloading(true)
|
||||
try {
|
||||
const token = useAuth.getState().token
|
||||
const res = await fetch(`/api/orgboard/products/${productId}/export`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
if (!res.ok) throw new Error(`Export failed (${res.status})`)
|
||||
const blob = await res.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${(product?.name ?? 'project').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}.zip`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message)
|
||||
} finally {
|
||||
setDownloading(false)
|
||||
}
|
||||
}, [productId, product])
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<div className="mx-auto max-w-4xl p-6">
|
||||
@@ -129,16 +158,24 @@ export function DeliveryPage() {
|
||||
<h1 className="flex items-center gap-2 text-2xl font-semibold tracking-tight">
|
||||
<Gauge className="size-6" /> Delivery
|
||||
</h1>
|
||||
{products.length > 0 && (
|
||||
<Select value={productId ?? ''} onValueChange={setProductId}>
|
||||
<SelectTrigger className="w-56"><SelectValue placeholder="Pick a product" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{products.map((p) => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{product && (
|
||||
<Button variant="outline" size="sm" disabled={downloading} onClick={downloadProject}>
|
||||
<Download data-icon="inline-start" />
|
||||
{downloading ? 'Preparing…' : 'Download project'}
|
||||
</Button>
|
||||
)}
|
||||
{products.length > 0 && (
|
||||
<Select value={productId ?? ''} onValueChange={setProductId}>
|
||||
<SelectTrigger className="w-56"><SelectValue placeholder="Pick a product" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{products.map((p) => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{product && (
|
||||
|
||||
Reference in New Issue
Block a user