feat: stop button for product creation stream

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 17:34:34 +02:00
parent 42ef490196
commit 2f6eea1122
+61 -29
View File
@@ -1,5 +1,5 @@
'use client' 'use client'
import { useState, useCallback, useEffect } from 'react' import { useState, useCallback, useEffect, useRef } from 'react'
import { Header } from '@/components/layout/Header' import { Header } from '@/components/layout/Header'
import { PendingRowsTable } from '@/components/creation/PendingRowsTable' import { PendingRowsTable } from '@/components/creation/PendingRowsTable'
import { MetafieldResolver } from '@/components/creation/MetafieldResolver' import { MetafieldResolver } from '@/components/creation/MetafieldResolver'
@@ -24,6 +24,8 @@ export default function CreationPage() {
const [resolutions, setResolutions] = useState<MetafieldResolution[]>([]) const [resolutions, setResolutions] = useState<MetafieldResolution[]>([])
const [events, setEvents] = useState<CreationStreamEvent[]>([]) const [events, setEvents] = useState<CreationStreamEvent[]>([])
const [done, setDone] = useState(false) const [done, setDone] = useState(false)
const [stopped, setStopped] = useState(false)
const abortRef = useRef<AbortController | null>(null)
// Collection // Collection
const [collections, setCollections] = useState<ShopifyCollection[]>([]) const [collections, setCollections] = useState<ShopifyCollection[]>([])
@@ -83,32 +85,48 @@ export default function CreationPage() {
const handleResolved = useCallback((r: MetafieldResolution[]) => { setResolutions(r) }, []) const handleResolved = useCallback((r: MetafieldResolution[]) => { setResolutions(r) }, [])
const stop = useCallback(() => {
abortRef.current?.abort()
setStopped(true)
setDone(true)
}, [])
const execute = async () => { const execute = async () => {
const controller = new AbortController()
abortRef.current = controller
setStep('execution') setStep('execution')
setEvents([]) setEvents([])
setDone(false) setDone(false)
const res = await fetch('/api/creation/execute', { setStopped(false)
method: 'POST', try {
headers: { 'Content-Type': 'application/json' }, const res = await fetch('/api/creation/execute', {
body: JSON.stringify({ resolutions, tab: selectedTab, collectionOverride: collectionOverride || undefined }), method: 'POST',
}) headers: { 'Content-Type': 'application/json' },
if (!res.body) return body: JSON.stringify({ resolutions, tab: selectedTab, collectionOverride: collectionOverride || undefined }),
const reader = res.body.getReader() signal: controller.signal,
const decoder = new TextDecoder() })
let buffer = '' if (!res.body) return
while (true) { const reader = res.body.getReader()
const { done: readerDone, value } = await reader.read() const decoder = new TextDecoder()
if (readerDone) break let buffer = ''
buffer += decoder.decode(value, { stream: true }) while (true) {
const lines = buffer.split('\n') const { done: readerDone, value } = await reader.read()
buffer = lines.pop() ?? '' if (readerDone) break
for (const line of lines) { buffer += decoder.decode(value, { stream: true })
if (!line.trim()) continue const lines = buffer.split('\n')
try { buffer = lines.pop() ?? ''
const event = JSON.parse(line) as CreationStreamEvent for (const line of lines) {
setEvents(prev => [...prev, event]) if (!line.trim()) continue
if (event.type === 'done') setDone(true) try {
} catch { /* ligne incomplète */ } const event = JSON.parse(line) as CreationStreamEvent
setEvents(prev => [...prev, event])
if (event.type === 'done') setDone(true)
} catch { /* ligne incomplète */ }
}
}
} catch (e) {
if ((e as Error).name !== 'AbortError') {
setEvents(prev => [...prev, { type: 'error', title: '', message: (e as Error).message }])
} }
} }
setDone(true) setDone(true)
@@ -140,10 +158,13 @@ export default function CreationPage() {
} }
const reset = () => { const reset = () => {
abortRef.current?.abort()
abortRef.current = null
setStep('famille') setStep('famille')
setSelectedTab(null) setSelectedTab(null)
setEvents([]) setEvents([])
setDone(false) setDone(false)
setStopped(false)
setByTab({}) setByTab({})
setTotal(0) setTotal(0)
setDetectedCollectionId('') setDetectedCollectionId('')
@@ -305,12 +326,23 @@ export default function CreationPage() {
{step === 'execution' && ( {step === 'execution' && (
<div className="space-y-4"> <div className="space-y-4">
<CreationProgress events={events} done={done} /> <CreationProgress events={events} done={done} />
{done && ( <div className="flex gap-3">
<button onClick={reset} {!done && (
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors"> <button onClick={stop}
Nouvelle session className="px-4 py-2 bg-red-700 hover:bg-red-600 text-white rounded-lg text-sm font-medium transition-colors">
</button> Arrêter
)} </button>
)}
{done && (
<>
{stopped && <p className="text-amber-400 text-sm self-center"> Création interrompue les produits déjà créés sont conservés dans Shopify.</p>}
<button onClick={reset}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors">
Nouvelle session
</button>
</>
)}
</div>
</div> </div>
)} )}
</div> </div>