feat(#42): FIX projects can't add scenes (studio + admin)
Build backend images / build content-svc (push) Failing after 57s
Build backend images / build file-svc (push) Failing after 56s
Build backend images / build gateway (push) Failing after 54s
Build backend images / build identity-svc (push) Failing after 1m0s
Build backend images / build notification-svc (push) Failing after 47s
Build backend images / build render-svc (push) Failing after 53s
Build backend images / build studio-svc (push) Failing after 57s

Template copy now carries choose_mode from the content project → studio store gets
chooseMode; AddSceneMenu returns null for FIX/MusicVisualizer. Admin ProjectScenes
hides '+ صحنهٔ جدید' (shows an 'scenes defined in AE' note) for fixed modes. Verified
choose_mode=FIX flows end-to-end. (Visible admin nav link added earlier.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 05:03:46 +03:30
parent bccebbd006
commit d56bcf1b23
6 changed files with 46 additions and 5 deletions
@@ -193,6 +193,14 @@ public class StudioService(StudioDbContext db)
FROM content.shared_colors sc
WHERE sc.project_id = {1};",
savedProjectId, originalProjectId);
// 5. carry the template's render mode (FIX/FLEXIBLE/…) so the studio knows whether
// adding scenes is allowed, plus a few useful project defaults.
await db.Database.ExecuteSqlRawAsync(@"
UPDATE studio.saved_projects sp
SET choose_mode = COALESCE((SELECT p.choose_mode::text FROM content.projects p WHERE p.id = {1}), sp.choose_mode)
WHERE sp.id = {0};",
savedProjectId, originalProjectId);
}
public async Task<SavedProjectFullResponse> UpdateProjectAsync(
+13 -4
View File
@@ -67,7 +67,10 @@ function sceneToDraft(s: Scene): SceneDraft {
}
// =============================================================================
export function ProjectScenes({ projectId }: { projectId: string }) {
// FIX / MusicVisualizer projects have a fixed scene set from After Effects layer names.
const FIXED_SCENE_MODES = new Set(["fix", "musicvisualizer"]);
export function ProjectScenes({ projectId, mode }: { projectId: string; mode?: string }) {
const [tab, setTab] = useState<"scenes" | "colors" | "presets">("scenes");
return (
<div dir="rtl">
@@ -80,7 +83,9 @@ export function ProjectScenes({ projectId }: { projectId: string }) {
>{l}</button>
))}
</div>
{tab === "scenes" && <ScenesTab projectId={projectId} />}
{tab === "scenes" && (
<ScenesTab projectId={projectId} fixedScenes={FIXED_SCENE_MODES.has((mode ?? "").toLowerCase())} />
)}
{tab === "colors" && <ColorsTab projectId={projectId} />}
{tab === "presets" && <PresetsTab projectId={projectId} />}
</div>
@@ -88,7 +93,7 @@ export function ProjectScenes({ projectId }: { projectId: string }) {
}
// ── Scenes ────────────────────────────────────────────────────────────────────
function ScenesTab({ projectId }: { projectId: string }) {
function ScenesTab({ projectId, fixedScenes }: { projectId: string; fixedScenes?: boolean }) {
const [rows, setRows] = useState<Scene[]>([]);
const [loading, setLoading] = useState(true);
const [draft, setDraft] = useState<SceneDraft | null>(null);
@@ -152,7 +157,11 @@ function ScenesTab({ projectId }: { projectId: string }) {
<p className="text-xs text-gray-500">صحنهها بلوکهای قابلویرایش این قالب هستند. کلید هر صحنه باید با نام کامپوزیشن افترافکت یکی باشد.</p>
<div className="flex shrink-0 items-center gap-2">
<button className="rounded-lg border border-indigo-500/40 px-3 py-2 text-sm text-indigo-300 hover:bg-indigo-600/10" onClick={() => setScanOpen(true)}>اسکن از افترافکت</button>
<button className={btn} onClick={() => { setEditId(null); setDraft(emptyDraft(rows.length)); }}>+ صحنهٔ جدید</button>
{fixedScenes ? (
<span className="rounded-lg border border-[#262b40] px-3 py-2 text-xs text-gray-500">صحنهها از روی پروژهٔ افترافکت تعریف میشوند (پروژهٔ Fix)</span>
) : (
<button className={btn} onClick={() => { setEditId(null); setDraft(emptyDraft(rows.length)); }}>+ صحنهٔ جدید</button>
)}
</div>
</div>
{scanOpen && (
+2 -1
View File
@@ -12,6 +12,7 @@ interface Proj {
id: string; container_id: string; container_name: string; container_slug: string;
name: string; image?: string | null; aspect?: string | null; resolution: string;
aep_file_url?: string | null; render_aep_comp: string; is_published: boolean; sort: number;
choose_mode?: string | null;
}
const card = "rounded-xl border border-[#1e2235] bg-[#0f1120]";
@@ -315,7 +316,7 @@ export function ProjectsAdmin() {
<button className="rounded-lg px-2 py-1 text-gray-400 hover:bg-[#161a2e] hover:text-white" onClick={() => setOpenScenes(null)}></button>
</div>
<div className="flex-1 overflow-y-auto p-5">
<ProjectScenes projectId={openScenes.id} />
<ProjectScenes projectId={openScenes.id} mode={openScenes.choose_mode ?? undefined} />
</div>
</div>
</div>
+10
View File
@@ -10,16 +10,26 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useStudioStore } from "@/lib/studio-store";
interface AddSceneMenuProps {
onAddBlank: () => void;
variant?: "header" | "footer";
}
// FIX / MusicVisualizer projects have a fixed scene set defined in After Effects
// (scenes come from layer names), so adding scenes is not allowed.
const FIXED_SCENE_MODES = new Set(["fix", "musicvisualizer"]);
export function AddSceneMenu({ onAddBlank, variant = "footer" }: AddSceneMenuProps) {
const t = useTranslations("auto.componentsStudioAddSceneMenu");
const [open, setOpen] = useState(false);
const isHeader = variant === "header";
const chooseMode = useStudioStore((s) => s.chooseMode);
if (FIXED_SCENE_MODES.has(chooseMode.toLowerCase())) {
return null; // fixed-structure project — no add-scene
}
return (
<Popover open={open} onOpenChange={setOpen}>
+9
View File
@@ -164,6 +164,9 @@ export interface VideoPersistedSceneData {
audioVolume?: number;
sceneBackgroundColor?: string;
sceneAccentColor?: string;
/** Project render mode (FIX / FLEXIBLE / MusicVisualizer / …). FIX/MusicVisualizer
* scenes come from AE layer names, so adding scenes is disabled. */
chooseMode?: string;
}
export function buildVideoSceneDataPayload(
@@ -214,5 +217,11 @@ export function parseVideoSceneData(
typeof sceneData.sceneAccentColor === "string"
? sceneData.sceneAccentColor
: DEFAULT_SCENE_ACCENT_COLOR,
chooseMode:
typeof sceneData.chooseMode === "string"
? sceneData.chooseMode
: typeof sceneData.primaryMode === "string"
? sceneData.primaryMode
: undefined,
};
}
+4
View File
@@ -159,6 +159,8 @@ export interface StudioState {
audioVolume: number;
sceneBackgroundColor: string;
sceneAccentColor: string;
/** Project render mode (FIX / FLEXIBLE / MusicVisualizer / …). Empty until hydrated. */
chooseMode: string;
past: StudioHistorySnapshot[];
future: StudioHistorySnapshot[];
layerClipboard: Layer | null;
@@ -249,6 +251,7 @@ export const useStudioStore = create<StudioStore>((set, get) => {
audioVolume: 100,
sceneBackgroundColor: DEFAULT_SCENE_BACKGROUND_COLOR,
sceneAccentColor: DEFAULT_SCENE_ACCENT_COLOR,
chooseMode: "",
past: [],
future: [],
layerClipboard: null,
@@ -757,6 +760,7 @@ export const useStudioStore = create<StudioStore>((set, get) => {
parsed.sceneBackgroundColor ?? DEFAULT_SCENE_BACKGROUND_COLOR,
sceneAccentColor:
parsed.sceneAccentColor ?? DEFAULT_SCENE_ACCENT_COLOR,
chooseMode: parsed.chooseMode ?? "",
past: [],
future: [],
});