mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
fix(studio): resync the shared SDK session after a Design-panel variable promote
Reported as "template variables are broken": binding an element's field to a variable via the flat inspector's "◇ var" promote chip (or editing an already-bound field's value) wrote the correct bytes to disk, but the Variables tab kept showing the pre-edit value until the whole Studio page was hard-reloaded. Root cause: DesignPanelPromoteProvider deliberately opens its OWN SDK session (`useSdkSession(projectId, selection.sourceFile ?? activeCompPath)`) so that promoting inside a sub-composition binds the variable in the sub-comp's own file, not the host's. For the common case — a top-level element, same file as `activeCompPath` — this session is a SEPARATE in-memory `Composition` instance from the shared one `VariablesPanel` (Variables tab, Slideshow, etc.) reads. A persist through the promote provider's session never fires the shared session's own "change" event. Worse, the shared session's file-change listener runs `isSelfWriteEcho(path, content)` to decide whether to reload — but `sdkSelfWriteRegistry` is keyed by file path only, not by session instance (its own doc comment assumes "the studio process has a single SDK session lifecycle at a time"). It sees the promote provider's write registered under the same path and concludes it's its own echo, permanently suppressing the reload it actually needs. Threaded `forceReloadSdkSession` (the same mechanism every other server-side-write path in Studio already uses for exactly this "resync after a write I didn't make myself" case) from App.tsx through StudioRightPanel into DesignPanelPromoteProvider, and call it after every successful promote/setDefault persist — unconditionally, not gated on the promote target matching activeCompPath, since re-opening a file that didn't change is a harmless no-op re-parse and a path-equality guard here already produced one subtly wrong comparison (activeCompPath can be null while the shared session still defaults to "index.html") before landing on this simpler version. Verified live: editing a variable-bound field's value now updates the Variables tab immediately, no reload required. App.tsx crossed the 600-line file-size gate after threading the new prop; extracted the tiny handleAddAssetAtPlayhead wrapper into its own useAddAssetAtPlayhead hook (with a regression test) to bring it back under. Full studio suite (2639 tests) green against a fresh main; typecheck/ oxlint/oxfmt clean.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { usePlayerStore } from "../player";
|
||||
import { useAddAssetAtPlayhead } from "./useAddAssetAtPlayhead";
|
||||
|
||||
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("useAddAssetAtPlayhead", () => {
|
||||
it("drops the asset on track 0 at the current playhead time", () => {
|
||||
usePlayerStore.getState().setCurrentTime(4.5);
|
||||
const handleTimelineAssetDrop = vi.fn();
|
||||
let addAsset: (assetPath: string) => unknown = () => undefined;
|
||||
|
||||
function Harness() {
|
||||
addAsset = useAddAssetAtPlayhead(handleTimelineAssetDrop);
|
||||
return null;
|
||||
}
|
||||
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
act(() => {
|
||||
root.render(React.createElement(Harness));
|
||||
});
|
||||
|
||||
act(() => {
|
||||
addAsset("assets/clip.mp4");
|
||||
});
|
||||
|
||||
expect(handleTimelineAssetDrop).toHaveBeenCalledWith("assets/clip.mp4", {
|
||||
start: 4.5,
|
||||
track: 0,
|
||||
});
|
||||
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useCallback } from "react";
|
||||
import type { TimelineElement } from "../player";
|
||||
import { usePlayerStore } from "../player";
|
||||
|
||||
/** Drops an asset onto track 0 at the current playhead time. */
|
||||
export function useAddAssetAtPlayhead(
|
||||
handleTimelineAssetDrop: (
|
||||
assetPath: string,
|
||||
placement: Pick<TimelineElement, "start" | "track">,
|
||||
durationOverride?: number,
|
||||
) => unknown,
|
||||
) {
|
||||
return useCallback(
|
||||
(assetPath: string) =>
|
||||
handleTimelineAssetDrop(assetPath, {
|
||||
start: usePlayerStore.getState().currentTime,
|
||||
track: 0,
|
||||
}),
|
||||
[handleTimelineAssetDrop],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user