mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-14 18:01:20 +08:00
578d6202b4
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.
85 lines
3.6 KiB
TypeScript
85 lines
3.6 KiB
TypeScript
/**
|
|
* Wires the Design panel's promote-to-variable context. Promote/bind operates
|
|
* on the file the selected element actually lives in — a sub-composition file
|
|
* when you select an element inside an inlined sub-comp, not the host. So we
|
|
* open (and persist to) an SDK session keyed on `selection.sourceFile`, not the
|
|
* host `activeCompPath`. Declaring a variable therefore lands in the sub-comp's
|
|
* own file, making it a knob on that reusable frame everywhere it is used. When
|
|
* nothing is selected (or the element is top-level) the target is the active
|
|
* composition, so behavior there is unchanged.
|
|
*/
|
|
|
|
import { useCallback, type ReactNode } from "react";
|
|
import type { Composition } from "@hyperframes/sdk";
|
|
import type { DomEditSelection } from "./editor/domEditingTypes";
|
|
import { useSdkSession } from "../hooks/useSdkSession";
|
|
import { useVariablesPersist, type UseVariablesPersistParams } from "../hooks/useVariablesPersist";
|
|
import { VariablePromoteProvider } from "../contexts/VariablePromoteContext";
|
|
import { getStudioSaveErrorMessage } from "../utils/studioSaveDiagnostics";
|
|
|
|
/** Persist wiring minus the target — this provider derives the target from the selection. */
|
|
type PersistDeps = Omit<UseVariablesPersistParams, "sdkSession" | "activeCompPath">;
|
|
|
|
export function DesignPanelPromoteProvider({
|
|
selection,
|
|
projectId,
|
|
activeCompPath,
|
|
showToast,
|
|
forceReloadSharedSdkSession,
|
|
children,
|
|
...persistDeps
|
|
}: PersistDeps & {
|
|
selection: DomEditSelection | null;
|
|
projectId: string | null;
|
|
activeCompPath: string | null;
|
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
/**
|
|
* Forces the app's SHARED SDK session (Variables tab, Slideshow, etc.) to
|
|
* re-open from disk. This provider opens its OWN session, separate from
|
|
* that shared one, so a persist through it never fires the shared session's
|
|
* own "change" event. When the target happens to be the same file the
|
|
* shared session already has open, that session is left holding stale
|
|
* in-memory content after a successful persist — worse, the self-write-echo
|
|
* registry that would normally reload it on the next file-change
|
|
* notification is keyed by file path only (not by session instance), so it
|
|
* mistakes this provider's write for its own echo and stays stale
|
|
* indefinitely. Called unconditionally (not gated on targetPath matching
|
|
* activeCompPath): re-opening a file that didn't change is a harmless
|
|
* no-op re-parse, cheaper than the bug class a subtly-wrong path guard
|
|
* could reintroduce.
|
|
*/
|
|
forceReloadSharedSdkSession?: () => void;
|
|
children: ReactNode;
|
|
}) {
|
|
const targetPath = selection?.sourceFile || activeCompPath || "index.html";
|
|
const handle = useSdkSession(projectId, targetPath, persistDeps.domEditSaveTimestampRef);
|
|
const rawPersist = useVariablesPersist({
|
|
...persistDeps,
|
|
sdkSession: handle.session,
|
|
publishSdkSession: handle.publish,
|
|
activeCompPath: targetPath,
|
|
});
|
|
const persist = useCallback(
|
|
async (label: string, mutate: (session: Composition) => void) => {
|
|
const committed = await rawPersist(label, mutate);
|
|
if (committed) forceReloadSharedSdkSession?.();
|
|
return committed;
|
|
},
|
|
[rawPersist, forceReloadSharedSdkSession],
|
|
);
|
|
const handlePersistError = useCallback(
|
|
(error: unknown) => showToast(getStudioSaveErrorMessage(error), "error"),
|
|
[showToast],
|
|
);
|
|
return (
|
|
<VariablePromoteProvider
|
|
session={handle.session}
|
|
selection={selection}
|
|
persist={persist}
|
|
onPersistError={handlePersistError}
|
|
>
|
|
{children}
|
|
</VariablePromoteProvider>
|
|
);
|
|
}
|