fix(studio): a reload that names no files still reloads the whole film

Eight callers bump the preview reload directly. If one lands in the same render as a scene
edit, the swap would have shown the scene and missed the other change. The reload counter
moves into usePreviewReloadKey, whose plain setter always means the whole film.
This commit is contained in:
Miguel Ángel
2026-09-24 06:15:35 -04:00
parent 92b71f2e43
commit 31c9c440ab
3 changed files with 66 additions and 6 deletions
+2 -6
View File
@@ -1,5 +1,5 @@
import { useOwnPreviewIframe, usePreviewIframeStore } from "./player/store/previewIframeStore";
import { notePreviewReload } from "./player/sceneRemount";
import { usePreviewReloadKey } from "./player/sceneRemount";
import { buildProjectApiPath } from "./utils/projectRouting";
import { useState, useCallback, useRef, useMemo, useLayoutEffect } from "react";
import { useDismissingTabSetter, useRightPanelIntent } from "./hooks/useRightPanelIntents";
@@ -82,7 +82,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
const [compIdToSrc, setCompIdToSrc] = useState<Map<string, string>>(new Map());
const previewIframe = useOwnPreviewIframe();
const [compositionLoading, setCompositionLoading] = useState(true);
const [refreshKey, setRefreshKey] = useState(0);
const { refreshKey, setRefreshKey, reloadPreview } = usePreviewReloadKey();
const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion();
const [blockPreview, setBlockPreview] = useState<BlockPreviewInfo | null>(null);
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
@@ -107,10 +107,6 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
const handleDomZIndexReorderCommitRef = useRef<TimelineZIndexReorderCommit | null>(null);
const pendingTimelineEditPathRef = useRef(new Set<string>());
const isGestureRecordingRef = useRef(false);
const reloadPreview = useCallback((changedPaths?: readonly string[]) => {
notePreviewReload(Array.isArray(changedPaths) ? changedPaths : undefined);
setRefreshKey((k) => k + 1);
}, []);
const fileManager = useFileManager({
projectId,
showToast,
@@ -0,0 +1,45 @@
// @vitest-environment happy-dom
import { act, createElement } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it } from "vitest";
import { notePreviewReload, takePreviewReloadPaths, usePreviewReloadKey } from "./sceneRemount";
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
afterEach(() => void takePreviewReloadPaths());
describe("preview reload paths", () => {
it("collects the scene files named before the reload runs", () => {
notePreviewReload(["compositions/a.html"]);
notePreviewReload(["compositions/b.html"]);
expect(takePreviewReloadPaths()).toEqual(["compositions/a.html", "compositions/b.html"]);
expect(takePreviewReloadPaths()).toBeNull();
});
it("reloads the whole film when any reload in the same batch names no files", () => {
notePreviewReload(["compositions/a.html"]);
notePreviewReload();
notePreviewReload(["compositions/b.html"]);
expect(takePreviewReloadPaths()).toBeNull();
});
it("usePreviewReloadKey: a plain setRefreshKey bump in the same batch turns a scene swap into a film reload", () => {
let api: ReturnType<typeof usePreviewReloadKey> | null = null;
const Probe = () => {
api = usePreviewReloadKey();
return null;
};
const root = createRoot(document.createElement("div"));
act(() => root.render(createElement(Probe)));
act(() => api!.reloadPreview(["compositions/a.html"]));
expect(api!.refreshKey).toBe(1);
expect(takePreviewReloadPaths()).toEqual(["compositions/a.html"]);
act(() => {
api!.reloadPreview(["compositions/a.html"]);
api!.setRefreshKey((key) => key + 1);
});
expect(api!.refreshKey).toBe(3);
expect(takePreviewReloadPaths()).toBeNull();
act(() => root.unmount());
});
});
@@ -1,3 +1,5 @@
import { useCallback, useState, type SetStateAction } from "react";
// Which files the next preview reload is for. A reload with no paths reloads the whole film.
let pendingPaths: Set<string> | "film" | null = null;
@@ -48,3 +50,20 @@ function resolveUrl(path: string, base: string): string | null {
return null;
}
}
/**
* The preview reload counter. `reloadPreview(paths)` names the scene files that changed; every other
* bump, including `setRefreshKey`, reloads the whole film.
*/
export function usePreviewReloadKey() {
const [refreshKey, setKey] = useState(0);
const setRefreshKey = useCallback((update: SetStateAction<number>) => {
notePreviewReload();
setKey(update);
}, []);
const reloadPreview = useCallback((changedPaths?: readonly string[]) => {
notePreviewReload(Array.isArray(changedPaths) ? changedPaths : undefined);
setKey((key) => key + 1);
}, []);
return { refreshKey, setRefreshKey, reloadPreview };
}