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'
import { useState, useCallback, useEffect } from 'react'
import { useState, useCallback, useEffect, useRef } from 'react'
import { Header } from '@/components/layout/Header'
import { PendingRowsTable } from '@/components/creation/PendingRowsTable'
import { MetafieldResolver } from '@/components/creation/MetafieldResolver'
@@ -24,6 +24,8 @@ export default function CreationPage() {
const [resolutions, setResolutions] = useState<MetafieldResolution[]>([])
const [events, setEvents] = useState<CreationStreamEvent[]>([])
const [done, setDone] = useState(false)
const [stopped, setStopped] = useState(false)
const abortRef = useRef<AbortController | null>(null)
// Collection
const [collections, setCollections] = useState<ShopifyCollection[]>([])
@@ -83,32 +85,48 @@ export default function CreationPage() {
const handleResolved = useCallback((r: MetafieldResolution[]) => { setResolutions(r) }, [])
const stop = useCallback(() => {
abortRef.current?.abort()
setStopped(true)
setDone(true)
}, [])
const execute = async () => {
const controller = new AbortController()
abortRef.current = controller
setStep('execution')
setEvents([])
setDone(false)
const res = await fetch('/api/creation/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ resolutions, tab: selectedTab, collectionOverride: collectionOverride || undefined }),
})
if (!res.body) return
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done: readerDone, value } = await reader.read()
if (readerDone) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
for (const line of lines) {
if (!line.trim()) continue
try {
const event = JSON.parse(line) as CreationStreamEvent
setEvents(prev => [...prev, event])
if (event.type === 'done') setDone(true)
} catch { /* ligne incomplète */ }
setStopped(false)
try {
const res = await fetch('/api/creation/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ resolutions, tab: selectedTab, collectionOverride: collectionOverride || undefined }),
signal: controller.signal,
})
if (!res.body) return
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done: readerDone, value } = await reader.read()
if (readerDone) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
for (const line of lines) {
if (!line.trim()) continue
try {
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)
@@ -140,10 +158,13 @@ export default function CreationPage() {
}
const reset = () => {
abortRef.current?.abort()
abortRef.current = null
setStep('famille')
setSelectedTab(null)
setEvents([])
setDone(false)
setStopped(false)
setByTab({})
setTotal(0)
setDetectedCollectionId('')
@@ -305,12 +326,23 @@ export default function CreationPage() {
{step === 'execution' && (
<div className="space-y-4">
<CreationProgress events={events} done={done} />
{done && (
<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 className="flex gap-3">
{!done && (
<button onClick={stop}
className="px-4 py-2 bg-red-700 hover:bg-red-600 text-white rounded-lg text-sm font-medium transition-colors">
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>