feat: stop button for product creation stream
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user