mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
fix(studio): label undo history with the track display row
The timeline track key is a fractional z-order sort key: an expanded sub-composition child gets `host.track + n / (siblings + 2)`. The undo history entry for the eye toggle interpolated that key directly, so hiding an expanded child recorded "Hide track 0.16666666666666666". Give the key-to-display-row conversion a single owner (timelineTrackDisplay.ts) and route both the track header labels and the history label through it, so the two cannot drift apart again. The raw key still routes the callbacks and lookups that need it. Adds a regression test that toggles a track keyed 1 / 6; a test on track 0 formats cleanly and proves nothing.
This commit is contained in:
@@ -95,7 +95,9 @@ describe("toggleTimelineTrackHidden", () => {
|
||||
expect(pendingRef.current).toEqual(new Set(["index.html", "scene.html"]));
|
||||
expect(timestampRef.current).toBeGreaterThan(0);
|
||||
expect(recordEdit).toHaveBeenCalledTimes(1);
|
||||
expect(recordEdit.mock.calls[0]?.[0]?.label).toBe("Hide track 0");
|
||||
// Display row, not the raw key: track 0 is the first row, so it reads "1",
|
||||
// the same number the track header announces for that row.
|
||||
expect(recordEdit.mock.calls[0]?.[0]?.label).toBe("Hide track 1");
|
||||
expect(Object.keys(recordEdit.mock.calls[0]?.[0]?.files ?? {}).sort()).toEqual([
|
||||
"index.html",
|
||||
"scene.html",
|
||||
@@ -134,6 +136,64 @@ describe("toggleTimelineTrackHidden", () => {
|
||||
|
||||
expect(writes.get("index.html")).not.toContain("data-hidden");
|
||||
});
|
||||
|
||||
// An expanded sub-comp child gets a synthesized FRACTIONAL track key
|
||||
// (`display.track + n / (siblings + 2)`), so a label built from the raw key
|
||||
// reads "Hide track 0.16666666666666666" in the undo history. Track 0 would
|
||||
// format cleanly and prove nothing, hence 1 / 6.
|
||||
it("labels the undo entry with the display row, not the fractional track key", async () => {
|
||||
const files = new Map([
|
||||
["index.html", `<div id="child" data-start="0" data-duration="2"></div>`],
|
||||
]);
|
||||
stubProjectFiles(files);
|
||||
|
||||
const recordEdit = vi.fn();
|
||||
|
||||
await toggleTimelineTrackHidden({
|
||||
projectId: "project-1",
|
||||
activeCompPath: "index.html",
|
||||
timelineElements: [
|
||||
element({ id: "host", domId: "host", track: 0 }),
|
||||
element({ id: "child", domId: "child", track: 1 / 6 }),
|
||||
],
|
||||
track: 1 / 6,
|
||||
hidden: true,
|
||||
previewIframe: null,
|
||||
writeProjectFile: async () => {},
|
||||
recordEdit,
|
||||
domEditSaveTimestampRef: { current: 0 },
|
||||
pendingTimelineEditPathRef: { current: new Set() },
|
||||
});
|
||||
|
||||
expect(recordEdit.mock.calls[0]?.[0]?.label).toBe("Hide track 2");
|
||||
});
|
||||
|
||||
it("labels a show back with the display row too", async () => {
|
||||
const files = new Map([
|
||||
["index.html", `<div id="child" data-start="0" data-duration="2" data-hidden=""></div>`],
|
||||
]);
|
||||
stubProjectFiles(files);
|
||||
|
||||
const recordEdit = vi.fn();
|
||||
|
||||
await toggleTimelineTrackHidden({
|
||||
projectId: "project-1",
|
||||
activeCompPath: "index.html",
|
||||
timelineElements: [
|
||||
element({ id: "host", domId: "host", track: 0 }),
|
||||
element({ id: "child", domId: "child", track: 1 / 6, hidden: true }),
|
||||
],
|
||||
track: 1 / 6,
|
||||
hidden: false,
|
||||
previewIframe: null,
|
||||
writeProjectFile: async () => {},
|
||||
recordEdit,
|
||||
domEditSaveTimestampRef: { current: 0 },
|
||||
pendingTimelineEditPathRef: { current: new Set() },
|
||||
});
|
||||
|
||||
expect(recordEdit.mock.calls[0]?.[0]?.label).toBe("Show track 2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleTimelineElementHidden", () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { usePlayerStore, type TimelineElement } from "../player";
|
||||
import { useExpandedTimelineElements } from "../player/hooks/useExpandedTimelineElements";
|
||||
import { timelineTrackOrder, trackDisplayNumber } from "../player/components/timelineTrackDisplay";
|
||||
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
||||
import { readTagSnippetByTarget, type PatchOperation } from "../utils/sourcePatcher";
|
||||
import {
|
||||
@@ -208,12 +209,15 @@ export async function toggleTimelineTrackHidden({
|
||||
domEditSaveTimestampRef,
|
||||
pendingTimelineEditPathRef,
|
||||
}: ToggleTimelineTrackHiddenInput): Promise<string[]> {
|
||||
// `track` is the fractional sort key the callback needs; the history entry is
|
||||
// read by a human, so it gets the display row instead.
|
||||
const displayNumber = trackDisplayNumber(timelineTrackOrder(timelineElements), track);
|
||||
return setElementsHidden({
|
||||
projectId,
|
||||
activeCompPath,
|
||||
elements: timelineElements.filter((element) => element.track === track),
|
||||
hidden,
|
||||
label: hidden ? `Hide track ${track}` : `Show track ${track}`,
|
||||
label: hidden ? `Hide track ${displayNumber}` : `Show track ${displayNumber}`,
|
||||
previewIframe,
|
||||
writeProjectFile,
|
||||
recordEdit,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TimelineClipDiamonds } from "./TimelineClipDiamonds";
|
||||
import { TimelinePropertyLanes } from "./TimelinePropertyLanes";
|
||||
import { TimelineTrackHeader } from "./TimelineTrackHeader";
|
||||
import { resolveTrackKeyframeClip } from "./useTimelineTrackLayout";
|
||||
import { trackDisplayNumber } from "./timelineTrackDisplay";
|
||||
import { clipTimingStart } from "../../hooks/gsapShared";
|
||||
import { getTimelineEditCapabilities, resolveBlockedTimelineEditIntent } from "./timelineEditing";
|
||||
import { CLIP_Y, CLIP_HANDLE_W, TRACK_H, getTimelineRowHeight } from "./timelineLayout";
|
||||
@@ -105,6 +106,7 @@ export function TimelineLanes({
|
||||
// in a virtualizer if editorial workflows ever push very high clip counts.
|
||||
// fallow-ignore-next-line complexity
|
||||
displayTrackOrder.map((trackNum, row) => {
|
||||
const displayNumber = trackDisplayNumber(displayTrackOrder, trackNum);
|
||||
const rowHeight = getTimelineRowHeight(row, rowHeights);
|
||||
const els = tracks.find(([t]) => t === trackNum)?.[1] ?? [];
|
||||
const ts = trackStyles.get(trackNum) ?? getTrackStyle("");
|
||||
@@ -150,8 +152,10 @@ export function TimelineLanes({
|
||||
trackNumber={trackNum}
|
||||
// What gets announced. `trackNum` is a fractional z-order sort
|
||||
// key, so it stays out of every label and in every callback.
|
||||
trackDisplayNumber={row + 1}
|
||||
trackLabel={els[0]?.label ?? els[0]?.domId ?? els[0]?.id ?? `Track ${row + 1}`}
|
||||
trackDisplayNumber={displayNumber}
|
||||
trackLabel={
|
||||
els[0]?.label ?? els[0]?.domId ?? els[0]?.id ?? `Track ${displayNumber}`
|
||||
}
|
||||
lanesId={lanesId}
|
||||
contentOrigin={contentOrigin}
|
||||
keyframeClip={keyframeClip}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* The one owner of "what track number does the user see".
|
||||
*
|
||||
* `TimelineElement.track` is a z-order SORT key, not a row number: an expanded
|
||||
* sub-composition child gets a synthesized fractional key (`host.track + n /
|
||||
* (siblings + 2)`, see `useExpandedTimelineElements`), so putting it in a string
|
||||
* announces "Hide track 0.16666666666666666". Every user-visible track number,
|
||||
* whether it is rendered by a component or baked into an undo-history label,
|
||||
* routes through here; the raw key stays in callbacks and lookups only.
|
||||
*/
|
||||
|
||||
/** Ascending distinct track keys, the row order the timeline renders in. */
|
||||
export function timelineTrackOrder(elements: readonly { track: number }[]): number[] {
|
||||
return [...new Set(elements.map((element) => element.track))].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* A track key's 1-based display row. A key not in `trackOrder` (a drag preview
|
||||
* onto a brand-new track, say) reads as the row it would land on at the end.
|
||||
*/
|
||||
export function trackDisplayNumber(trackOrder: readonly number[], track: number): number {
|
||||
const row = trackOrder.indexOf(track);
|
||||
return row < 0 ? trackOrder.length + 1 : row + 1;
|
||||
}
|
||||
Reference in New Issue
Block a user