diff --git a/v2/operator-app/src/routes/briefs/edit.tsx b/v2/operator-app/src/routes/briefs/edit.tsx index 7cdc44b..56cab57 100644 --- a/v2/operator-app/src/routes/briefs/edit.tsx +++ b/v2/operator-app/src/routes/briefs/edit.tsx @@ -1,12 +1,44 @@ -// Brief edit page — reuses the same Zod-shape JSON the server expects on PATCH. -// Minimal UI: pre-fills a JSON textarea with the current brief's full shape and -// submits the parsed JSON. Avoids duplicating the 7-section form from `new.tsx`. -import { useEffect, useState } from 'react'; +// Brief edit page. Prefills a JSON textarea from the current brief's full shape +// and submits the parsed JSON via PATCH. Robust to the brief data arriving late +// or to `full` being missing — falls back to reconstructing the brief shape +// from the public columns. Doesn't clobber user-typed content on data refetch. +import { useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { useBrief, useUpdateBrief, type BriefCreateInput } from '../../api/briefs'; +import { useBrief, useUpdateBrief, type BriefCreateInput, type BriefSummary } from '../../api/briefs'; import { ApiError } from '../../api/client'; import { useMe } from '../../auth/useMe'; +function reconstructFromPublic(b: BriefSummary): BriefCreateInput { + // When the server's `full` field is absent (older briefs, or any stored + // shape we can't trust), build a valid BRIEF_INPUT from the public columns. + const positioning = (b.positioning && typeof b.positioning === 'object' + ? b.positioning as { positioning?: string; brand?: { name?: string; handle?: string } } + : null); + return { + client_name: b.client_name, + category: b.category, + brand: { + name: positioning?.brand?.name ?? b.client_name, + handle: positioning?.brand?.handle ?? '', + ...(positioning?.positioning ? { positioning: positioning.positioning } : {}), + }, + competitors: [], + audience: { primary: '', age_range: '', gender: '', interests: [] }, + geo: '', + language: 'en', + business_question: b.business_question, + kpis: Array.isArray(b.kpis) ? b.kpis : [], + budget_usd: b.budget_usd, + date_window_days: b.date_window_days, + platforms: (b.platforms as 'tiktok'[]) ?? ['tiktok'], + context_vision: b.context_vision ?? undefined, + prior_report_id: b.prior_report_id ?? null, + min_likes: b.min_likes, + min_plays: b.min_plays, + min_stl_pct: b.min_stl_pct, + }; +} + export default function BriefEdit() { const { id } = useParams(); const navigate = useNavigate(); @@ -16,17 +48,30 @@ export default function BriefEdit() { const [text, setText] = useState(''); const [err, setErr] = useState(null); const [issues, setIssues] = useState<{ path: (string | number)[]; message: string }[] | null>(null); + const seededRef = useRef(false); + // Initialise the textarea exactly once when data first arrives. Subsequent + // refetches (window focus, invalidation) MUST NOT overwrite what the user + // has typed. If `full` is missing, fall back to the public-columns view. useEffect(() => { - if (data?.brief?.full) setText(JSON.stringify(data.brief.full, null, 2)); + if (seededRef.current || !data?.brief) return; + const source = data.brief.full ?? reconstructFromPublic(data.brief); + setText(JSON.stringify(source, null, 2)); + seededRef.current = true; }, [data]); - if (isLoading) return
Loading…
; + if (isLoading) return
Loading brief…
; if (error || !data) return
Failed to load brief.
; const role = me?.active_team?.role; if (role !== 'editor' && role !== 'admin' && role !== 'owner') { - return
Editor or higher role required to edit briefs.
; + return
Editor, admin, or owner role required to edit briefs.
; + } + + function resetFromCurrent() { + if (!data?.brief) return; + const source = data.brief.full ?? reconstructFromPublic(data.brief); + setText(JSON.stringify(source, null, 2)); } function onSubmit(e: React.FormEvent) { @@ -37,7 +82,7 @@ export default function BriefEdit() { catch (e2) { setErr(`Invalid JSON: ${(e2 as Error).message}`); return; } update.mutate(parsed, { onSuccess: () => navigate(`/briefs/${id}`), - onError: (e2) => { + onError: (e2: Error) => { if (e2 instanceof ApiError) { setErr(e2.message); if (e2.issues) setIssues(e2.issues); @@ -50,16 +95,34 @@ export default function BriefEdit() { return (
-
-

Edit brief

-

{data.brief.client_name} · {data.brief.slug}

+
+
+

Edit brief

+

{data.brief.client_name} · {data.brief.slug}

+
+
+ {!data.brief.full && ( +
+ This brief's full Zod shape wasn't returned by the server (older brief?). + The textarea below has been reconstructed from public columns — + competitors / audience / geo are blank and will need re-entering. +
+ )}