feat(ideas): upload-plan knop — short-circuit van Make-Plan AI-flow (#197)
Voegt een 'Upload plan' knop toe in idea-row-actions (verschijnt in zowel list als idea-detail). Klik → file picker → kies .md → server-side parse + opslaan; idea-status springt naar PLAN_READY. Vandaaruit de bestaande 'Maak PBI' knop voor materialize. Server (uploadPlanMdAction): - Toegestaan vanuit DRAFT, GRILLED, PLAN_FAILED, PLAN_READY - DRAFT → skip-grill: status gaat direct naar PLAN_READY - PLAN_READY overschrijft het bestaande plan (consistent met updatePlanMdAction, geen confirmation) - Geblokkeerd in GRILLING/PLANNING (job loopt), PLANNED (al gematerialiseerd) - Parse-failure → 422 + details (NIET opslaan, zodat een onparseerbaar plan nooit in de DB belandt) - Empty / >100k chars → 422 - Schrijft IdeaLog NOTE met from_status + length - Rate-limit + demo-guard + ownership-check via loadOwnedIdea (zelfde patroon als updatePlanMdAction) UI (idea-row-actions.tsx): - Hidden <input type=file accept=".md,.markdown,text/markdown,text/plain"> - FileReader → text → action - Toast bij success + router.refresh() - Blocked-tooltip in andere statussen Tests: 10 nieuwe in __tests__/actions/ideas-crud.test.ts dekkend voor: happy paths (DRAFT/GRILLED/PLAN_READY-overwrite/PLAN_FAILED), blocks (PLANNED/GRILLING), validation (empty/oversized/parse-fail), 404. Full suite groen: 849/849. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
551550791e
commit
76c2efd27c
3 changed files with 224 additions and 1 deletions
|
|
@ -14,7 +14,7 @@
|
|||
// Demo-tooltip om elke muteer-knop. connectedWorkers wordt gelezen uit
|
||||
// useSoloStore (M12 grill-keuze 16 — geen lift voor v1).
|
||||
|
||||
import { useTransition } from 'react'
|
||||
import { useRef, useTransition } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
Archive,
|
||||
|
|
@ -24,6 +24,7 @@ import {
|
|||
Layers,
|
||||
RotateCw,
|
||||
Sparkles,
|
||||
Upload,
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ import {
|
|||
startGrillJobAction,
|
||||
startMakePlanJobAction,
|
||||
materializeIdeaPlanAction,
|
||||
uploadPlanMdAction,
|
||||
} from '@/actions/ideas'
|
||||
import type { IdeaDto } from '@/lib/idea-dto'
|
||||
|
||||
|
|
@ -90,6 +92,48 @@ export function IdeaRowActions({ idea, isDemo, onArchive }: IdeaRowActionsProps)
|
|||
})()
|
||||
const materializeEnabled = !materializeBlockedReason && !isDemo && !pending
|
||||
|
||||
// ---- Upload plan ----
|
||||
// Synchrone server-action (parse + DB), geen worker nodig. Mag vanuit
|
||||
// DRAFT (skip-grill), GRILLED, PLAN_FAILED of PLAN_READY (overschrijft het
|
||||
// bestaande plan zonder confirmation — consistent met updatePlanMdAction).
|
||||
const uploadPlanAllowedStates = ['draft', 'grilled', 'plan_failed', 'plan_ready']
|
||||
const uploadPlanBlockedReason = (() => {
|
||||
if (uploadPlanAllowedStates.includes(status)) return null
|
||||
if (status === 'grilling' || status === 'planning') return 'Job loopt al'
|
||||
if (status === 'planned') return 'Idee is al gepland'
|
||||
return null
|
||||
})()
|
||||
const uploadPlanEnabled = !uploadPlanBlockedReason && !isDemo && !pending
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
function handleUploadPlanClick() {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
function handlePlanFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0]
|
||||
// Reset zodat dezelfde file na een fout opnieuw gekozen kan worden.
|
||||
e.target.value = ''
|
||||
if (!file) return
|
||||
|
||||
startTransition(async () => {
|
||||
let text: string
|
||||
try {
|
||||
text = await file.text()
|
||||
} catch {
|
||||
toast.error('Kon bestand niet lezen')
|
||||
return
|
||||
}
|
||||
const r = await uploadPlanMdAction(idea.id, text)
|
||||
if ('error' in r) {
|
||||
toast.error(r.error)
|
||||
return
|
||||
}
|
||||
toast.success('Plan geüpload — idee staat nu op PLAN_READY')
|
||||
router.refresh()
|
||||
})
|
||||
}
|
||||
|
||||
// ---- Failed-states tonen "Probeer opnieuw" ----
|
||||
const isFailedState = status === 'grill_failed' || status === 'plan_failed'
|
||||
|
||||
|
|
@ -172,6 +216,26 @@ export function IdeaRowActions({ idea, isDemo, onArchive }: IdeaRowActionsProps)
|
|||
/>
|
||||
</span>
|
||||
|
||||
{/* Upload plan — synchrone short-circuit van de Make-Plan AI-flow */}
|
||||
<span data-debug-id="idea-row-actions__upload-plan">
|
||||
<ActionButton
|
||||
label="Upload plan"
|
||||
icon={<Upload className="size-3.5" />}
|
||||
enabled={uploadPlanEnabled}
|
||||
blockedReason={uploadPlanBlockedReason}
|
||||
isDemo={isDemo}
|
||||
onClick={handleUploadPlanClick}
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".md,.markdown,text/markdown,text/plain"
|
||||
className="hidden"
|
||||
onChange={handlePlanFileChange}
|
||||
aria-label="Plan-markdown bestand kiezen"
|
||||
/>
|
||||
</span>
|
||||
|
||||
{/* Materialiseer */}
|
||||
<ActionButton
|
||||
label="Maak PBI"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue