feat: persist metafield resolutions across sessions

Les associations colonne→métachamp sont sauvegardées dans data/metafield-resolutions.json
et rechargées automatiquement à la prochaine création de produits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 15:28:53 +02:00
parent caef95bd55
commit 114e20c9fb
3 changed files with 62 additions and 7 deletions
+20
View File
@@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { loadResolutionCache, saveResolutionCache } from '@/lib/resolutionCache'
import type { MetafieldResolution } from '@/types/creation'
export async function GET() {
const session = await getServerSession(authOptions)
if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 })
const cache = await loadResolutionCache()
return NextResponse.json(cache)
}
export async function PUT(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 })
const body = await req.json() as Record<string, MetafieldResolution>
await saveResolutionCache(body)
return NextResponse.json({ ok: true })
}
+22 -7
View File
@@ -18,25 +18,28 @@ export function MetafieldResolver({ specificColumns, onResolved }: Props) {
const [searches, setSearches] = useState<Record<string, string>>({}) const [searches, setSearches] = useState<Record<string, string>>({})
const [openDropdown, setOpenDropdown] = useState<string | null>(null) const [openDropdown, setOpenDropdown] = useState<string | null>(null)
const dropdownRef = useRef<HTMLDivElement>(null) const dropdownRef = useRef<HTMLDivElement>(null)
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => { useEffect(() => {
if (specificColumns.length === 0) { setLoading(false); onResolved([]); return } if (specificColumns.length === 0) { setLoading(false); onResolved([]); return }
const params = specificColumns.map(c => encodeURIComponent(c)).join(',') const params = specificColumns.map(c => encodeURIComponent(c)).join(',')
fetch(`/api/creation/metafields?columns=${params}`) Promise.all([
.then(r => r.json()) fetch(`/api/creation/metafields?columns=${params}`).then(r => r.json()),
.then(data => { fetch('/api/creation/resolutions').then(r => r.json()),
])
.then(([data, cache]: [{ error?: string; definitions: MetafieldDefinition[]; matched: Record<string, MetafieldResolution>; unmatched: string[] }, Record<string, MetafieldResolution>]) => {
if (data.error) throw new Error(data.error) if (data.error) throw new Error(data.error)
setDefinitions(data.definitions) setDefinitions(data.definitions)
setMatched(data.matched) setMatched(data.matched)
setUnmatched(data.unmatched) setUnmatched(data.unmatched)
const init: Record<string, MetafieldResolution> = { ...data.matched } const init: Record<string, MetafieldResolution> = { ...data.matched }
// Défaut : 'skip' — l'utilisateur choisit explicitement ce qu'il veut créer
data.unmatched.forEach((col: string) => { data.unmatched.forEach((col: string) => {
init[col] = { columnHeader: col, action: 'skip', namespace: 'custom', key: normalizeMetafieldKey(col) } // Utilise le cache persisté si disponible, sinon 'skip' par défaut
init[col] = cache[col] ?? { columnHeader: col, action: 'skip', namespace: 'custom', key: normalizeMetafieldKey(col) }
}) })
setResolutions(init) setResolutions(init)
}) })
.catch(e => setError(e.message)) .catch(e => setError((e as Error).message))
.finally(() => setLoading(false)) .finally(() => setLoading(false))
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [specificColumns.join(',')]) }, [specificColumns.join(',')])
@@ -47,7 +50,19 @@ export function MetafieldResolver({ specificColumns, onResolved }: Props) {
}, [resolutions, loading]) }, [resolutions, loading])
const updateResolution = (col: string, partial: Partial<MetafieldResolution>) => { const updateResolution = (col: string, partial: Partial<MetafieldResolution>) => {
setResolutions(prev => ({ ...prev, [col]: { ...prev[col], ...partial } })) setResolutions(prev => {
const next = { ...prev, [col]: { ...prev[col], ...partial } }
// Sauvegarde en différé pour éviter un appel par frappe
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
saveTimeoutRef.current = setTimeout(() => {
fetch('/api/creation/resolutions', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(next),
}).catch(() => null)
}, 800)
return next
})
} }
const filteredDefs = (col: string) => { const filteredDefs = (col: string) => {
+20
View File
@@ -0,0 +1,20 @@
import { readFile, writeFile, mkdir } from 'fs/promises'
import { join } from 'path'
import type { MetafieldResolution } from '@/types/creation'
const CACHE_DIR = join(process.cwd(), 'data')
const CACHE_FILE = join(CACHE_DIR, 'metafield-resolutions.json')
export async function loadResolutionCache(): Promise<Record<string, MetafieldResolution>> {
try {
const raw = await readFile(CACHE_FILE, 'utf-8')
return JSON.parse(raw) as Record<string, MetafieldResolution>
} catch {
return {}
}
}
export async function saveResolutionCache(cache: Record<string, MetafieldResolution>): Promise<void> {
await mkdir(CACHE_DIR, { recursive: true })
await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), 'utf-8')
}