fix(studio): propagate z-index reorder save failures and drop dead targetTrack

handleDomZIndexReorderCommit no longer swallows per-entry save failures: it settles every
patch, and on any rejection rolls back the eager DOM z-index/position and the optimistic
store zIndex before rejecting, so a failed save cannot leave the UI showing a stacking order
that never persisted or let an ordered-after timing write proceed.

Also removes the dead targetTrack parameter threaded through the timeline edit helpers;
vertical placement is owned by the z-index intent.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-09 16:53:26 -04:00
parent 51dd8a0753
commit 6080f5ad3e
6 changed files with 168 additions and 12 deletions
@@ -160,6 +160,7 @@ export function useElementLifecycleOps({
// persistDomEditOperations → onTrySdkPersist, so it is already SDK-cut-over as setStyle.
// No SDK reorder/reparent op exists; DOM sibling order stays server-authoritative if ever needed.
const handleDomZIndexReorderCommit = useCallback(
// fallow-ignore-next-line complexity
(
entries: Array<{
element: HTMLElement;
@@ -168,6 +169,7 @@ export function useElementLifecycleOps({
selector?: string;
selectorIndex?: number;
sourceFile: string;
key?: string;
}>,
) => {
if (entries.length === 0) return Promise.resolve();
@@ -178,8 +180,15 @@ export function useElementLifecycleOps({
);
const coalesceKey = `z-reorder:${entries.map((e) => e.id ?? e.selector ?? e.element.getAttribute("data-hf-id") ?? "el").join(":")}`;
const saves: Array<Promise<void>> = [];
const rollbacks: Array<() => void> = [];
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const priorZIndex = entry.element.style.zIndex;
const priorPosition = entry.element.style.position;
const priorStoreEntry = entry.key
? usePlayerStore.getState().elements.find((el) => (el.key ?? el.id) === entry.key)
: undefined;
let positionChanged = false;
entry.element.style.zIndex = String(entry.zIndex);
const patches: Array<{ type: "inline-style"; property: string; value: string }> = [
{ type: "inline-style", property: "z-index", value: String(entry.zIndex) },
@@ -188,11 +197,27 @@ export function useElementLifecycleOps({
const win = entry.element.ownerDocument?.defaultView;
if (win && win.getComputedStyle(entry.element).position === "static") {
entry.element.style.position = "relative";
positionChanged = true;
patches.push({ type: "inline-style", property: "position", value: "relative" });
}
} catch {
/* cross-origin or detached — skip */
}
if (entry.key) {
usePlayerStore
.getState()
.updateElement(entry.key, { zIndex: entry.zIndex, hasExplicitZIndex: true });
}
rollbacks.push(() => {
entry.element.style.zIndex = priorZIndex;
if (positionChanged) entry.element.style.position = priorPosition;
if (entry.key && priorStoreEntry) {
usePlayerStore.getState().updateElement(entry.key, {
zIndex: priorStoreEntry.zIndex,
hasExplicitZIndex: priorStoreEntry.hasExplicitZIndex,
});
}
});
saves.push(
commitPositionPatchToHtml(
{
@@ -209,12 +234,21 @@ export function useElementLifecycleOps({
coalesceKey,
skipRefresh: i < entries.length - 1,
},
).catch(() => undefined),
),
);
}
// Resolves once every z-index patch is persisted so a same-file timing write
// can be ordered after it (see applyTimelineStackingReorder callers).
return Promise.all(saves).then(() => undefined);
return Promise.allSettled(saves).then((settled) => {
const rejected = settled.find(
(result): result is PromiseRejectedResult => result.status === "rejected",
);
if (rejected) {
for (const rollback of rollbacks) rollback();
return Promise.reject(rejected.reason);
}
return undefined;
});
},
[commitPositionPatchToHtml, onReorderShadow],
);