@hyperframes/studio 0.8.115 → 0.8.116
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-C-9N8nk0.js → hyperframes-player-mGYvPVpw.js} +1 -1
- package/dist/assets/{index-BoUM6Xqb.js → index-BWHt0-ir.js} +1 -1
- package/dist/assets/{index-CAvZ0AA-.js → index-DYNaXPVf.js} +201 -201
- package/dist/assets/{index-CVuorIMv.js → index-Dr3hNpvS.js} +1 -1
- package/dist/{chunk-TXZMMOMA.js → chunk-3F47EFCT.js} +100 -2
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{domEditingLayers-D7VYFJJD.js → domEditingLayers-OL7GLSS4.js} +2 -2
- package/dist/index.d.ts +104 -22
- package/dist/index.html +1 -1
- package/dist/index.js +1131 -1044
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
- package/src/components/editor/domGroupMoveBy.ts +95 -0
- package/src/components/editor/dragUndoPaint.test.ts +5 -5
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
- package/src/components/editor/manualEditingAvailability.ts +8 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -6
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/probeSourceElement.ts +2 -1
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useFxCarveNodes.ts +2 -1
- package/src/components/editor/useFxLevelling.ts +2 -1
- package/src/components/feedback/projectProvenance.ts +2 -1
- package/src/components/nle/NLEContext.test.ts +1 -0
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/NLEPreview.tsx +12 -10
- package/src/components/nle/PreviewPoster.tsx +2 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/hooks/domSelectionTestHarness.ts +23 -0
- package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
- package/src/hooks/domStyleCommit.ts +8 -1
- package/src/hooks/draggedGsapPosition.ts +10 -0
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +2 -105
- package/src/hooks/gsapDragPositionCommit.ts +30 -393
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapMutationClient.ts +4 -3
- package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
- package/src/hooks/gsapParsedTween.test.ts +62 -0
- package/src/hooks/gsapParsedTween.ts +243 -0
- package/src/hooks/gsapPositionDetection.test.ts +78 -0
- package/src/hooks/gsapPositionDetection.ts +52 -33
- package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
- package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
- package/src/hooks/gsapResizeIntercept.test.ts +63 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -169
- package/src/hooks/gsapResizeSweep.test.ts +16 -33
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
- package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
- package/src/hooks/gsapRuntimeBridge.ts +105 -131
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +8 -0
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +26 -58
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
- package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
- package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
- package/src/hooks/gsapValueAtPlayhead.ts +292 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
- package/src/hooks/keyframeCacheAstLoad.ts +2 -1
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +23 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -2
- package/src/hooks/useDomGeometryCommit.ts +1 -0
- package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
- package/src/hooks/useEditHistoryActions.test.tsx +15 -0
- package/src/hooks/useEditHistoryActions.ts +16 -5
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useEnableKeyframes.test.ts +32 -1
- package/src/hooks/useEnableKeyframes.ts +2 -1
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +19 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +4 -2
- package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
- package/src/hooks/useFileManager.ts +14 -10
- package/src/hooks/useFileTree.ts +3 -2
- package/src/hooks/useFrameCapture.ts +5 -1
- package/src/hooks/useFreezeFrame.ts +3 -2
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +17 -29
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +5 -0
- package/src/hooks/useGsapAwareGroupMove.ts +16 -16
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapScriptCommits.test.tsx +3 -1
- package/src/hooks/useGsapScriptCommits.ts +12 -10
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +277 -1
- package/src/hooks/usePersistentEditHistory.ts +47 -8
- package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
- package/src/hooks/usePreviewPersistence.ts +13 -11
- package/src/hooks/useProjectFileWriter.ts +20 -19
- package/src/hooks/useRemoveBackground.ts +2 -1
- package/src/hooks/useSdkSession.ts +2 -1
- package/src/hooks/useServerConnection.ts +4 -3
- package/src/hooks/useStudioSelectionPublisher.ts +2 -1
- package/src/hooks/useThumbnailStill.test.ts +100 -0
- package/src/hooks/useThumbnailStill.ts +37 -0
- package/src/hooks/useTimelineDeleteOps.ts +2 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/src/player/lib/thumbnailScheduler.test.ts +16 -0
- package/src/player/lib/thumbnailScheduler.ts +10 -2
- package/src/player/sceneSwap.ts +2 -1
- package/src/telemetry/client.ts +2 -1
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/elementGsap.test.ts +9 -0
- package/src/utils/elementGsap.ts +7 -0
- package/src/utils/gsapSoftReload.ts +2 -3
- package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
- package/src/utils/gsapUndoRestore.ts +3 -6
- package/src/utils/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +1 -3
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +4 -1
- package/src/utils/studioHelpers.ts +7 -1
- package/src/utils/studioPendingEdits.test.ts +23 -6
- package/src/utils/studioPendingEdits.ts +32 -8
- package/src/utils/studioTelemetry.ts +2 -1
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +2 -1
- package/dist/chunk-TXZMMOMA.js.map +0 -1
- /package/dist/{domEditingLayers-D7VYFJJD.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
|
@@ -8,6 +8,7 @@ import { serializeStudioFileMutation } from "../utils/studioFileMutationCoordina
|
|
|
8
8
|
import { useFileTree } from "./useFileTree";
|
|
9
9
|
import { useEditorSave } from "./useEditorSave";
|
|
10
10
|
import { useProjectFileWriter } from "./useProjectFileWriter";
|
|
11
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
11
12
|
|
|
12
13
|
// ── Types ──
|
|
13
14
|
|
|
@@ -133,7 +134,7 @@ export function useFileManager({
|
|
|
133
134
|
setEditingFile({ path, content: null });
|
|
134
135
|
return;
|
|
135
136
|
}
|
|
136
|
-
|
|
137
|
+
studioApiFetch(`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`)
|
|
137
138
|
.then((r) => {
|
|
138
139
|
if (!r.ok) throw new Error(`Failed to load ${path} (${r.status})`);
|
|
139
140
|
return r.json();
|
|
@@ -167,9 +168,12 @@ export function useFileManager({
|
|
|
167
168
|
const requestId = ++revealRequestIdRef.current;
|
|
168
169
|
const controller = new AbortController();
|
|
169
170
|
revealAbortRef.current = controller;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
studioApiFetch(
|
|
172
|
+
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(sourceFile)}`,
|
|
173
|
+
{
|
|
174
|
+
signal: controller.signal,
|
|
175
|
+
},
|
|
176
|
+
)
|
|
173
177
|
.then((r) => r.json())
|
|
174
178
|
.then((data: { content?: string; version?: string }) => {
|
|
175
179
|
if (requestId !== revealRequestIdRef.current) return;
|
|
@@ -200,7 +204,7 @@ export function useFileManager({
|
|
|
200
204
|
|
|
201
205
|
const qs = dir ? `?dir=${encodeURIComponent(dir)}` : "";
|
|
202
206
|
try {
|
|
203
|
-
const res = await
|
|
207
|
+
const res = await studioApiFetch(`/api/projects/${encodeURIComponent(pid)}/upload${qs}`, {
|
|
204
208
|
method: "POST",
|
|
205
209
|
body: formData,
|
|
206
210
|
});
|
|
@@ -246,7 +250,7 @@ export function useFileManager({
|
|
|
246
250
|
content =
|
|
247
251
|
'<!DOCTYPE html>\n<html>\n<head>\n <meta charset="UTF-8">\n</head>\n<body>\n\n</body>\n</html>\n';
|
|
248
252
|
}
|
|
249
|
-
const res = await
|
|
253
|
+
const res = await studioApiFetch(
|
|
250
254
|
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`,
|
|
251
255
|
{
|
|
252
256
|
method: "POST",
|
|
@@ -270,7 +274,7 @@ export function useFileManager({
|
|
|
270
274
|
async (path: string) => {
|
|
271
275
|
const pid = projectIdRef.current;
|
|
272
276
|
if (!pid) return;
|
|
273
|
-
const res = await
|
|
277
|
+
const res = await studioApiFetch(
|
|
274
278
|
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path + "/.gitkeep")}`,
|
|
275
279
|
{
|
|
276
280
|
method: "POST",
|
|
@@ -293,7 +297,7 @@ export function useFileManager({
|
|
|
293
297
|
async (path: string) => {
|
|
294
298
|
const pid = projectIdRef.current;
|
|
295
299
|
if (!pid) return;
|
|
296
|
-
const res = await
|
|
300
|
+
const res = await studioApiFetch(
|
|
297
301
|
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`,
|
|
298
302
|
{
|
|
299
303
|
method: "DELETE",
|
|
@@ -315,7 +319,7 @@ export function useFileManager({
|
|
|
315
319
|
async (oldPath: string, newPath: string) => {
|
|
316
320
|
const pid = projectIdRef.current;
|
|
317
321
|
if (!pid) return;
|
|
318
|
-
const res = await
|
|
322
|
+
const res = await studioApiFetch(
|
|
319
323
|
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(oldPath)}`,
|
|
320
324
|
{
|
|
321
325
|
method: "PATCH",
|
|
@@ -342,7 +346,7 @@ export function useFileManager({
|
|
|
342
346
|
async (path: string) => {
|
|
343
347
|
const pid = projectIdRef.current;
|
|
344
348
|
if (!pid) return;
|
|
345
|
-
const res = await
|
|
349
|
+
const res = await studioApiFetch(`/api/projects/${encodeURIComponent(pid)}/duplicate-file`, {
|
|
346
350
|
method: "POST",
|
|
347
351
|
headers: { "Content-Type": "application/json" },
|
|
348
352
|
body: JSON.stringify({ path }),
|
package/src/hooks/useFileTree.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
|
|
3
3
|
import { FONT_EXT } from "../utils/mediaTypes";
|
|
4
4
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
5
5
|
import { captureProjectProvenance } from "../components/feedback/projectProvenance";
|
|
6
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
6
7
|
|
|
7
8
|
interface UseFileTreeOptions {
|
|
8
9
|
projectId: string | null;
|
|
@@ -38,7 +39,7 @@ export function useFileTree({ projectId, projectIdRef }: UseFileTreeOptions) {
|
|
|
38
39
|
if (!projectId) return;
|
|
39
40
|
let cancelled = false;
|
|
40
41
|
setFetched({ projectId, loaded: false, fileTree: [], compositionPaths: [], projectDir: null });
|
|
41
|
-
|
|
42
|
+
studioApiFetch(buildProjectApiPath(projectId))
|
|
42
43
|
// An unresolvable project answers 404 with a JSON body, so without this
|
|
43
44
|
// the error path parsed cleanly and the success branch below recorded an
|
|
44
45
|
// empty tree — and an empty provenance snapshot — for a project that was
|
|
@@ -84,7 +85,7 @@ export function useFileTree({ projectId, projectIdRef }: UseFileTreeOptions) {
|
|
|
84
85
|
refreshAbortRef.current = controller;
|
|
85
86
|
let data: { files?: string[]; compositions?: string[] };
|
|
86
87
|
try {
|
|
87
|
-
const res = await
|
|
88
|
+
const res = await studioApiFetch(buildProjectApiPath(pid), { signal: controller.signal });
|
|
88
89
|
data = await res.json();
|
|
89
90
|
} catch (err) {
|
|
90
91
|
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
@@ -2,6 +2,7 @@ import { useMemo, useState, useCallback, useRef, type MouseEvent } from "react";
|
|
|
2
2
|
import { useMountEffect } from "./useMountEffect";
|
|
3
3
|
import { liveTime, usePlayerStore } from "../player";
|
|
4
4
|
import { buildFrameCaptureFilename, buildFrameCaptureUrl } from "../utils/frameCapture";
|
|
5
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
5
6
|
|
|
6
7
|
interface UseFrameCaptureParams {
|
|
7
8
|
projectId: string | null;
|
|
@@ -67,7 +68,10 @@ export function useFrameCapture({
|
|
|
67
68
|
const controller = new AbortController();
|
|
68
69
|
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
69
70
|
try {
|
|
70
|
-
const response = await
|
|
71
|
+
const response = await studioApiFetch(href, {
|
|
72
|
+
cache: "no-store",
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
});
|
|
71
75
|
clearTimeout(timeout);
|
|
72
76
|
if (!response.ok) {
|
|
73
77
|
let msg = `Capture failed (${response.status})`;
|
|
@@ -6,6 +6,7 @@ import { buildProjectApiPath } from "../utils/projectRouting";
|
|
|
6
6
|
import { markStudioWriteToken } from "../utils/studioFileVersion";
|
|
7
7
|
import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
|
|
8
8
|
import type { RecordEditInput } from "../utils/studioFileHistory";
|
|
9
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
9
10
|
|
|
10
11
|
type ProjectFileWriter = (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
11
12
|
|
|
@@ -31,7 +32,7 @@ function errorOf(value: unknown): string | null {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
async function readVersion(projectId: string, path: string): Promise<string> {
|
|
34
|
-
const response = await
|
|
35
|
+
const response = await studioApiFetch(
|
|
35
36
|
buildProjectApiPath(projectId, `/files/${encodeURIComponent(path)}`),
|
|
36
37
|
);
|
|
37
38
|
const body: unknown = await response.json().catch(() => null);
|
|
@@ -53,7 +54,7 @@ export async function requestFreezeFrame(input: {
|
|
|
53
54
|
const expectedVersion = await readVersion(input.projectId, input.path);
|
|
54
55
|
const transactionToken = `freeze:${crypto.randomUUID()}`;
|
|
55
56
|
markStudioWriteToken(transactionToken);
|
|
56
|
-
const response = await
|
|
57
|
+
const response = await studioApiFetch(
|
|
57
58
|
buildProjectApiPath(input.projectId, "/file-mutations/freeze-frame"),
|
|
58
59
|
{
|
|
59
60
|
method: "POST",
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type ElementMatchSelection,
|
|
17
17
|
} from "../utils/studioHelpers";
|
|
18
18
|
import type { TimelineElement } from "../player";
|
|
19
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
19
20
|
|
|
20
21
|
interface UseGroupCommitsParams extends DomEditCommitBaseParams {
|
|
21
22
|
/** Resync the SDK session after a server-side write (the wrapper/unwrap changes
|
|
@@ -132,7 +133,7 @@ async function commitStructuralMutation(
|
|
|
132
133
|
writeFile: deps.writeProjectFile,
|
|
133
134
|
recordEdit: deps.editHistory.recordEdit,
|
|
134
135
|
rewrite: async (originalContent) => {
|
|
135
|
-
const mutateResponse = await
|
|
136
|
+
const mutateResponse = await studioApiFetch(
|
|
136
137
|
buildProjectApiPath(pid, `/file-mutations/${route}/${encodeURIComponent(targetPath)}`),
|
|
137
138
|
{
|
|
138
139
|
method: "POST",
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
|
+
import type { DomEditGroupPathOffsetCommit } from "../components/editor/domEditOverlayGestures";
|
|
8
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
9
|
+
import { trackStudioEditBlocked } from "../utils/studioSaveDiagnostics";
|
|
10
|
+
import { GSAP_EDIT_BLOCK_COPY } from "./gsapEditOutcome";
|
|
11
|
+
import { mountGsapAwareEditing } from "./useGsapAwareEditing.testHelpers";
|
|
12
|
+
|
|
13
|
+
vi.mock("../utils/studioSaveDiagnostics", async (importOriginal) => ({
|
|
14
|
+
...(await importOriginal<typeof import("../utils/studioSaveDiagnostics")>()),
|
|
15
|
+
trackStudioEditBlocked: vi.fn(),
|
|
16
|
+
trackStudioSaveFailure: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
usePlayerStore.setState({ autoKeyframeEnabled: true, currentTime: 1, activeKeyframePct: null });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
document.body.innerHTML = "";
|
|
28
|
+
Reflect.deleteProperty(window, "__timelines");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const positionTween = (id: string, extra: Partial<GsapAnimation> = {}) =>
|
|
32
|
+
({
|
|
33
|
+
id: `#${id}-to-0-position`,
|
|
34
|
+
targetSelector: `#${id}`,
|
|
35
|
+
propertyGroup: "position",
|
|
36
|
+
method: "to",
|
|
37
|
+
properties: { x: 100 },
|
|
38
|
+
position: 0,
|
|
39
|
+
resolvedStart: 0,
|
|
40
|
+
duration: 2,
|
|
41
|
+
ease: "none",
|
|
42
|
+
...extra,
|
|
43
|
+
}) as unknown as GsapAnimation;
|
|
44
|
+
|
|
45
|
+
/** Three members GSAP is visibly moving, one `to` tween each in the file. */
|
|
46
|
+
function mountGroup(animations: GsapAnimation[]) {
|
|
47
|
+
const elements = ["a", "b", "c"].map((id) => {
|
|
48
|
+
const el = Object.assign(document.createElement("div"), { id });
|
|
49
|
+
document.body.append(el);
|
|
50
|
+
return el;
|
|
51
|
+
});
|
|
52
|
+
const live = elements.map((el) => ({
|
|
53
|
+
targets: () => [el],
|
|
54
|
+
vars: { x: 100, duration: 2 },
|
|
55
|
+
duration: () => 2,
|
|
56
|
+
startTime: () => 0,
|
|
57
|
+
}));
|
|
58
|
+
const timelines = { root: { getChildren: () => live, duration: () => 2 } };
|
|
59
|
+
Reflect.set(window, "__timelines", timelines);
|
|
60
|
+
const iframe = {
|
|
61
|
+
contentWindow: { gsap: { getProperty: () => 0, set: vi.fn() }, __timelines: timelines },
|
|
62
|
+
contentDocument: document,
|
|
63
|
+
} as unknown as HTMLIFrameElement;
|
|
64
|
+
const commitMutation = Object.assign(vi.fn().mockResolvedValue(undefined), {
|
|
65
|
+
batch: vi.fn().mockResolvedValue(undefined),
|
|
66
|
+
});
|
|
67
|
+
const showToast = vi.fn();
|
|
68
|
+
const stageElementPositionOffset = vi.fn(() => ({ save: vi.fn(), rollback: vi.fn() }));
|
|
69
|
+
const { editing, root } = mountGsapAwareEditing({
|
|
70
|
+
gsapCommitMutation: commitMutation,
|
|
71
|
+
activeCompPath: "index.html",
|
|
72
|
+
previewIframeRef: { current: iframe },
|
|
73
|
+
showToast,
|
|
74
|
+
makeFetchFallback: () => async () => animations,
|
|
75
|
+
stageElementPositionOffset,
|
|
76
|
+
});
|
|
77
|
+
const groupCommit = (updates: DomEditGroupPathOffsetCommit[]) =>
|
|
78
|
+
editing().handleGsapAwareGroupPathOffsetCommit(updates);
|
|
79
|
+
const updates = elements.map((element) => ({
|
|
80
|
+
selection: {
|
|
81
|
+
element,
|
|
82
|
+
id: element.id,
|
|
83
|
+
selector: `#${element.id}`,
|
|
84
|
+
} as unknown as DomEditSelection,
|
|
85
|
+
next: { x: 30, y: 0 },
|
|
86
|
+
}));
|
|
87
|
+
const written = () => [
|
|
88
|
+
...commitMutation.mock.calls.map((call) => call[1]),
|
|
89
|
+
...commitMutation.batch.mock.calls.flatMap(([calls]) =>
|
|
90
|
+
calls.map((c: { mutation: unknown }) => c.mutation),
|
|
91
|
+
),
|
|
92
|
+
];
|
|
93
|
+
return { elements, iframe, updates, groupCommit, commitMutation, written, showToast, root };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("a group drag plans every member before its first write", () => {
|
|
97
|
+
it("refuses the whole group when one member's tween cannot take a keyframe", async () => {
|
|
98
|
+
const h = mountGroup([
|
|
99
|
+
positionTween("a"),
|
|
100
|
+
positionTween("b", { ease: undefined }),
|
|
101
|
+
positionTween("c"),
|
|
102
|
+
]);
|
|
103
|
+
const styles = h.elements.map((el) => el.getAttribute("style"));
|
|
104
|
+
|
|
105
|
+
await expect(h.groupCommit(h.updates)).rejects.toMatchObject({
|
|
106
|
+
name: "GsapEditBlockedError",
|
|
107
|
+
reason: "keyframes-uneditable",
|
|
108
|
+
detail: "unknown-ease",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(h.written()).toEqual([]);
|
|
112
|
+
expect(h.commitMutation.batch).not.toHaveBeenCalled();
|
|
113
|
+
const gsap = (h.iframe.contentWindow as unknown as { gsap: { set: unknown } }).gsap;
|
|
114
|
+
expect(gsap.set).not.toHaveBeenCalled();
|
|
115
|
+
expect(h.elements.map((el) => el.getAttribute("style"))).toEqual(styles);
|
|
116
|
+
expect(h.showToast).toHaveBeenCalledTimes(1);
|
|
117
|
+
expect(h.showToast).toHaveBeenCalledWith(GSAP_EDIT_BLOCK_COPY["keyframes-uneditable"], "error");
|
|
118
|
+
expect(trackStudioEditBlocked).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(trackStudioEditBlocked).toHaveBeenCalledWith(expect.objectContaining({ targetId: "b" }));
|
|
120
|
+
act(() => h.root.unmount());
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("refuses members animated in two files, since the batch writes one file", async () => {
|
|
124
|
+
const h = mountGroup(["a", "b", "c"].map((id) => positionTween(id)));
|
|
125
|
+
const updates = h.updates.map((u, i) => ({
|
|
126
|
+
...u,
|
|
127
|
+
selection: { ...u.selection, sourceFile: i === 2 ? "scenes/intro.html" : "index.html" },
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
await expect(h.groupCommit(updates)).rejects.toMatchObject({ reason: "mixed-files" });
|
|
131
|
+
|
|
132
|
+
expect(h.written()).toEqual([]);
|
|
133
|
+
expect(trackStudioEditBlocked).toHaveBeenCalledWith(expect.objectContaining({ targetId: "c" }));
|
|
134
|
+
act(() => h.root.unmount());
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("writes a group whose members name the open file or leave it implied", async () => {
|
|
138
|
+
const h = mountGroup(["a", "b", "c"].map((id) => positionTween(id)));
|
|
139
|
+
const updates = h.updates.map((u, i) => ({
|
|
140
|
+
...u,
|
|
141
|
+
selection: { ...u.selection, sourceFile: i === 0 ? "" : "index.html" },
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
await h.groupCommit(updates);
|
|
145
|
+
|
|
146
|
+
expect(h.written()).toHaveLength(3);
|
|
147
|
+
act(() => h.root.unmount());
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("writes every member when each plan holds", async () => {
|
|
151
|
+
const h = mountGroup(["a", "b", "c"].map((id) => positionTween(id)));
|
|
152
|
+
|
|
153
|
+
await h.groupCommit(h.updates);
|
|
154
|
+
|
|
155
|
+
const ids = h.written().map((m) => (m as { animationId?: string }).animationId);
|
|
156
|
+
expect(ids).toEqual(["#a-to-0-position", "#b-to-0-position", "#c-to-0-position"]);
|
|
157
|
+
expect(h.showToast).not.toHaveBeenCalled();
|
|
158
|
+
act(() => h.root.unmount());
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -119,18 +119,21 @@ function mountResizeHandler(
|
|
|
119
119
|
type AwareEditingParams = Parameters<typeof useGsapAwareEditing>[0];
|
|
120
120
|
|
|
121
121
|
function mountGroupHandler({
|
|
122
|
-
gsapCommitMutation,
|
|
123
|
-
makeFetchFallback,
|
|
122
|
+
gsapCommitMutation = vi.fn().mockResolvedValue(undefined),
|
|
123
|
+
makeFetchFallback = () => vi.fn().mockResolvedValue([]),
|
|
124
124
|
trackGsapInteractionFailure = vi.fn(),
|
|
125
125
|
stageElementPositionOffset = vi.fn(),
|
|
126
126
|
handleDomRotationCommit = vi.fn(),
|
|
127
|
-
}:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
}: Partial<
|
|
128
|
+
Pick<
|
|
129
|
+
AwareEditingParams,
|
|
130
|
+
| "gsapCommitMutation"
|
|
131
|
+
| "makeFetchFallback"
|
|
132
|
+
| "trackGsapInteractionFailure"
|
|
133
|
+
| "stageElementPositionOffset"
|
|
134
|
+
| "handleDomRotationCommit"
|
|
135
|
+
>
|
|
136
|
+
> = {}) {
|
|
134
137
|
let groupCommit!: (
|
|
135
138
|
updates: DomEditGroupPathOffsetCommit[],
|
|
136
139
|
) => Promise<void | import("../utils/previewFeatureUsage").GeometryCommitResult>;
|
|
@@ -216,8 +219,6 @@ describe("useGsapAwareEditing keeps the route a gesture chose at press", () => {
|
|
|
216
219
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
217
220
|
const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
|
|
218
221
|
const { pathOffsetCommit, groupCommit, root } = mountGroupHandler({
|
|
219
|
-
gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
|
|
220
|
-
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
221
222
|
stageElementPositionOffset,
|
|
222
223
|
});
|
|
223
224
|
const box = {
|
|
@@ -236,11 +237,7 @@ describe("useGsapAwareEditing keeps the route a gesture chose at press", () => {
|
|
|
236
237
|
describe("useGsapAwareEditing refuses a group GSAP took over before writing any member", () => {
|
|
237
238
|
it("writes no member when one CSS-route member has been folded since the press", async () => {
|
|
238
239
|
const stageElementPositionOffset = vi.fn(() => ({ save: vi.fn(), rollback: vi.fn() }));
|
|
239
|
-
const { groupCommit, root } = mountGroupHandler({
|
|
240
|
-
gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
|
|
241
|
-
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
242
|
-
stageElementPositionOffset,
|
|
243
|
-
});
|
|
240
|
+
const { groupCommit, root } = mountGroupHandler({ stageElementPositionOffset });
|
|
244
241
|
const card = {
|
|
245
242
|
element: document.createElement("div"),
|
|
246
243
|
id: "card",
|
|
@@ -268,7 +265,6 @@ describe("useGsapAwareEditing rotation routing", () => {
|
|
|
268
265
|
const gsapCommitMutation = vi.fn();
|
|
269
266
|
const h = mountGroupHandler({
|
|
270
267
|
gsapCommitMutation,
|
|
271
|
-
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
272
268
|
handleDomRotationCommit,
|
|
273
269
|
});
|
|
274
270
|
const box = { element, id: "box", selector: "#box" } as unknown as DomEditSelection;
|
|
@@ -332,7 +328,6 @@ describe("useGsapAwareEditing shared-tween moves", () => {
|
|
|
332
328
|
const commitMutation = vi.fn();
|
|
333
329
|
const { pathOffsetCommit, root } = mountGroupHandler({
|
|
334
330
|
gsapCommitMutation: commitMutation,
|
|
335
|
-
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
336
331
|
stageElementPositionOffset,
|
|
337
332
|
});
|
|
338
333
|
const word = { element: gsapPositioned("span"), hfId: "w0", selector: ".w" };
|
|
@@ -351,11 +346,7 @@ describe("useGsapAwareEditing shared-tween moves", () => {
|
|
|
351
346
|
});
|
|
352
347
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
353
348
|
const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
|
|
354
|
-
const { groupCommit, root } = mountGroupHandler({
|
|
355
|
-
gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
|
|
356
|
-
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
357
|
-
stageElementPositionOffset,
|
|
358
|
-
});
|
|
349
|
+
const { groupCommit, root } = mountGroupHandler({ stageElementPositionOffset });
|
|
359
350
|
const word = { element: gsapPositioned("span"), hfId: "w0", selector: ".w" };
|
|
360
351
|
const box = { element: gsapPositioned("div"), id: "box", selector: "#box" };
|
|
361
352
|
const plain = { element: document.createElement("div"), id: "plain", selector: "#plain" };
|
|
@@ -406,6 +397,7 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
406
397
|
null,
|
|
407
398
|
expect.any(Function),
|
|
408
399
|
expect.any(Function),
|
|
400
|
+
undefined,
|
|
409
401
|
);
|
|
410
402
|
act(() => h.root.unmount());
|
|
411
403
|
});
|
|
@@ -658,7 +650,6 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
658
650
|
);
|
|
659
651
|
const { groupCommit, root } = mountGroupHandler({
|
|
660
652
|
gsapCommitMutation: commitMutation,
|
|
661
|
-
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
662
653
|
});
|
|
663
654
|
const updates = [
|
|
664
655
|
{
|
|
@@ -759,11 +750,7 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
759
750
|
mocks.drag.mockImplementation(async (selection) => {
|
|
760
751
|
throw selection.id === "a" ? failures[0] : failures[1];
|
|
761
752
|
});
|
|
762
|
-
const { groupCommit, root } = mountGroupHandler({
|
|
763
|
-
gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
|
|
764
|
-
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
765
|
-
trackGsapInteractionFailure,
|
|
766
|
-
});
|
|
753
|
+
const { groupCommit, root } = mountGroupHandler({ trackGsapInteractionFailure });
|
|
767
754
|
const updates = [
|
|
768
755
|
{
|
|
769
756
|
selection: { element: gsapPositioned("div"), id: "a", selector: "#a" },
|
|
@@ -782,6 +769,7 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
782
769
|
updates[0]?.selection,
|
|
783
770
|
"drag",
|
|
784
771
|
"Move animated layer (group)",
|
|
772
|
+
true,
|
|
785
773
|
);
|
|
786
774
|
mocks.drag.mockReset();
|
|
787
775
|
if (priorDragImplementation) mocks.drag.mockImplementation(priorDragImplementation);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
3
|
+
import { useGsapAwareEditing } from "./useGsapAwareEditing";
|
|
4
|
+
import { useGsapInteractionFailureTelemetry } from "./useGsapInteractionFailureTelemetry";
|
|
5
|
+
|
|
6
|
+
type Params = Parameters<typeof useGsapAwareEditing>[0];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mounts useGsapAwareEditing with no-op callbacks for every param not given, and the real
|
|
10
|
+
* failure telemetry toasting through `showToast`. `editing()` reads the latest render.
|
|
11
|
+
*/
|
|
12
|
+
export function mountGsapAwareEditing(
|
|
13
|
+
params: Partial<Omit<Params, "trackGsapInteractionFailure">> & Pick<Params, "showToast">,
|
|
14
|
+
) {
|
|
15
|
+
let editing!: ReturnType<typeof useGsapAwareEditing>;
|
|
16
|
+
function Harness() {
|
|
17
|
+
editing = useGsapAwareEditing({
|
|
18
|
+
domEditSelection: null,
|
|
19
|
+
selectedGsapAnimations: [],
|
|
20
|
+
gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
previewIframeRef: { current: null },
|
|
22
|
+
bumpGsapCache: vi.fn(),
|
|
23
|
+
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
24
|
+
stageElementPositionOffset: vi.fn(),
|
|
25
|
+
handleDomBoxSizeCommit: vi.fn(),
|
|
26
|
+
handleDomRotationCommit: vi.fn(),
|
|
27
|
+
commitPositionPatchToHtml: vi.fn(),
|
|
28
|
+
addGsapAnimation: vi.fn(),
|
|
29
|
+
convertToKeyframes: vi.fn(),
|
|
30
|
+
setArcPath: vi.fn(),
|
|
31
|
+
updateArcSegment: vi.fn(),
|
|
32
|
+
...params,
|
|
33
|
+
trackGsapInteractionFailure: useGsapInteractionFailureTelemetry(
|
|
34
|
+
"index.html",
|
|
35
|
+
params.showToast,
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const root = mountReactHarness(<Harness />);
|
|
41
|
+
return { editing: () => editing, root };
|
|
42
|
+
}
|
|
@@ -49,6 +49,7 @@ export interface UseGsapAwareEditingParams {
|
|
|
49
49
|
domEditSelection: DomEditSelection | null;
|
|
50
50
|
selectedGsapAnimations: GsapAnimation[];
|
|
51
51
|
gsapCommitMutation: CommitMutation | null;
|
|
52
|
+
activeCompPath?: string | null;
|
|
52
53
|
previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
53
54
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
54
55
|
bumpGsapCache: () => void;
|
|
@@ -61,6 +62,7 @@ export interface UseGsapAwareEditingParams {
|
|
|
61
62
|
selection: DomEditSelection | null,
|
|
62
63
|
mutationType: string,
|
|
63
64
|
label: string,
|
|
65
|
+
toast?: boolean,
|
|
64
66
|
) => void;
|
|
65
67
|
// DOM fallbacks (from useDomEditCommits)
|
|
66
68
|
stageElementPositionOffset: (
|
|
@@ -117,6 +119,7 @@ export function useGsapAwareEditing({
|
|
|
117
119
|
domEditSelection,
|
|
118
120
|
selectedGsapAnimations,
|
|
119
121
|
gsapCommitMutation,
|
|
122
|
+
activeCompPath,
|
|
120
123
|
previewIframeRef,
|
|
121
124
|
showToast,
|
|
122
125
|
bumpGsapCache,
|
|
@@ -190,6 +193,7 @@ export function useGsapAwareEditing({
|
|
|
190
193
|
|
|
191
194
|
const handleGsapAwareGroupPathOffsetCommit = useGsapAwareGroupMove({
|
|
192
195
|
gsapCommitMutation,
|
|
196
|
+
activeCompPath,
|
|
193
197
|
previewIframeRef,
|
|
194
198
|
makeFetchFallback,
|
|
195
199
|
trackGsapInteractionFailure,
|
|
@@ -285,6 +289,7 @@ export function useGsapAwareEditing({
|
|
|
285
289
|
previewIframeRef.current,
|
|
286
290
|
commitMutation,
|
|
287
291
|
makeFetchFallback(selection),
|
|
292
|
+
offset,
|
|
288
293
|
);
|
|
289
294
|
assertGsapEditPersisted(outcome);
|
|
290
295
|
// Saved before the buffered GSAP writes, so their reload stays the gesture's last render.
|
|
@@ -9,24 +9,14 @@ import { gsapWritesPosition } from "./gsapRuntimeKeyframes";
|
|
|
9
9
|
import { refuseGsapTakeover } from "./elementOffsetStager";
|
|
10
10
|
import { tryGsapDragIntercept } from "./gsapRuntimeBridge";
|
|
11
11
|
import { assertGsapEditPersisted } from "./gsapEditOutcome";
|
|
12
|
+
import { firstPreflightFailure } from "./gsapGroupPreflight";
|
|
12
13
|
|
|
13
14
|
// A distinct key keeps consecutive group drags in separate undo entries.
|
|
14
15
|
let groupDragCommitCounter = 0;
|
|
15
16
|
|
|
16
|
-
function firstPreflightFailure(
|
|
17
|
-
results: PromiseSettledResult<void>[],
|
|
18
|
-
updates: DomEditGroupPathOffsetCommit[],
|
|
19
|
-
): { error: unknown; selection: DomEditSelection } | null {
|
|
20
|
-
for (const [index, result] of results.entries()) {
|
|
21
|
-
if (result.status !== "rejected") continue;
|
|
22
|
-
const selection = updates[index]?.selection;
|
|
23
|
-
if (selection) return { error: result.reason, selection };
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
17
|
export function useGsapAwareGroupMove({
|
|
29
18
|
gsapCommitMutation,
|
|
19
|
+
activeCompPath,
|
|
30
20
|
previewIframeRef,
|
|
31
21
|
makeFetchFallback,
|
|
32
22
|
trackGsapInteractionFailure,
|
|
@@ -35,6 +25,7 @@ export function useGsapAwareGroupMove({
|
|
|
35
25
|
}: Pick<
|
|
36
26
|
UseGsapAwareEditingParams,
|
|
37
27
|
| "gsapCommitMutation"
|
|
28
|
+
| "activeCompPath"
|
|
38
29
|
| "previewIframeRef"
|
|
39
30
|
| "makeFetchFallback"
|
|
40
31
|
| "trackGsapInteractionFailure"
|
|
@@ -44,10 +35,12 @@ export function useGsapAwareGroupMove({
|
|
|
44
35
|
const handleGsapAwareGroupPathOffsetCommit = useCallback(
|
|
45
36
|
async (
|
|
46
37
|
updates: DomEditGroupPathOffsetCommit[],
|
|
38
|
+
options: { refusalToast?: boolean } = {},
|
|
47
39
|
): Promise<import("../utils/previewFeatureUsage").GeometryCommitResult> => {
|
|
48
40
|
const writes = observeGsapGesture(gsapCommitMutation);
|
|
49
41
|
const writer = writes.commit;
|
|
50
42
|
if (!writer) return { ok: true, changed: false };
|
|
43
|
+
const toastRefusal = options.refusalToast !== false;
|
|
51
44
|
// One coalesce key across slow writes keeps the group drag in one undo entry.
|
|
52
45
|
const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
|
|
53
46
|
// Hold the render until every member is saved, or unwritten members snap back.
|
|
@@ -87,16 +80,16 @@ export function useGsapAwareGroupMove({
|
|
|
87
80
|
// mutation, so a blocked member never leaves earlier siblings partially moved. Preflights
|
|
88
81
|
// write nothing and share one in-flight parse per file, so they run together.
|
|
89
82
|
const preflightResults = await Promise.allSettled(
|
|
90
|
-
updates.map(async ({ selection, plainTranslate }) => {
|
|
83
|
+
updates.map(async ({ selection, next, plainTranslate }) => {
|
|
91
84
|
if (plainTranslate ?? !gsapWritesPosition(selection.element)) {
|
|
92
|
-
refuseGsapTakeover(selection.element, showToast);
|
|
85
|
+
refuseGsapTakeover(selection.element, toastRefusal ? showToast : () => {});
|
|
93
86
|
return void offsetMembers.set(selection, true);
|
|
94
87
|
}
|
|
95
88
|
const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
|
|
96
89
|
preflightAnimations.set(selection, animations);
|
|
97
90
|
const outcome = await tryGsapDragIntercept(
|
|
98
91
|
selection,
|
|
99
|
-
|
|
92
|
+
next,
|
|
100
93
|
animations,
|
|
101
94
|
previewIframeRef.current,
|
|
102
95
|
coalescedCommit,
|
|
@@ -107,13 +100,19 @@ export function useGsapAwareGroupMove({
|
|
|
107
100
|
assertGsapEditPersisted(outcome);
|
|
108
101
|
}),
|
|
109
102
|
);
|
|
110
|
-
const preflightFailure = firstPreflightFailure(
|
|
103
|
+
const preflightFailure = firstPreflightFailure(
|
|
104
|
+
preflightResults,
|
|
105
|
+
updates,
|
|
106
|
+
offsetMembers,
|
|
107
|
+
activeCompPath,
|
|
108
|
+
);
|
|
111
109
|
if (preflightFailure) {
|
|
112
110
|
trackGsapInteractionFailure(
|
|
113
111
|
preflightFailure.error,
|
|
114
112
|
preflightFailure.selection,
|
|
115
113
|
"drag",
|
|
116
114
|
"Move animated layer (group)",
|
|
115
|
+
toastRefusal,
|
|
117
116
|
);
|
|
118
117
|
throw preflightFailure.error;
|
|
119
118
|
}
|
|
@@ -167,6 +166,7 @@ export function useGsapAwareGroupMove({
|
|
|
167
166
|
},
|
|
168
167
|
[
|
|
169
168
|
gsapCommitMutation,
|
|
169
|
+
activeCompPath,
|
|
170
170
|
previewIframeRef,
|
|
171
171
|
makeFetchFallback,
|
|
172
172
|
trackGsapInteractionFailure,
|
|
@@ -4,8 +4,8 @@ import { trackStudioEditBlocked, trackStudioSaveFailure } from "../utils/studioS
|
|
|
4
4
|
import { isGsapEditBlockedError } from "./gsapEditOutcome";
|
|
5
5
|
import { wasAlreadyToasted } from "./domEditPersistFailure";
|
|
6
6
|
|
|
7
|
-
function failureToast(error: unknown): string | null {
|
|
8
|
-
if (wasAlreadyToasted(error)) return null;
|
|
7
|
+
function failureToast(error: unknown, toast: boolean): string | null {
|
|
8
|
+
if (!toast || wasAlreadyToasted(error)) return null;
|
|
9
9
|
return isGsapEditBlockedError(error) ? error.message : "Failed to save animated edit.";
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -14,7 +14,13 @@ export function useGsapInteractionFailureTelemetry(
|
|
|
14
14
|
showToast: (message: string, tone?: "error" | "info") => void,
|
|
15
15
|
) {
|
|
16
16
|
return useCallback(
|
|
17
|
-
(
|
|
17
|
+
(
|
|
18
|
+
error: unknown,
|
|
19
|
+
selection: DomEditSelection | null,
|
|
20
|
+
mutationType: string,
|
|
21
|
+
label: string,
|
|
22
|
+
toast?: boolean,
|
|
23
|
+
) => {
|
|
18
24
|
const report = isGsapEditBlockedError(error)
|
|
19
25
|
? trackStudioEditBlocked
|
|
20
26
|
: trackStudioSaveFailure;
|
|
@@ -28,7 +34,7 @@ export function useGsapInteractionFailureTelemetry(
|
|
|
28
34
|
targetSelector: selection?.selector,
|
|
29
35
|
targetSourceFile: selection?.sourceFile,
|
|
30
36
|
});
|
|
31
|
-
const message = failureToast(error);
|
|
37
|
+
const message = failureToast(error, toast !== false);
|
|
32
38
|
if (message) showToast(message, "error");
|
|
33
39
|
},
|
|
34
40
|
[activeCompPath, showToast],
|