@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
|
@@ -25,6 +25,7 @@ interface DataAttributeCommitOptions {
|
|
|
25
25
|
coalescePrefix: string;
|
|
26
26
|
skipRefresh: boolean;
|
|
27
27
|
refreshAfter?: boolean;
|
|
28
|
+
onSettled?: (ok: boolean) => void;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function resolveFullAttrName(attr: string, prefixData: boolean | undefined): string {
|
|
@@ -57,6 +58,25 @@ interface CapturedAttributeElement {
|
|
|
57
58
|
previousValue: string | null;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
interface CapturedMultiAttributeElement {
|
|
62
|
+
element: HTMLElement;
|
|
63
|
+
previousValues: Map<string, string | null>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function captureMultiAttributeElement(
|
|
67
|
+
doc: Document | null | undefined,
|
|
68
|
+
selection: DomEditSelection,
|
|
69
|
+
activeCompPath: string | null,
|
|
70
|
+
fullAttrs: string[],
|
|
71
|
+
): CapturedMultiAttributeElement | null {
|
|
72
|
+
const el = findPreviewAttributeElement(doc, selection, activeCompPath);
|
|
73
|
+
if (!el) return null;
|
|
74
|
+
const previousValues = new Map(
|
|
75
|
+
fullAttrs.map((fullAttr) => [fullAttr, el.getAttribute(fullAttr)]),
|
|
76
|
+
);
|
|
77
|
+
return { element: el, previousValues };
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
function captureAttributeElement(
|
|
61
81
|
doc: Document | null | undefined,
|
|
62
82
|
selection: DomEditSelection,
|
|
@@ -128,6 +148,7 @@ export function useDomEditAttributeCommits({
|
|
|
128
148
|
onError: (error) => reportDomEditPersistFailure(domEditSelection, [op], error, showToast),
|
|
129
149
|
shouldResync: () => isLatestCommit() && !!options.refreshAfter,
|
|
130
150
|
resync: () => refreshDomEditSelectionFromPreview(domEditSelection),
|
|
151
|
+
onSettled: options.onSettled,
|
|
131
152
|
});
|
|
132
153
|
},
|
|
133
154
|
[
|
|
@@ -140,6 +161,105 @@ export function useDomEditAttributeCommits({
|
|
|
140
161
|
],
|
|
141
162
|
);
|
|
142
163
|
|
|
164
|
+
// Commits several data-* attributes on the SAME element in ONE persist call
|
|
165
|
+
// — needed when two attributes together describe a single logical value
|
|
166
|
+
// (e.g. a pinned timing range's start+duration): committing them through two
|
|
167
|
+
// separate sequential `commitDataAttribute` calls leaves a window where the
|
|
168
|
+
// second call resolves `domEditSelection` fresh from current hook state, so
|
|
169
|
+
// a selection change between the two awaits would misdirect it at the
|
|
170
|
+
// NEWLY selected element instead of the one being edited, and a failure of
|
|
171
|
+
// just the second call would leave the two attributes in an inconsistent
|
|
172
|
+
// half-applied state. Bundling them into one `PatchOperation[]` against an
|
|
173
|
+
// explicit, caller-supplied `selection` (not the "current" one) closes both
|
|
174
|
+
// gaps — matching `onCommitAnimatedProperties`'s same-shaped fix for GSAP
|
|
175
|
+
// property batches.
|
|
176
|
+
const commitDataAttributes = useCallback(
|
|
177
|
+
async (
|
|
178
|
+
selection: DomEditSelection,
|
|
179
|
+
attrs: Record<string, string | null>,
|
|
180
|
+
options: DataAttributeCommitOptions,
|
|
181
|
+
) => {
|
|
182
|
+
const iframe = previewIframeRef.current;
|
|
183
|
+
const entries = Object.entries(attrs).map(([attr, value]) => ({
|
|
184
|
+
attr,
|
|
185
|
+
fullAttr: resolveFullAttrName(attr, true),
|
|
186
|
+
value,
|
|
187
|
+
}));
|
|
188
|
+
const commitKey = `${options.coalescePrefix}:${entries
|
|
189
|
+
.map((entry) => entry.attr)
|
|
190
|
+
.sort()
|
|
191
|
+
.join(",")}:${getDomEditTargetKey(selection)}`;
|
|
192
|
+
const isLatestCommit = bumpDomEditCommitMapVersion(
|
|
193
|
+
domAttributeCommitVersionRef.current,
|
|
194
|
+
commitKey,
|
|
195
|
+
);
|
|
196
|
+
const ops: PatchOperation[] = entries.map((entry) => ({
|
|
197
|
+
type: "attribute",
|
|
198
|
+
property: entry.attr,
|
|
199
|
+
value: entry.value,
|
|
200
|
+
}));
|
|
201
|
+
let captured: CapturedMultiAttributeElement | null = null;
|
|
202
|
+
|
|
203
|
+
await runDomEditCommit({
|
|
204
|
+
capture: () => {
|
|
205
|
+
captured = captureMultiAttributeElement(
|
|
206
|
+
iframe?.contentDocument,
|
|
207
|
+
selection,
|
|
208
|
+
activeCompPath,
|
|
209
|
+
entries.map((entry) => entry.fullAttr),
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
apply: () => {
|
|
213
|
+
if (!captured) return;
|
|
214
|
+
for (const entry of entries) {
|
|
215
|
+
const nextValue = entry.value === null || entry.value === "" ? null : entry.value;
|
|
216
|
+
setOrRemovePreviewAttribute(captured.element, entry.fullAttr, nextValue);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
persist: () =>
|
|
220
|
+
persistDomEditOperations(selection, ops, {
|
|
221
|
+
label: options.label,
|
|
222
|
+
coalesceKey: commitKey,
|
|
223
|
+
skipRefresh: options.skipRefresh,
|
|
224
|
+
}),
|
|
225
|
+
shouldRevert: () => isLatestCommit(),
|
|
226
|
+
revert: () => {
|
|
227
|
+
if (!captured) return;
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
setOrRemovePreviewAttribute(
|
|
230
|
+
captured.element,
|
|
231
|
+
entry.fullAttr,
|
|
232
|
+
captured.previousValues.get(entry.fullAttr) ?? null,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
onError: (error) => reportDomEditPersistFailure(selection, ops, error, showToast),
|
|
237
|
+
shouldResync: () => isLatestCommit() && !!options.refreshAfter,
|
|
238
|
+
resync: () => refreshDomEditSelectionFromPreview(selection),
|
|
239
|
+
onSettled: options.onSettled,
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
[
|
|
243
|
+
activeCompPath,
|
|
244
|
+
persistDomEditOperations,
|
|
245
|
+
refreshDomEditSelectionFromPreview,
|
|
246
|
+
showToast,
|
|
247
|
+
previewIframeRef,
|
|
248
|
+
],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const handleDomAttributesCommit = useCallback(
|
|
252
|
+
async (selection: DomEditSelection, attrs: Record<string, string>) => {
|
|
253
|
+
await commitDataAttributes(selection, attrs, {
|
|
254
|
+
label: "Edit timing",
|
|
255
|
+
coalescePrefix: "attrs",
|
|
256
|
+
skipRefresh: false,
|
|
257
|
+
refreshAfter: true,
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
[commitDataAttributes],
|
|
261
|
+
);
|
|
262
|
+
|
|
143
263
|
const handleDomAttributeCommit = useCallback(
|
|
144
264
|
async (attr: string, value: string) => {
|
|
145
265
|
await commitDataAttribute(attr, value, {
|
|
@@ -153,11 +273,12 @@ export function useDomEditAttributeCommits({
|
|
|
153
273
|
);
|
|
154
274
|
|
|
155
275
|
const handleDomAttributeLiveCommit = useCallback(
|
|
156
|
-
async (attr: string, value: string | null) => {
|
|
276
|
+
async (attr: string, value: string | null, onSettled?: (ok: boolean) => void) => {
|
|
157
277
|
await commitDataAttribute(attr, value, {
|
|
158
278
|
label: `Edit ${attr.replace(/^(data-)?/, "").replace(/-/g, " ")}`,
|
|
159
279
|
coalescePrefix: "attr-live",
|
|
160
280
|
skipRefresh: true,
|
|
281
|
+
onSettled,
|
|
161
282
|
});
|
|
162
283
|
},
|
|
163
284
|
[commitDataAttribute],
|
|
@@ -223,5 +344,6 @@ export function useDomEditAttributeCommits({
|
|
|
223
344
|
handleDomAttributeCommit,
|
|
224
345
|
handleDomAttributeLiveCommit,
|
|
225
346
|
handleDomHtmlAttributeCommit,
|
|
347
|
+
handleDomAttributesCommit,
|
|
226
348
|
};
|
|
227
349
|
}
|
|
@@ -34,7 +34,7 @@ interface RenderedDomEditCommits {
|
|
|
34
34
|
|
|
35
35
|
interface RenderDomEditCommitsOptions {
|
|
36
36
|
importedFontAssets?: ImportedFontAsset[];
|
|
37
|
-
writeProjectFile?: (path: string, content: string) => Promise<void>;
|
|
37
|
+
writeProjectFile?: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
type FetchHandler = (
|
|
@@ -1050,6 +1050,46 @@ describe("useDomEditCommits style persist handling", () => {
|
|
|
1050
1050
|
}
|
|
1051
1051
|
});
|
|
1052
1052
|
|
|
1053
|
+
it("uses the patched server content as the custom-font write precondition", async () => {
|
|
1054
|
+
const patchedContent =
|
|
1055
|
+
'<!doctype html><html><head></head><body><div data-hf-id="hf-card">Card</div></body></html>';
|
|
1056
|
+
stubPatchFetch(
|
|
1057
|
+
{ ok: true, changed: true, matched: true, content: patchedContent },
|
|
1058
|
+
patchedContent,
|
|
1059
|
+
);
|
|
1060
|
+
const { iframe, element } = createPreviewElement();
|
|
1061
|
+
const selection = createSelection(element, {
|
|
1062
|
+
textFields: [textField({ key: "self", value: "Card", source: "self", tagName: "div" })],
|
|
1063
|
+
});
|
|
1064
|
+
const writeProjectFile = vi.fn(async () => {});
|
|
1065
|
+
const rendered = renderDomEditCommits(selection, iframe, { writeProjectFile });
|
|
1066
|
+
|
|
1067
|
+
try {
|
|
1068
|
+
await act(async () => {
|
|
1069
|
+
await rendered.hook.commitDomTextFields(
|
|
1070
|
+
selection,
|
|
1071
|
+
[textField({ key: "self", value: "Card", source: "self", tagName: "div" })],
|
|
1072
|
+
{
|
|
1073
|
+
importedFont: {
|
|
1074
|
+
family: "Imported",
|
|
1075
|
+
path: "fonts/Imported.woff2",
|
|
1076
|
+
url: "/api/projects/p1/preview/fonts/Imported.woff2",
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
);
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
expect(writeProjectFile).toHaveBeenCalledWith(
|
|
1083
|
+
"index.html",
|
|
1084
|
+
expect.stringContaining("@font-face"),
|
|
1085
|
+
patchedContent,
|
|
1086
|
+
);
|
|
1087
|
+
expect(rendered.showToast).not.toHaveBeenCalled();
|
|
1088
|
+
} finally {
|
|
1089
|
+
rendered.cleanup();
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1053
1093
|
it("keeps a rejected patch request (HTTP error) to one toast", async () => {
|
|
1054
1094
|
const { rendered, cleanup } = renderStyleCommitWithFetch(async (input) => {
|
|
1055
1095
|
const url = requestUrl(input);
|
|
@@ -12,11 +12,7 @@ import {
|
|
|
12
12
|
import { buildDomEditPatchTarget, type DomEditSelection } from "../components/editor/domEditing";
|
|
13
13
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
14
14
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
15
|
-
import type {
|
|
16
|
-
CommitDomEditPatchBatches,
|
|
17
|
-
DomEditPatchBatch,
|
|
18
|
-
PersistDomEditOperations,
|
|
19
|
-
} from "./domEditCommitTypes";
|
|
15
|
+
import type { CommitDomEditPatchBatches, PersistDomEditOperations } from "./domEditCommitTypes";
|
|
20
16
|
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
21
17
|
import {
|
|
22
18
|
DomEditPersistUnsafeValueError,
|
|
@@ -27,31 +23,16 @@ import { useDomEditPositionPatchCommit } from "./useDomEditPositionPatchCommit";
|
|
|
27
23
|
import { useDomEditTextCommits } from "./useDomEditTextCommits";
|
|
28
24
|
import { useDomGeometryCommits } from "./useDomGeometryCommits";
|
|
29
25
|
import { useElementLifecycleOps } from "./useElementLifecycleOps";
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function readErrorResponseBody(
|
|
43
|
-
response: Response,
|
|
44
|
-
): Promise<{ error?: string; fields?: string[] } | null> {
|
|
45
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
46
|
-
if (!contentType.includes("application/json")) return null;
|
|
47
|
-
return (await response.json().catch(() => null)) as { error?: string; fields?: string[] } | null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function formatPatchRejectionMessage(body: { error?: string; fields?: string[] } | null): string {
|
|
51
|
-
if (!body?.error) return "Couldn't save edit";
|
|
52
|
-
return `Couldn't save edit: ${body.error}${formatFieldsSuffix(body.fields)}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
26
|
+
import {
|
|
27
|
+
AtomicElementPatchConvergenceError,
|
|
28
|
+
batchesAreInlineStyleOnly,
|
|
29
|
+
formatPatchRejectionMessage,
|
|
30
|
+
formatUnsafeFieldList,
|
|
31
|
+
getErrorDetail,
|
|
32
|
+
patchElementBatches,
|
|
33
|
+
readErrorResponseBody,
|
|
34
|
+
} from "./useDomEditCommitsHelpers";
|
|
35
|
+
import { cutoverCommittedOrThrow, type CutoverResult } from "../utils/sdkCutover";
|
|
55
36
|
interface RecordEditInput {
|
|
56
37
|
label: string;
|
|
57
38
|
kind: EditHistoryKind;
|
|
@@ -60,144 +41,12 @@ interface RecordEditInput {
|
|
|
60
41
|
files: Record<string, { before: string; after: string }>;
|
|
61
42
|
}
|
|
62
43
|
|
|
63
|
-
/** Human-readable identifier for a batch patch target (for the unmatched warning). */
|
|
64
|
-
function describeBatchPatchTarget(patch: DomEditPatchBatch["patches"][number]): string {
|
|
65
|
-
return patch.target.id ?? patch.target.hfId ?? patch.target.selector ?? "(unaddressed)";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Surface server-reported unmatched patches. The server atomically refuses the
|
|
70
|
-
* whole multi-file gesture; the caller uses `durable: false` to roll back and
|
|
71
|
-
* reload, so report the refusal without turning it into a second failure.
|
|
72
|
-
*/
|
|
73
|
-
function reportUnmatchedBatchPatches(batch: DomEditPatchBatch, matched: boolean[]): void {
|
|
74
|
-
const unmatchedIds = batch.patches
|
|
75
|
-
.filter((_, index) => matched[index] === false)
|
|
76
|
-
.map(describeBatchPatchTarget);
|
|
77
|
-
if (unmatchedIds.length === 0) return;
|
|
78
|
-
console.warn(
|
|
79
|
-
`[studio] z-index reorder: server could not match ${unmatchedIds.length} patch target(s) in ` +
|
|
80
|
-
`${batch.sourceFile} (the whole z-order gesture will revert on reload):`,
|
|
81
|
-
unmatchedIds.join(", "),
|
|
82
|
-
);
|
|
83
|
-
trackStudioSaveFailure({
|
|
84
|
-
source: "dom_edit",
|
|
85
|
-
error: new Error(`Batch patch target(s) unmatched: ${unmatchedIds.join(", ")}`),
|
|
86
|
-
filePath: batch.sourceFile,
|
|
87
|
-
mutationType: "z-reorder-unmatched",
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
interface AtomicElementPatchFile {
|
|
92
|
-
sourceFile: string;
|
|
93
|
-
changed: boolean;
|
|
94
|
-
matched?: boolean[];
|
|
95
|
-
before: string;
|
|
96
|
-
after: string;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
class AtomicElementPatchConvergenceError extends Error {
|
|
100
|
-
constructor(message: string, options?: { cause?: unknown }) {
|
|
101
|
-
super(message, options);
|
|
102
|
-
this.name = "AtomicElementPatchConvergenceError";
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Keep the atomic response contract in one guard so callers do not compose validity.
|
|
107
|
-
// fallow-ignore-next-line complexity
|
|
108
|
-
function isAtomicElementPatchFile(value: unknown): value is AtomicElementPatchFile {
|
|
109
|
-
return (
|
|
110
|
-
typeof value === "object" &&
|
|
111
|
-
value !== null &&
|
|
112
|
-
"sourceFile" in value &&
|
|
113
|
-
typeof value.sourceFile === "string" &&
|
|
114
|
-
"changed" in value &&
|
|
115
|
-
typeof value.changed === "boolean" &&
|
|
116
|
-
(!("matched" in value) ||
|
|
117
|
-
(Array.isArray(value.matched) &&
|
|
118
|
-
value.matched.every((matched) => typeof matched === "boolean"))) &&
|
|
119
|
-
"before" in value &&
|
|
120
|
-
typeof value.before === "string" &&
|
|
121
|
-
"after" in value &&
|
|
122
|
-
typeof value.after === "string" &&
|
|
123
|
-
value.changed === (value.before !== value.after)
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// This is the single client owner for dispatching and validating the aggregate
|
|
128
|
-
// atomic endpoint. Splitting validation from the request would weaken that wire contract.
|
|
129
|
-
// fallow-ignore-next-line complexity
|
|
130
|
-
async function patchElementBatches(projectId: string, batches: DomEditPatchBatch[]) {
|
|
131
|
-
const body = JSON.stringify({ batches });
|
|
132
|
-
try {
|
|
133
|
-
const response = await fetch(
|
|
134
|
-
`/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element-batches`,
|
|
135
|
-
{
|
|
136
|
-
method: "POST",
|
|
137
|
-
headers: { "Content-Type": "application/json" },
|
|
138
|
-
body,
|
|
139
|
-
},
|
|
140
|
-
);
|
|
141
|
-
if (!response.ok) {
|
|
142
|
-
const rejection = await readErrorResponseBody(response);
|
|
143
|
-
throw new StudioSaveHttpError(formatPatchRejectionMessage(rejection), response.status);
|
|
144
|
-
}
|
|
145
|
-
const result: unknown = await response.json().catch(() => null);
|
|
146
|
-
if (
|
|
147
|
-
typeof result !== "object" ||
|
|
148
|
-
result === null ||
|
|
149
|
-
!("durable" in result) ||
|
|
150
|
-
typeof result.durable !== "boolean" ||
|
|
151
|
-
!("files" in result) ||
|
|
152
|
-
!Array.isArray(result.files) ||
|
|
153
|
-
result.files.length !== batches.length ||
|
|
154
|
-
!result.files.every(isAtomicElementPatchFile) ||
|
|
155
|
-
(!result.durable && result.files.some((file) => file.changed))
|
|
156
|
-
) {
|
|
157
|
-
throw new StudioSaveHttpError("Invalid atomic element patch response", 502);
|
|
158
|
-
}
|
|
159
|
-
const files = result.files.map((file, index) => {
|
|
160
|
-
const batch = batches[index];
|
|
161
|
-
const matched = file.matched ?? [];
|
|
162
|
-
if (
|
|
163
|
-
!batch ||
|
|
164
|
-
file.sourceFile !== batch.sourceFile ||
|
|
165
|
-
(matched.length !== 0 && matched.length !== batch.patches.length)
|
|
166
|
-
) {
|
|
167
|
-
throw new StudioSaveHttpError("Invalid atomic element patch response", 502);
|
|
168
|
-
}
|
|
169
|
-
reportUnmatchedBatchPatches(batch, matched);
|
|
170
|
-
return {
|
|
171
|
-
...file,
|
|
172
|
-
matched,
|
|
173
|
-
allMatched: matched.length === batch.patches.length && matched.every(Boolean),
|
|
174
|
-
};
|
|
175
|
-
});
|
|
176
|
-
return { durable: result.durable, files };
|
|
177
|
-
} catch (error) {
|
|
178
|
-
throw new AtomicElementPatchConvergenceError(getErrorDetail(error), { cause: error });
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* A batch is reload-skippable only when it is style-only: every operation is an
|
|
184
|
-
* `inline-style` write. The z-reorder commit applies those exact styles to the
|
|
185
|
-
* live iframe DOM synchronously, so persisting them adds nothing the preview
|
|
186
|
-
* doesn't already show. Any other op type (attribute / text-content / …) can
|
|
187
|
-
* have server-side semantics the live DOM hasn't mirrored — reload for those.
|
|
188
|
-
*/
|
|
189
|
-
function batchesAreInlineStyleOnly(batches: DomEditPatchBatch[]): boolean {
|
|
190
|
-
return batches.every((batch) =>
|
|
191
|
-
batch.patches.every((patch) => patch.operations.every((op) => op.type === "inline-style")),
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
44
|
export interface UseDomEditCommitsParams {
|
|
196
45
|
activeCompPath: string | null;
|
|
197
46
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
198
47
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
199
48
|
queueDomEditSave: <T>(save: () => Promise<T>) => Promise<T>;
|
|
200
|
-
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
49
|
+
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
201
50
|
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
202
51
|
editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
|
|
203
52
|
fileTree: string[];
|
|
@@ -222,16 +71,20 @@ export interface UseDomEditCommitsParams {
|
|
|
222
71
|
* path, whose session is already current) so a later SDK edit doesn't
|
|
223
72
|
* serialize the pre-write doc and revert the server's change. */
|
|
224
73
|
forceReloadSdkSession?: () => void;
|
|
225
|
-
/** Stage 7 Step 3c: called before the server-side patch path
|
|
74
|
+
/** Stage 7 Step 3c: called before the server-side patch path. */
|
|
226
75
|
onTrySdkPersist?: (
|
|
227
76
|
selection: DomEditSelection,
|
|
228
77
|
operations: PatchOperation[],
|
|
229
78
|
originalContent: string,
|
|
230
79
|
targetPath: string,
|
|
231
80
|
options?: { label?: string; coalesceKey?: string; skipRefresh?: boolean },
|
|
232
|
-
) => Promise<
|
|
233
|
-
/** Stage 7 §3.1: called before the server-side delete path
|
|
234
|
-
onTrySdkDelete?: (
|
|
81
|
+
) => Promise<CutoverResult>;
|
|
82
|
+
/** Stage 7 §3.1: called before the server-side delete path. */
|
|
83
|
+
onTrySdkDelete?: (
|
|
84
|
+
hfId: string,
|
|
85
|
+
originalContent: string,
|
|
86
|
+
targetPath: string,
|
|
87
|
+
) => Promise<CutoverResult>;
|
|
235
88
|
/** Resolver-shadow tripwire for z-index reorder targets (telemetry-only, decoupled from cutover). */
|
|
236
89
|
onReorderShadow?: (targets: string[]) => void;
|
|
237
90
|
}
|
|
@@ -326,18 +179,17 @@ export function useDomEditCommits({
|
|
|
326
179
|
// Skip the SDK path when prepareContent is set (e.g. @font-face injection
|
|
327
180
|
// for a custom font): sdkCutoverPersist serializes only the patched DOM
|
|
328
181
|
// and would drop the injected content. Let the server path run prepareContent.
|
|
329
|
-
if (
|
|
330
|
-
onTrySdkPersist
|
|
331
|
-
!options?.prepareContent &&
|
|
332
|
-
(await onTrySdkPersist(selection, operations, originalContent, targetPath, {
|
|
182
|
+
if (onTrySdkPersist && !options?.prepareContent) {
|
|
183
|
+
const cutover = await onTrySdkPersist(selection, operations, originalContent, targetPath, {
|
|
333
184
|
label: options?.label,
|
|
334
185
|
coalesceKey: options?.coalesceKey,
|
|
335
186
|
skipRefresh: options?.skipRefresh,
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
187
|
+
});
|
|
188
|
+
if (cutoverCommittedOrThrow(cutover)) {
|
|
189
|
+
// SDK handled it — its in-memory doc is already current, so do NOT
|
|
190
|
+
// forceReload (that would echo-reload the session we just wrote).
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
341
193
|
}
|
|
342
194
|
|
|
343
195
|
// Mark the save timestamp before the file write so the SSE file-change
|
|
@@ -393,7 +245,7 @@ export function useDomEditCommits({
|
|
|
393
245
|
const preparedContent = options.prepareContent(patchedContent, targetPath);
|
|
394
246
|
if (preparedContent !== patchedContent) {
|
|
395
247
|
try {
|
|
396
|
-
await writeProjectFile(targetPath, preparedContent);
|
|
248
|
+
await writeProjectFile(targetPath, preparedContent, patchedContent);
|
|
397
249
|
finalContent = preparedContent;
|
|
398
250
|
} catch (error) {
|
|
399
251
|
// The patch above already landed on disk — only the prepareContent
|
|
@@ -523,6 +375,7 @@ export function useDomEditCommits({
|
|
|
523
375
|
handleDomAttributeCommit,
|
|
524
376
|
handleDomAttributeLiveCommit,
|
|
525
377
|
handleDomHtmlAttributeCommit,
|
|
378
|
+
handleDomAttributesCommit,
|
|
526
379
|
handleDomTextCommit,
|
|
527
380
|
commitDomTextFields,
|
|
528
381
|
handleDomTextFieldStyleCommit,
|
|
@@ -585,6 +438,7 @@ export function useDomEditCommits({
|
|
|
585
438
|
handleDomAttributeCommit,
|
|
586
439
|
handleDomAttributeLiveCommit,
|
|
587
440
|
handleDomHtmlAttributeCommit,
|
|
441
|
+
handleDomAttributesCommit,
|
|
588
442
|
handleDomTextCommit,
|
|
589
443
|
commitDomTextFields,
|
|
590
444
|
handleDomTextFieldStyleCommit,
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { StudioSaveHttpError, trackStudioSaveFailure } from "../utils/studioSaveDiagnostics";
|
|
2
|
+
import type { DomEditPatchBatch } from "./domEditCommitTypes";
|
|
3
|
+
import { formatFieldsSuffix } from "./gsapScriptCommitHelpers";
|
|
4
|
+
|
|
5
|
+
export function formatUnsafeFieldList(fields: Array<{ path: string }>): string {
|
|
6
|
+
return fields.map((field) => field.path).join(", ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getErrorDetail(error: unknown): string {
|
|
10
|
+
return error instanceof Error ? error.message : String(error);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function readErrorResponseBody(
|
|
14
|
+
response: Response,
|
|
15
|
+
): Promise<{ error?: string; fields?: string[] } | null> {
|
|
16
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
17
|
+
if (!contentType.includes("application/json")) return null;
|
|
18
|
+
return (await response.json().catch(() => null)) as { error?: string; fields?: string[] } | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatPatchRejectionMessage(
|
|
22
|
+
body: { error?: string; fields?: string[] } | null,
|
|
23
|
+
): string {
|
|
24
|
+
if (!body?.error) return "Couldn't save edit";
|
|
25
|
+
return `Couldn't save edit: ${body.error}${formatFieldsSuffix(body.fields)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Human-readable identifier for a batch patch target (for the unmatched warning). */
|
|
29
|
+
function describeBatchPatchTarget(patch: DomEditPatchBatch["patches"][number]): string {
|
|
30
|
+
return patch.target.id ?? patch.target.hfId ?? patch.target.selector ?? "(unaddressed)";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Surface server-reported unmatched patches. The server atomically refuses the
|
|
35
|
+
* whole multi-file gesture; the caller uses `durable: false` to roll back and
|
|
36
|
+
* reload, so report the refusal without turning it into a second failure.
|
|
37
|
+
*/
|
|
38
|
+
function reportUnmatchedBatchPatches(batch: DomEditPatchBatch, matched: boolean[]): void {
|
|
39
|
+
const unmatchedIds = batch.patches
|
|
40
|
+
.filter((_, index) => matched[index] === false)
|
|
41
|
+
.map(describeBatchPatchTarget);
|
|
42
|
+
if (unmatchedIds.length === 0) return;
|
|
43
|
+
console.warn(
|
|
44
|
+
`[studio] z-index reorder: server could not match ${unmatchedIds.length} patch target(s) in ` +
|
|
45
|
+
`${batch.sourceFile} (the whole z-order gesture will revert on reload):`,
|
|
46
|
+
unmatchedIds.join(", "),
|
|
47
|
+
);
|
|
48
|
+
trackStudioSaveFailure({
|
|
49
|
+
source: "dom_edit",
|
|
50
|
+
error: new Error(`Batch patch target(s) unmatched: ${unmatchedIds.join(", ")}`),
|
|
51
|
+
filePath: batch.sourceFile,
|
|
52
|
+
mutationType: "z-reorder-unmatched",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface AtomicElementPatchFile {
|
|
57
|
+
sourceFile: string;
|
|
58
|
+
changed: boolean;
|
|
59
|
+
matched?: boolean[];
|
|
60
|
+
before: string;
|
|
61
|
+
after: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class AtomicElementPatchConvergenceError extends Error {
|
|
65
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
66
|
+
super(message, options);
|
|
67
|
+
this.name = "AtomicElementPatchConvergenceError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Keep the atomic response contract in one guard so callers do not compose validity.
|
|
72
|
+
// fallow-ignore-next-line complexity
|
|
73
|
+
function isAtomicElementPatchFile(value: unknown): value is AtomicElementPatchFile {
|
|
74
|
+
return (
|
|
75
|
+
typeof value === "object" &&
|
|
76
|
+
value !== null &&
|
|
77
|
+
"sourceFile" in value &&
|
|
78
|
+
typeof value.sourceFile === "string" &&
|
|
79
|
+
"changed" in value &&
|
|
80
|
+
typeof value.changed === "boolean" &&
|
|
81
|
+
(!("matched" in value) ||
|
|
82
|
+
(Array.isArray(value.matched) &&
|
|
83
|
+
value.matched.every((matched) => typeof matched === "boolean"))) &&
|
|
84
|
+
"before" in value &&
|
|
85
|
+
typeof value.before === "string" &&
|
|
86
|
+
"after" in value &&
|
|
87
|
+
typeof value.after === "string" &&
|
|
88
|
+
value.changed === (value.before !== value.after)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// This is the single client owner for dispatching and validating the aggregate
|
|
93
|
+
// atomic endpoint. Splitting validation from the request would weaken that wire contract.
|
|
94
|
+
// fallow-ignore-next-line complexity
|
|
95
|
+
export async function patchElementBatches(projectId: string, batches: DomEditPatchBatch[]) {
|
|
96
|
+
const body = JSON.stringify({ batches });
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(
|
|
99
|
+
`/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element-batches`,
|
|
100
|
+
{
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body,
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const rejection = await readErrorResponseBody(response);
|
|
108
|
+
throw new StudioSaveHttpError(formatPatchRejectionMessage(rejection), response.status);
|
|
109
|
+
}
|
|
110
|
+
const result: unknown = await response.json().catch(() => null);
|
|
111
|
+
if (
|
|
112
|
+
typeof result !== "object" ||
|
|
113
|
+
result === null ||
|
|
114
|
+
!("durable" in result) ||
|
|
115
|
+
typeof result.durable !== "boolean" ||
|
|
116
|
+
!("files" in result) ||
|
|
117
|
+
!Array.isArray(result.files) ||
|
|
118
|
+
result.files.length !== batches.length ||
|
|
119
|
+
!result.files.every(isAtomicElementPatchFile) ||
|
|
120
|
+
(!result.durable && result.files.some((file) => file.changed))
|
|
121
|
+
) {
|
|
122
|
+
throw new StudioSaveHttpError("Invalid atomic element patch response", 502);
|
|
123
|
+
}
|
|
124
|
+
const files = result.files.map((file, index) => {
|
|
125
|
+
const batch = batches[index];
|
|
126
|
+
const matched = file.matched ?? [];
|
|
127
|
+
if (
|
|
128
|
+
!batch ||
|
|
129
|
+
file.sourceFile !== batch.sourceFile ||
|
|
130
|
+
(matched.length !== 0 && matched.length !== batch.patches.length)
|
|
131
|
+
) {
|
|
132
|
+
throw new StudioSaveHttpError("Invalid atomic element patch response", 502);
|
|
133
|
+
}
|
|
134
|
+
reportUnmatchedBatchPatches(batch, matched);
|
|
135
|
+
return {
|
|
136
|
+
...file,
|
|
137
|
+
matched,
|
|
138
|
+
allMatched: matched.length === batch.patches.length && matched.every(Boolean),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
return { durable: result.durable, files };
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new AtomicElementPatchConvergenceError(getErrorDetail(error), { cause: error });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A batch is reload-skippable only when it is style-only: every operation is an
|
|
149
|
+
* `inline-style` write. The z-reorder commit applies those exact styles to the
|
|
150
|
+
* live iframe DOM synchronously, so persisting them adds nothing the preview
|
|
151
|
+
* doesn't already show. Any other op type (attribute / text-content / …) can
|
|
152
|
+
* have server-side semantics the live DOM hasn't mirrored — reload for those.
|
|
153
|
+
*/
|
|
154
|
+
export function batchesAreInlineStyleOnly(batches: DomEditPatchBatch[]): boolean {
|
|
155
|
+
return batches.every((batch) =>
|
|
156
|
+
batch.patches.every((patch) => patch.operations.every((op) => op.type === "inline-style")),
|
|
157
|
+
);
|
|
158
|
+
}
|