Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/core/edit-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down
25 changes: 25 additions & 0 deletions tests/edit-load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading