From bdbd815b07348b61cd42b87d34a5df1512b5ad60 Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Thu, 2 Jul 2026 21:12:18 +1000 Subject: [PATCH] fix: resolve existing placeholders when merge fields are registered after load --- src/core/edit-session.ts | 18 ++++++++++++++++++ tests/edit-load.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/core/edit-session.ts b/src/core/edit-session.ts index 169c851..10930be 100644 --- a/src/core/edit-session.ts +++ b/src/core/edit-session.ts @@ -2297,6 +2297,24 @@ export class Edit { return this.mergeFieldService.resolve(input); } + /** + * Re-detect merge field placeholders across the document and re-resolve the canvas. + * Use after registering or updating fields directly on the merge field service (rather + * than through a clip-level command): clips that already contain `{{ FIELD }}` + * placeholders pick up their resolved values immediately, without a reload. + */ + public refreshMergeFields(): void { + const bindingsPerClip = this.detectMergeFieldBindings(this.mergeFieldService.toSerializedArray()); + for (const [clipId, bindings] of bindingsPerClip) { + if (bindings.size > 0) { + this.document.setClipBindingsForClip(clipId, bindings); + } + } + // resolve() recomputes the resolved edit and emits Resolved — the player + // reconciler and timeline react to that event and repaint with the new values. + this.resolve(); + } + // ─── Template Edit Access (via document bindings) ────────────────────────── /* @internal Get the exportable clip (with merge field placeholders restored) */ diff --git a/tests/edit-load.test.ts b/tests/edit-load.test.ts index 3b015b1..234f3d1 100644 --- a/tests/edit-load.test.ts +++ b/tests/edit-load.test.ts @@ -748,6 +748,31 @@ describe("Edit loadEdit()", () => { const player = edit.getPlayerClip(0, 0); expect(player?.clipConfiguration.asset).toHaveProperty("src", "https://example.com/img.jpg"); }); + + it("refreshMergeFields resolves placeholders registered after load", async () => { + const shotstackEdit = new ShotstackEdit({ + timeline: { + tracks: [{ clips: [{ asset: { type: "image", src: "{{ MEDIA_URL }}" }, start: 0, length: 3, fit: "crop" }] }] + }, + output: { size: { width: 1920, height: 1080 }, format: "mp4" } + }); + await shotstackEdit.load(); + + // No merge array at load → the placeholder stays literal in the resolved edit. + const before = shotstackEdit.getEdit({ includeIds: true }); + const clipId = (before.timeline?.tracks?.[0]?.clips?.[0] as { id?: string })?.id as string; + expect(shotstackEdit.getResolvedClipById(clipId)?.asset).toHaveProperty("src", "{{ MEDIA_URL }}"); + + shotstackEdit.mergeFields.register({ name: "MEDIA_URL", defaultValue: "https://resolved.example.com/img.jpg" }); + shotstackEdit.refreshMergeFields(); + + // Resolved side picks up the default; the document keeps the placeholder and the field. + expect(shotstackEdit.getResolvedClipById(clipId)?.asset).toHaveProperty("src", "https://resolved.example.com/img.jpg"); + const after = shotstackEdit.getEdit(); + const documentAsset = after.timeline?.tracks?.[0]?.clips?.[0]?.asset as { src?: string } | undefined; + expect(documentAsset?.src).toBe("{{ MEDIA_URL }}"); + expect(after.merge).toEqual([{ find: "MEDIA_URL", replace: "https://resolved.example.com/img.jpg" }]); + }); }); describe("fonts", () => {