From 114e20c9fb3a05e777af55cd63244033ca785aef Mon Sep 17 00:00:00 2001 From: Mathieu Date: Wed, 24 Jun 2026 15:28:53 +0200 Subject: [PATCH] feat: persist metafield resolutions across sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/api/creation/resolutions/route.ts | 20 +++++++++++++ src/components/creation/MetafieldResolver.tsx | 29 ++++++++++++++----- src/lib/resolutionCache.ts | 20 +++++++++++++ 3 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 src/app/api/creation/resolutions/route.ts create mode 100644 src/lib/resolutionCache.ts diff --git a/src/app/api/creation/resolutions/route.ts b/src/app/api/creation/resolutions/route.ts new file mode 100644 index 0000000..4a85e16 --- /dev/null +++ b/src/app/api/creation/resolutions/route.ts @@ -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 + await saveResolutionCache(body) + return NextResponse.json({ ok: true }) +} diff --git a/src/components/creation/MetafieldResolver.tsx b/src/components/creation/MetafieldResolver.tsx index 85f45c2..965c340 100644 --- a/src/components/creation/MetafieldResolver.tsx +++ b/src/components/creation/MetafieldResolver.tsx @@ -18,25 +18,28 @@ export function MetafieldResolver({ specificColumns, onResolved }: Props) { const [searches, setSearches] = useState>({}) const [openDropdown, setOpenDropdown] = useState(null) const dropdownRef = useRef(null) + const saveTimeoutRef = useRef | null>(null) useEffect(() => { if (specificColumns.length === 0) { setLoading(false); onResolved([]); return } const params = specificColumns.map(c => encodeURIComponent(c)).join(',') - fetch(`/api/creation/metafields?columns=${params}`) - .then(r => r.json()) - .then(data => { + Promise.all([ + fetch(`/api/creation/metafields?columns=${params}`).then(r => r.json()), + fetch('/api/creation/resolutions').then(r => r.json()), + ]) + .then(([data, cache]: [{ error?: string; definitions: MetafieldDefinition[]; matched: Record; unmatched: string[] }, Record]) => { if (data.error) throw new Error(data.error) setDefinitions(data.definitions) setMatched(data.matched) setUnmatched(data.unmatched) const init: Record = { ...data.matched } - // Défaut : 'skip' — l'utilisateur choisit explicitement ce qu'il veut créer 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) }) - .catch(e => setError(e.message)) + .catch(e => setError((e as Error).message)) .finally(() => setLoading(false)) // eslint-disable-next-line react-hooks/exhaustive-deps }, [specificColumns.join(',')]) @@ -47,7 +50,19 @@ export function MetafieldResolver({ specificColumns, onResolved }: Props) { }, [resolutions, loading]) const updateResolution = (col: string, partial: Partial) => { - 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) => { diff --git a/src/lib/resolutionCache.ts b/src/lib/resolutionCache.ts new file mode 100644 index 0000000..02f4097 --- /dev/null +++ b/src/lib/resolutionCache.ts @@ -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> { + try { + const raw = await readFile(CACHE_FILE, 'utf-8') + return JSON.parse(raw) as Record + } catch { + return {} + } +} + +export async function saveResolutionCache(cache: Record): Promise { + await mkdir(CACHE_DIR, { recursive: true }) + await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), 'utf-8') +}