@hyperframes/studio 0.7.58 → 0.7.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-CtTDO63S.js → hyperframes-player-3XTTaVNf.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
- package/dist/assets/index-DXbu6IPT.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
- package/dist/assets/index-cH6NfVV_.js +426 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13755 -8627
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +42 -53
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +107 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.ts +2 -2
- package/src/hooks/timelineTrackVisibility.test.ts +52 -0
- package/src/hooks/timelineTrackVisibility.ts +27 -10
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +41 -1
- package/src/hooks/useDomEditCommits.ts +31 -177
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useEditorSave.ts +1 -1
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
- package/src/hooks/useFileManager.ts +151 -65
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePanelLayout.test.ts +118 -0
- package/src/hooks/usePanelLayout.ts +24 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +4 -1
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useRazorSplit.history.test.tsx +12 -4
- package/src/hooks/useRazorSplit.test.tsx +88 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
- package/src/hooks/useRazorSplit.ts +50 -16
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +4 -1
- package/src/hooks/useTimelineEditing.ts +18 -10
- package/src/hooks/useTimelineEditingTypes.ts +5 -1
- package/src/hooks/useTimelineGroupEditing.ts +17 -3
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.test.ts +19 -0
- package/src/utils/domEditSaveQueue.ts +2 -1
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +511 -62
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +293 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +34 -29
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioFileVersion.test.ts +36 -0
- package/src/utils/studioFileVersion.ts +23 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.test.ts +18 -0
- package/src/utils/studioSaveDiagnostics.ts +38 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -28,6 +28,7 @@ vi.mock("../utils/studioTelemetry", () => ({
|
|
|
28
28
|
|
|
29
29
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
30
30
|
import type { MutationResult } from "./gsapScriptCommitTypes";
|
|
31
|
+
import { persistSdkSerialize } from "../utils/sdkCutover";
|
|
31
32
|
import { applyPreviewSync, useGsapScriptCommits } from "./useGsapScriptCommits";
|
|
32
33
|
|
|
33
34
|
// ── applyPreviewSync (pure preview-sync decision) ────────────────────────────
|
|
@@ -214,7 +215,11 @@ type HookApi = ReturnType<typeof useGsapScriptCommits>;
|
|
|
214
215
|
|
|
215
216
|
let cleanup: (() => void) | null = null;
|
|
216
217
|
|
|
217
|
-
function renderCommitHook(
|
|
218
|
+
function renderCommitHook(
|
|
219
|
+
options: {
|
|
220
|
+
writeProjectFile?: (path: string, content: string) => Promise<void>;
|
|
221
|
+
} = {},
|
|
222
|
+
) {
|
|
218
223
|
const reloadPreview = vi.fn();
|
|
219
224
|
const onCacheInvalidate = vi.fn();
|
|
220
225
|
const onFileContentChanged = vi.fn();
|
|
@@ -235,7 +240,7 @@ function renderCommitHook() {
|
|
|
235
240
|
onFileContentChanged,
|
|
236
241
|
showToast,
|
|
237
242
|
sdkSession: null,
|
|
238
|
-
writeProjectFile:
|
|
243
|
+
writeProjectFile: options.writeProjectFile,
|
|
239
244
|
forceReloadSdkSession,
|
|
240
245
|
});
|
|
241
246
|
return null;
|
|
@@ -422,4 +427,45 @@ describe("runCommit — instantPatch wiring", () => {
|
|
|
422
427
|
expect(applySoftReload).toHaveBeenCalledTimes(1);
|
|
423
428
|
expect(deps.onCacheInvalidate).toHaveBeenCalledTimes(1);
|
|
424
429
|
});
|
|
430
|
+
|
|
431
|
+
it("serializes a legacy fallback request behind an in-flight SDK whole-file edit", async () => {
|
|
432
|
+
let releaseSdkWrite: (() => void) | undefined;
|
|
433
|
+
const sdkWriteGate = new Promise<void>((resolve) => {
|
|
434
|
+
releaseSdkWrite = resolve;
|
|
435
|
+
});
|
|
436
|
+
let notifySdkWriteStarted: (() => void) | undefined;
|
|
437
|
+
const sdkWriteStarted = new Promise<void>((resolve) => {
|
|
438
|
+
notifySdkWriteStarted = resolve;
|
|
439
|
+
});
|
|
440
|
+
const writeProjectFile = vi.fn(async () => {
|
|
441
|
+
notifySdkWriteStarted?.();
|
|
442
|
+
await sdkWriteGate;
|
|
443
|
+
});
|
|
444
|
+
const deps = renderCommitHook({ writeProjectFile });
|
|
445
|
+
const sdkEdit = persistSdkSerialize(() => "SDK_AFTER", "index.html", "BEFORE", {
|
|
446
|
+
editHistory: { recordEdit: deps.recordEdit },
|
|
447
|
+
writeProjectFile,
|
|
448
|
+
readProjectFile: vi.fn(async () => "BEFORE"),
|
|
449
|
+
reloadPreview: deps.reloadPreview,
|
|
450
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
451
|
+
});
|
|
452
|
+
await sdkWriteStarted;
|
|
453
|
+
|
|
454
|
+
mockFetchResult();
|
|
455
|
+
let legacyEdit: Promise<void> | undefined;
|
|
456
|
+
act(() => {
|
|
457
|
+
legacyEdit = deps.api.commitMutation(selection, { x: 10 }, { label: "Legacy fallback" });
|
|
458
|
+
});
|
|
459
|
+
await Promise.resolve();
|
|
460
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
461
|
+
|
|
462
|
+
releaseSdkWrite?.();
|
|
463
|
+
await act(async () => {
|
|
464
|
+
await Promise.all([sdkEdit, legacyEdit]);
|
|
465
|
+
});
|
|
466
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
467
|
+
"/api/projects/proj-1/gsap-mutations/index.html",
|
|
468
|
+
expect.objectContaining({ method: "POST" }),
|
|
469
|
+
);
|
|
470
|
+
});
|
|
425
471
|
});
|
|
@@ -6,6 +6,12 @@ import { usePlayerStore } from "../player/store/playerStore";
|
|
|
6
6
|
import { applySoftReload, extractGsapScriptText } from "../utils/gsapSoftReload";
|
|
7
7
|
import type { SoftReloadResult } from "../utils/gsapSoftReload";
|
|
8
8
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
9
|
+
import { serializeStudioFileMutation } from "../utils/studioFileMutationCoordinator";
|
|
10
|
+
import {
|
|
11
|
+
getStudioSaveErrorMessage,
|
|
12
|
+
isStudioSaveErrorAlreadyToasted,
|
|
13
|
+
markStudioSaveErrorAlreadyToasted,
|
|
14
|
+
} from "../utils/studioSaveDiagnostics";
|
|
9
15
|
import type { CutoverDeps } from "../utils/sdkCutover";
|
|
10
16
|
import { updateKeyframeCacheFromParsed } from "./gsapKeyframeCacheHelpers";
|
|
11
17
|
import { patchRuntimeTweenInPlace } from "./gsapRuntimePatch";
|
|
@@ -81,15 +87,19 @@ async function runMutationRequest(
|
|
|
81
87
|
if (unsafeFields.length > 0) {
|
|
82
88
|
showToast?.("Couldn't read element layout — try again at a different playhead time", "error");
|
|
83
89
|
if (options.skipReload) return;
|
|
84
|
-
throw
|
|
85
|
-
|
|
90
|
+
throw markStudioSaveErrorAlreadyToasted(
|
|
91
|
+
new Error(
|
|
92
|
+
`Mutation contains unsafe values: ${unsafeFields.map((field) => field.path).join(", ")}`,
|
|
93
|
+
),
|
|
86
94
|
);
|
|
87
95
|
}
|
|
88
96
|
try {
|
|
89
97
|
return await request();
|
|
90
98
|
} catch (error) {
|
|
91
|
-
if (error instanceof GsapMutationHttpError)
|
|
99
|
+
if (error instanceof GsapMutationHttpError) {
|
|
92
100
|
showToast?.(formatGsapMutationRejectionToast(error), "error");
|
|
101
|
+
markStudioSaveErrorAlreadyToasted(error);
|
|
102
|
+
}
|
|
93
103
|
if (options.skipReload) return;
|
|
94
104
|
throw error;
|
|
95
105
|
}
|
|
@@ -120,6 +130,54 @@ function refreshMutationPreview(
|
|
|
120
130
|
onCacheInvalidate();
|
|
121
131
|
}
|
|
122
132
|
|
|
133
|
+
function isActiveCommitTarget(
|
|
134
|
+
projectIdRef: { current: string | null },
|
|
135
|
+
activeCompPathRef: { current: string | null },
|
|
136
|
+
projectId: string,
|
|
137
|
+
compositionPath: string | null,
|
|
138
|
+
): boolean {
|
|
139
|
+
return projectIdRef.current === projectId && activeCompPathRef.current === compositionPath;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function syncCommittedGsapMutation({
|
|
143
|
+
iframe,
|
|
144
|
+
selection,
|
|
145
|
+
mutation,
|
|
146
|
+
targetPath,
|
|
147
|
+
result,
|
|
148
|
+
options,
|
|
149
|
+
onFileContentChanged,
|
|
150
|
+
forceReloadSdkSession,
|
|
151
|
+
reloadPreview,
|
|
152
|
+
onCacheInvalidate,
|
|
153
|
+
}: {
|
|
154
|
+
iframe: HTMLIFrameElement | null;
|
|
155
|
+
selection: DomEditSelection;
|
|
156
|
+
mutation: Record<string, unknown>;
|
|
157
|
+
targetPath: string;
|
|
158
|
+
result: MutationResult;
|
|
159
|
+
options: CommitMutationOptions;
|
|
160
|
+
onFileContentChanged?: (path: string, content: string) => void;
|
|
161
|
+
forceReloadSdkSession?: () => void;
|
|
162
|
+
reloadPreview: () => void;
|
|
163
|
+
onCacheInvalidate: () => void;
|
|
164
|
+
}): void {
|
|
165
|
+
if (result.after != null) onFileContentChanged?.(targetPath, result.after);
|
|
166
|
+
// Server wrote the file; the in-memory SDK doc is now stale. Resync it so a
|
|
167
|
+
// later SDK-routed edit doesn't serialize the pre-write doc and revert this.
|
|
168
|
+
forceReloadSdkSession?.();
|
|
169
|
+
if (options.skipReload) return;
|
|
170
|
+
if (result.parsed?.animations) {
|
|
171
|
+
updateKeyframeCacheFromParsed(
|
|
172
|
+
result.parsed.animations,
|
|
173
|
+
targetPath,
|
|
174
|
+
selection.id ?? undefined,
|
|
175
|
+
mutation,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
refreshMutationPreview(iframe, result, options, reloadPreview, onCacheInvalidate);
|
|
179
|
+
}
|
|
180
|
+
|
|
123
181
|
/**
|
|
124
182
|
* Apply a soft reload and enforce the U4 invariant via the richer
|
|
125
183
|
* `SoftReloadResult`, with telemetry on every non-success path so the invariant
|
|
@@ -208,7 +266,10 @@ export function applyPreviewSync(
|
|
|
208
266
|
|
|
209
267
|
// oxfmt-ignore
|
|
210
268
|
// fallow-ignore-next-line complexity
|
|
211
|
-
export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef, editHistory, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, showToast, sdkSession, writeProjectFile, forceReloadSdkSession }: GsapScriptCommitsParams) {
|
|
269
|
+
export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef, editHistory, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, showToast, sdkSession, publishSdkSession, writeProjectFile, forceReloadSdkSession }: GsapScriptCommitsParams) {
|
|
270
|
+
const activeProjectId = projectIdRef.current;
|
|
271
|
+
const activeCompPathRef = useRef(activeCompPath);
|
|
272
|
+
activeCompPathRef.current = activeCompPath;
|
|
212
273
|
// Serializer for per-key commits (options.serializeKey). Keyed by
|
|
213
274
|
// `gsap:${animationId}:meta`, it chains a meta commit onto the prior one for
|
|
214
275
|
// the same animationId so their POSTs can't interleave. Held in a ref so the
|
|
@@ -225,69 +286,115 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
|
|
|
225
286
|
});
|
|
226
287
|
}, [editHistory]);
|
|
227
288
|
|
|
228
|
-
const finalizeSuccessfulMutation = useCallback(async (selection: DomEditSelection, mutation: Record<string, unknown>, targetPath: string, result: MutationResult, options: CommitMutationOptions) => {
|
|
289
|
+
const finalizeSuccessfulMutation = useCallback(async (projectId: string, compositionPath: string | null, selection: DomEditSelection, mutation: Record<string, unknown>, targetPath: string, result: MutationResult, options: CommitMutationOptions) => {
|
|
290
|
+
if (projectIdRef.current !== projectId) return;
|
|
291
|
+
const previewIsActive = isActiveCommitTarget(
|
|
292
|
+
projectIdRef,
|
|
293
|
+
activeCompPathRef,
|
|
294
|
+
projectId,
|
|
295
|
+
compositionPath,
|
|
296
|
+
);
|
|
229
297
|
// A no-op file write may still owe the runtime a deferred instant patch.
|
|
230
|
-
if (
|
|
231
|
-
|
|
298
|
+
if (result.changed === false) {
|
|
299
|
+
if (previewIsActive) {
|
|
300
|
+
finishUnchangedMutation(previewIframeRef.current, result, options, reloadPreview);
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (previewIsActive) domEditSaveTimestampRef.current = Date.now();
|
|
232
305
|
await recordMutationEdit(targetPath, result, options);
|
|
233
|
-
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
306
|
+
// The durable mutation belongs to the project captured when it was queued.
|
|
307
|
+
// A later project must never receive its file state or preview refresh.
|
|
308
|
+
if (!isActiveCommitTarget(projectIdRef, activeCompPathRef, projectId, compositionPath)) return;
|
|
309
|
+
syncCommittedGsapMutation({
|
|
310
|
+
iframe: previewIframeRef.current,
|
|
311
|
+
selection,
|
|
312
|
+
mutation,
|
|
313
|
+
targetPath,
|
|
241
314
|
result,
|
|
242
315
|
options,
|
|
316
|
+
onFileContentChanged,
|
|
317
|
+
forceReloadSdkSession,
|
|
243
318
|
reloadPreview,
|
|
244
319
|
onCacheInvalidate,
|
|
245
|
-
);
|
|
246
|
-
}, [previewIframeRef, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, forceReloadSdkSession, recordMutationEdit]);
|
|
320
|
+
});
|
|
321
|
+
}, [projectIdRef, previewIframeRef, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, forceReloadSdkSession, recordMutationEdit]);
|
|
247
322
|
|
|
248
|
-
const runCommit = useCallback(async (selection: DomEditSelection, mutation: Record<string, unknown>, options: CommitMutationOptions) => {
|
|
249
|
-
const pid = projectIdRef.current;
|
|
250
|
-
if (!pid) return;
|
|
251
|
-
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
323
|
+
const runCommit = useCallback(async (pid: string, compositionPath: string | null, targetPath: string, selection: DomEditSelection, mutation: Record<string, unknown>, options: CommitMutationOptions) => {
|
|
252
324
|
const result = await runMutationRequest([mutation], options, showToast, () =>
|
|
253
325
|
mutateGsapScript(pid, targetPath, mutation),
|
|
254
326
|
);
|
|
255
327
|
if (!result) return;
|
|
256
|
-
await finalizeSuccessfulMutation(selection, mutation, targetPath, result, options);
|
|
257
|
-
}, [
|
|
328
|
+
await finalizeSuccessfulMutation(pid, compositionPath, selection, mutation, targetPath, result, options);
|
|
329
|
+
}, [showToast, finalizeSuccessfulMutation]);
|
|
258
330
|
|
|
259
|
-
const runBatchCommit = useCallback(async (calls: CommitMutationCall[], options: CommitMutationOptions) => {
|
|
260
|
-
const pid = projectIdRef.current;
|
|
331
|
+
const runBatchCommit = useCallback(async (pid: string, compositionPath: string | null, targetPath: string, calls: CommitMutationCall[], options: CommitMutationOptions) => {
|
|
261
332
|
const first = calls[0];
|
|
262
333
|
const last = calls.at(-1);
|
|
263
|
-
if (!
|
|
264
|
-
const targetPath = first.selection.sourceFile || activeCompPath || "index.html";
|
|
334
|
+
if (!first || !last) return;
|
|
265
335
|
const mutations = calls.map(({ mutation }) => mutation);
|
|
266
336
|
const result = await runMutationRequest(mutations, options, showToast, () =>
|
|
267
337
|
mutateGsapScriptBatch(pid, targetPath, mutations),
|
|
268
338
|
);
|
|
269
339
|
if (!result) return;
|
|
270
|
-
await finalizeSuccessfulMutation(last.selection, last.mutation, targetPath, result, options);
|
|
271
|
-
}, [
|
|
340
|
+
await finalizeSuccessfulMutation(pid, compositionPath, last.selection, last.mutation, targetPath, result, options);
|
|
341
|
+
}, [showToast, finalizeSuccessfulMutation]);
|
|
272
342
|
|
|
273
343
|
// Every GSAP-script commit is a read-modify-write of one file. Overlapping
|
|
274
344
|
// commits to the SAME file (any op type, any animation) interleave server-side,
|
|
275
|
-
// so
|
|
345
|
+
// so every legacy request takes the same project/file lock as SDK writes. An
|
|
346
|
+
// explicit key adds ordering for related calls but never replaces the file lock.
|
|
276
347
|
const commitMutation = useMemo<CommitMutation>(() => {
|
|
348
|
+
const serializeFile = <T,>(file: string, task: () => Promise<T>): Promise<T> => {
|
|
349
|
+
if (writeProjectFile) {
|
|
350
|
+
return serializeStudioFileMutation(writeProjectFile, file, task);
|
|
351
|
+
}
|
|
352
|
+
return serializerRef.current(`gsap-file:${file}`, task);
|
|
353
|
+
};
|
|
354
|
+
const serializeCommit = <T,>(
|
|
355
|
+
file: string,
|
|
356
|
+
serializeKey: string | undefined,
|
|
357
|
+
task: () => Promise<T>,
|
|
358
|
+
): Promise<T> => {
|
|
359
|
+
const fileKey = `gsap-file:${file}`;
|
|
360
|
+
const run = () => serializeFile(file, task);
|
|
361
|
+
if (serializeKey && (writeProjectFile || serializeKey !== fileKey)) {
|
|
362
|
+
return serializerRef.current(serializeKey, run);
|
|
363
|
+
}
|
|
364
|
+
return run();
|
|
365
|
+
};
|
|
277
366
|
const commit: CommitMutation = (selection, mutation, options) => {
|
|
367
|
+
if (!activeProjectId) return Promise.resolve();
|
|
278
368
|
const file = selection.sourceFile || activeCompPath || "index.html";
|
|
279
|
-
|
|
280
|
-
|
|
369
|
+
return serializeCommit(file, options.serializeKey, () =>
|
|
370
|
+
runCommit(activeProjectId, activeCompPath, file, selection, mutation, options),
|
|
371
|
+
);
|
|
281
372
|
};
|
|
282
373
|
commit.batch = (calls, options) => {
|
|
374
|
+
if (!activeProjectId) return Promise.resolve();
|
|
283
375
|
const file = calls[0]?.selection.sourceFile || activeCompPath || "index.html";
|
|
284
|
-
|
|
285
|
-
|
|
376
|
+
return serializeCommit(file, options.serializeKey, () =>
|
|
377
|
+
runBatchCommit(activeProjectId, activeCompPath, file, calls, options),
|
|
378
|
+
);
|
|
286
379
|
};
|
|
287
380
|
return commit;
|
|
288
|
-
}, [runCommit, runBatchCommit, activeCompPath]);
|
|
381
|
+
}, [runCommit, runBatchCommit, activeCompPath, activeProjectId, writeProjectFile]);
|
|
289
382
|
const trackGsapSaveFailure = useGsapSaveFailureTelemetry(activeCompPath);
|
|
290
|
-
const
|
|
383
|
+
const handleGsapSaveFailure = useCallback(
|
|
384
|
+
(
|
|
385
|
+
error: unknown,
|
|
386
|
+
selection: DomEditSelection,
|
|
387
|
+
mutation: Record<string, unknown>,
|
|
388
|
+
label?: string,
|
|
389
|
+
) => {
|
|
390
|
+
trackGsapSaveFailure(error, selection, mutation, label);
|
|
391
|
+
if (!isStudioSaveErrorAlreadyToasted(error)) {
|
|
392
|
+
showToast?.(`Couldn't save animation: ${getStudioSaveErrorMessage(error)}`, "error");
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
[showToast, trackGsapSaveFailure],
|
|
396
|
+
);
|
|
397
|
+
const commitMutationSafely = useSafeGsapCommitMutation(commitMutation, handleGsapSaveFailure);
|
|
291
398
|
|
|
292
399
|
// One stable SDK-deps object shared by all GSAP child hooks. Memoized so the
|
|
293
400
|
// hooks' callbacks keep a stable identity (an inline literal here re-fired the
|
|
@@ -315,23 +422,15 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
|
|
|
315
422
|
},
|
|
316
423
|
[previewIframeRef, reloadPreview, onCacheInvalidate],
|
|
317
424
|
);
|
|
318
|
-
// Reuse the SAME per-file serializer the legacy commitMutation path uses, so
|
|
319
|
-
// SDK gsap-write flushes serialize against legacy commits AND each other —
|
|
320
|
-
// overlapping same-file read-modify-writes can't interleave and lose an edit.
|
|
321
|
-
const serializeByFile = useCallback(
|
|
322
|
-
<T>(key: string, task: () => Promise<T>): Promise<T> => serializerRef.current(key, task),
|
|
323
|
-
[],
|
|
324
|
-
);
|
|
325
425
|
// Read the on-disk bytes of targetPath so the SDK GSAP persist captures the
|
|
326
426
|
// exact prior content as its undo `before` (matching the style/delete paths),
|
|
327
427
|
// instead of a normalized full-DOM re-emit that would reformat the whole file.
|
|
328
428
|
const readProjectFileContent = useCallback(
|
|
329
429
|
(path: string): Promise<string> => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
return readSharedProjectFileContent(pid, path);
|
|
430
|
+
if (!activeProjectId) throw new Error("No active project");
|
|
431
|
+
return readSharedProjectFileContent(activeProjectId, path);
|
|
333
432
|
},
|
|
334
|
-
[
|
|
433
|
+
[activeProjectId],
|
|
335
434
|
);
|
|
336
435
|
const sdkDeps = useMemo<CutoverDeps | null>(
|
|
337
436
|
() =>
|
|
@@ -343,8 +442,8 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
|
|
|
343
442
|
domEditSaveTimestampRef,
|
|
344
443
|
refresh: sdkRefresh,
|
|
345
444
|
compositionPath: activeCompPath,
|
|
346
|
-
serialize: serializeByFile,
|
|
347
445
|
readProjectFile: readProjectFileContent,
|
|
446
|
+
publishSession: publishSdkSession,
|
|
348
447
|
}
|
|
349
448
|
: null,
|
|
350
449
|
[
|
|
@@ -354,8 +453,8 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
|
|
|
354
453
|
domEditSaveTimestampRef,
|
|
355
454
|
sdkRefresh,
|
|
356
455
|
activeCompPath,
|
|
357
|
-
serializeByFile,
|
|
358
456
|
readProjectFileContent,
|
|
457
|
+
publishSdkSession,
|
|
359
458
|
],
|
|
360
459
|
);
|
|
361
460
|
|
|
@@ -363,6 +462,7 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
|
|
|
363
462
|
sdkSession,
|
|
364
463
|
sdkDeps,
|
|
365
464
|
activeCompPath,
|
|
465
|
+
onFlushError: handleGsapSaveFailure,
|
|
366
466
|
});
|
|
367
467
|
const animationOps = useGsapAnimationOps({
|
|
368
468
|
projectIdRef,
|
|
@@ -377,7 +477,7 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
|
|
|
377
477
|
activeCompPath,
|
|
378
478
|
commitMutation,
|
|
379
479
|
commitMutationSafely,
|
|
380
|
-
trackGsapSaveFailure,
|
|
480
|
+
trackGsapSaveFailure: handleGsapSaveFailure,
|
|
381
481
|
sdkSession,
|
|
382
482
|
sdkDeps,
|
|
383
483
|
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
6
|
+
import { useGsapSelectionHandlers } from "./useGsapSelectionHandlers";
|
|
7
|
+
|
|
8
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
type Params = Parameters<typeof useGsapSelectionHandlers>[0];
|
|
11
|
+
type Handlers = ReturnType<typeof useGsapSelectionHandlers>;
|
|
12
|
+
|
|
13
|
+
function makeSelection(): DomEditSelection {
|
|
14
|
+
return {
|
|
15
|
+
id: "box",
|
|
16
|
+
hfId: "hf-box",
|
|
17
|
+
selector: "#box",
|
|
18
|
+
sourceFile: "index.html",
|
|
19
|
+
element: document.createElement("div"),
|
|
20
|
+
} as unknown as DomEditSelection;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeParams(overrides: Partial<Params> = {}): Params {
|
|
24
|
+
const resolved = () => vi.fn().mockResolvedValue(undefined);
|
|
25
|
+
return {
|
|
26
|
+
domEditSelection: makeSelection(),
|
|
27
|
+
updateGsapProperty: vi.fn(),
|
|
28
|
+
updateGsapMeta: resolved(),
|
|
29
|
+
deleteGsapAnimation: resolved(),
|
|
30
|
+
deleteAllForSelector: resolved(),
|
|
31
|
+
addGsapAnimation: resolved(),
|
|
32
|
+
addGsapProperty: resolved(),
|
|
33
|
+
removeGsapProperty: resolved(),
|
|
34
|
+
updateGsapFromProperty: resolved(),
|
|
35
|
+
addGsapFromProperty: resolved(),
|
|
36
|
+
removeGsapFromProperty: resolved(),
|
|
37
|
+
addKeyframe: vi.fn(),
|
|
38
|
+
addKeyframeBatch: resolved(),
|
|
39
|
+
removeKeyframe: vi.fn(),
|
|
40
|
+
moveKeyframe: vi.fn(),
|
|
41
|
+
resizeKeyframedTween: vi.fn(),
|
|
42
|
+
convertToKeyframes: resolved(),
|
|
43
|
+
removeAllKeyframes: resolved(),
|
|
44
|
+
handleDomManualEditsReset: vi.fn(),
|
|
45
|
+
selectedGsapAnimations: [],
|
|
46
|
+
showToast: vi.fn(),
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderHandlers(params: Params): { handlers: () => Handlers; unmount: () => void } {
|
|
52
|
+
let current: Handlers | undefined;
|
|
53
|
+
function Probe() {
|
|
54
|
+
current = useGsapSelectionHandlers(params);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const root = createRoot(document.createElement("div"));
|
|
58
|
+
act(() => root.render(<Probe />));
|
|
59
|
+
return {
|
|
60
|
+
handlers: () => {
|
|
61
|
+
if (!current) throw new Error("Hook did not render");
|
|
62
|
+
return current;
|
|
63
|
+
},
|
|
64
|
+
unmount: () => act(() => root.unmount()),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function flushRejection(): Promise<void> {
|
|
69
|
+
await act(async () => {
|
|
70
|
+
await Promise.resolve();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("useGsapSelectionHandlers save failures", () => {
|
|
75
|
+
it("surfaces a rejected animation metadata save", async () => {
|
|
76
|
+
const error = new Error("write failed");
|
|
77
|
+
const showToast = vi.fn();
|
|
78
|
+
const rendered = renderHandlers(
|
|
79
|
+
makeParams({ updateGsapMeta: vi.fn().mockRejectedValue(error), showToast }),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
act(() => rendered.handlers().handleGsapUpdateMeta("anim-1", { duration: 2 }));
|
|
83
|
+
await flushRejection();
|
|
84
|
+
|
|
85
|
+
expect(showToast).toHaveBeenCalledWith("Couldn't save animation: write failed", "error");
|
|
86
|
+
rendered.unmount();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("surfaces a rejected non-debounced property save", async () => {
|
|
90
|
+
const error = new Error("write failed");
|
|
91
|
+
const showToast = vi.fn();
|
|
92
|
+
const rendered = renderHandlers(
|
|
93
|
+
makeParams({ addGsapProperty: vi.fn().mockRejectedValue(error), showToast }),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
act(() => rendered.handlers().handleGsapAddProperty("anim-1", "opacity"));
|
|
97
|
+
await flushRejection();
|
|
98
|
+
|
|
99
|
+
expect(showToast).toHaveBeenCalledWith("Couldn't save animation: write failed", "error");
|
|
100
|
+
rendered.unmount();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("does not duplicate a toast already emitted by the mutation request", async () => {
|
|
104
|
+
const error = Object.assign(new Error("write failed"), { alreadyToasted: true });
|
|
105
|
+
const showToast = vi.fn();
|
|
106
|
+
const rendered = renderHandlers(
|
|
107
|
+
makeParams({ addGsapAnimation: vi.fn().mockRejectedValue(error), showToast }),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
act(() => rendered.handlers().handleGsapAddAnimation("to"));
|
|
111
|
+
await flushRejection();
|
|
112
|
+
|
|
113
|
+
expect(showToast).not.toHaveBeenCalled();
|
|
114
|
+
rendered.unmount();
|
|
115
|
+
});
|
|
116
|
+
});
|