From 93411da46248ccd63afe0085ece5792680edbf89 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 11 Jun 2026 06:49:22 +0330 Subject: [PATCH] feat(presets): pre-fill the user's project from preset values (A4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Use this example" now actually fills the new project, not just stores a ref. - studio-svc: CreateProjectAsync applies the chosen preset story's saved values after the template-graph copy. ApplyPresetValuesAsync reads content.preset_stories.scenes_spa = { values: {contentKey:value}, colors: {elementKey:hex} } and overlays them onto studio.saved_scene_contents (by key) + saved_shared_colors/saved_scene_colors (by element_key, is_selected). Keys are globally unique (AE convention) so key-only matching is safe. Malformed scenes_spa is skipped (defaults kept). Runs in the create tx. - admin UI: ProjectPresetStories raw scenes_spa textarea replaced with a structured PresetValueEditor — loads each preset scene's content elements + the project's shared colours and renders a type-aware input per item (text/textarea/number, media→upload, fill/color→colour). Serializes to scenes_spa {values,colors}; parses it back on edit. Verified e2e: authored a preset with values+colour → used it → the new project's saved_scene_contents + saved_shared_colors carry the preset values (which the B2 render binder then writes into AE). Co-Authored-By: Claude Opus 4.8 --- .../Application/Services/StudioService.cs | 58 ++++++++ src/components/admin/ProjectPresetStories.tsx | 137 ++++++++++++++++-- 2 files changed, 186 insertions(+), 9 deletions(-) diff --git a/services/studio/FlatRender.StudioSvc/Application/Services/StudioService.cs b/services/studio/FlatRender.StudioSvc/Application/Services/StudioService.cs index b948430..0efd64c 100644 --- a/services/studio/FlatRender.StudioSvc/Application/Services/StudioService.cs +++ b/services/studio/FlatRender.StudioSvc/Application/Services/StudioService.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using FlatRender.StudioSvc.Domain.Entities; using FlatRender.StudioSvc.Domain.Enums; using FlatRender.StudioSvc.Infrastructure.Data; @@ -91,10 +92,67 @@ public class StudioService(StudioDbContext db) if (req.CopyDefaultValues && req.OriginalProjectId != Guid.Empty) await CopyTemplateGraphAsync(project.Id, req.OriginalProjectId); + // Pre-fill from the chosen preset story (premade example video): overlay its + // saved values onto the freshly-copied scene contents + colours. + if (req.PresetStoryId.HasValue) + await ApplyPresetValuesAsync(project.Id, req.PresetStoryId.Value); + await tx.CommitAsync(); return await GetProjectAsync(project.Id, userId); } + /// + /// Applies an admin-authored preset story's filled values onto a freshly-created + /// project. The preset stores them in content.preset_stories.scenes_spa as + /// { "values": { contentKey: value }, "colors": { elementKey: hex } }. + /// Content/colour keys are globally unique (AE naming convention), so matching by + /// key alone reaches the right scene element. Runs inside the create transaction. + /// + private async Task ApplyPresetValuesAsync(Guid savedProjectId, Guid presetStoryId) + { + var spa = await db.Database + .SqlQuery($@"SELECT scenes_spa AS ""Value"" FROM content.preset_stories + WHERE id = {presetStoryId} AND deleted_at IS NULL") + .FirstOrDefaultAsync(); + if (string.IsNullOrWhiteSpace(spa)) return; + + JsonElement root; + try { root = JsonDocument.Parse(spa).RootElement; } + catch { return; } // malformed scenes_spa → skip pre-fill, leave defaults + if (root.ValueKind != JsonValueKind.Object) return; + + // content element values → saved_scene_contents.value (match by key) + if (root.TryGetProperty("values", out var vals) && vals.ValueKind == JsonValueKind.Object) + { + var valuesJson = vals.GetRawText(); + await db.Database.ExecuteSqlInterpolatedAsync($@" + UPDATE studio.saved_scene_contents c + SET value = pv.value, updated_at = now() + FROM json_each_text({valuesJson}::json) AS pv(key, value) + WHERE c.key = pv.key + AND c.saved_scene_id IN ( + SELECT id FROM studio.saved_scenes WHERE saved_project_id = {savedProjectId});"); + } + + // colour values → shared + per-scene colours (match by element_key) + if (root.TryGetProperty("colors", out var cols) && cols.ValueKind == JsonValueKind.Object) + { + var colorsJson = cols.GetRawText(); + await db.Database.ExecuteSqlInterpolatedAsync($@" + UPDATE studio.saved_shared_colors sc + SET value = pc.value, is_selected = true + FROM json_each_text({colorsJson}::json) AS pc(key, value) + WHERE sc.element_key = pc.key AND sc.saved_project_id = {savedProjectId};"); + await db.Database.ExecuteSqlInterpolatedAsync($@" + UPDATE studio.saved_scene_colors c + SET value = pc.value, is_selected = true + FROM json_each_text({colorsJson}::json) AS pc(key, value) + WHERE c.element_key = pc.key + AND c.saved_scene_id IN ( + SELECT id FROM studio.saved_scenes WHERE saved_project_id = {savedProjectId});"); + } + } + /// /// Copies a content template project's scenes, content elements and colour elements /// into a freshly-created editable project. Runs inside the caller's transaction; diff --git a/src/components/admin/ProjectPresetStories.tsx b/src/components/admin/ProjectPresetStories.tsx index 79fa62f..c5bacdf 100644 --- a/src/components/admin/ProjectPresetStories.tsx +++ b/src/components/admin/ProjectPresetStories.tsx @@ -19,11 +19,24 @@ interface Scene { id: string; key: string; title: string; default_duration_sec?: type Draft = { name: string; description: string; demo: string; is_published: boolean; sort: number; - scenes_spa: string; scenes: PresetScene[]; + // Filled values the user's project starts with: content-element key → value, and + // colour element key → hex. Serialized into scenes_spa = { values, colors }. + values: Record; colors: Record; scenes: PresetScene[]; }; function emptyDraft(sort: number): Draft { - return { name: "", description: "", demo: "", is_published: true, sort, scenes_spa: "", scenes: [] }; + return { name: "", description: "", demo: "", is_published: true, sort, values: {}, colors: {}, scenes: [] }; +} + +function parseSpa(spa?: string | null): { values: Record; colors: Record } { + if (!spa) return { values: {}, colors: {} }; + try { + const o = JSON.parse(spa); + return { + values: o && typeof o.values === "object" && o.values ? o.values : {}, + colors: o && typeof o.colors === "object" && o.colors ? o.colors : {}, + }; + } catch { return { values: {}, colors: {} }; } } function num(v: number | null | undefined) { return v === null || v === undefined ? "" : String(v); } @@ -57,9 +70,11 @@ export function ProjectPresetStories({ projectId }: { projectId: string }) { const full: StoryFull | null = await fetch(`${base}/${s.id}`, { cache: "no-store" }).then((x) => x.json()).catch(() => null); if (!full) { setErr("بارگذاری ویدیوی نمونه ناموفق بود"); return; } setEditId(s.id); + const spa = parseSpa(full.scenes_spa); setDraft({ name: full.name, description: full.description ?? "", demo: full.demo ?? "", - is_published: full.is_published, sort: full.sort, scenes_spa: full.scenes_spa ?? "", + is_published: full.is_published, sort: full.sort, + values: spa.values, colors: spa.colors, scenes: (full.scenes ?? []).map((p) => ({ ...p })), }); }; @@ -70,10 +85,11 @@ export function ProjectPresetStories({ projectId }: { projectId: string }) { if (!draft) return; if (!draft.name.trim()) { setErr("نام ویدیوی نمونه الزامی است"); return; } setSaving(true); setErr(null); + const hasFilled = Object.keys(draft.values).length > 0 || Object.keys(draft.colors).length > 0; const body = { project_id: projectId, name: draft.name.trim(), description: draft.description || null, demo: draft.demo || null, is_published: draft.is_published, sort: draft.sort ?? 0, - scenes_spa: draft.scenes_spa || null, + scenes_spa: hasFilled ? JSON.stringify({ values: draft.values, colors: draft.colors }) : null, scenes: draft.scenes.map((s, i) => ({ scene_id: s.scene_id, sort: i, default_duration_sec: s.default_duration_sec ?? null })), }; const res = await fetch(editId ? `${base}/${editId}` : base, { @@ -147,11 +163,15 @@ export function ProjectPresetStories({ projectId }: { projectId: string }) { ))} -
- مقادیر آماده (JSON پیشرفته) — اختیاری -