fix(web): stop autosave loop by ignoring volatile server-echo fields
Server Deploy / deploy (push) Successful in 39s

This commit is contained in:
2026-07-21 12:01:57 +01:00
parent 2b7ef98dff
commit d9a33b0672
+18 -7
View File
@@ -35,6 +35,17 @@ function AdhocBadge() {
);
}
// Stable snapshot of only the fields the editor controls. Excludes volatile
// server-echo fields (e.g. updated_at) that would otherwise change on every
// save and cause autosave to loop forever.
function snapshotOf(w: Workflow): string {
return JSON.stringify({
name: w.name,
target_server_ids: w.target_server_ids,
steps: w.steps,
});
}
function timeAgo(date: Date): string {
const s = Math.floor((Date.now() - date.getTime()) / 1000);
if (s < 5) return "just now";
@@ -80,7 +91,7 @@ export default function WorkflowBuilder() {
useEffect(() => {
if (loaded && !wf) {
setWf(loaded);
savedSnapshotRef.current = JSON.stringify(loaded);
savedSnapshotRef.current = snapshotOf(loaded);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loaded]);
@@ -115,7 +126,7 @@ export default function WorkflowBuilder() {
// Must stay above the early return below so hook order is stable.
useEffect(() => {
if (!wf || savedSnapshotRef.current === null) return;
if (JSON.stringify(wf) === savedSnapshotRef.current) return;
if (snapshotOf(wf) === savedSnapshotRef.current) return;
const t = setTimeout(() => {
save();
}, 800);
@@ -147,7 +158,7 @@ export default function WorkflowBuilder() {
if (savingRef.current) return;
const current = wfRef.current;
if (!current) return;
const snapshot = JSON.stringify(current);
const snapshot = snapshotOf(current);
if (snapshot === savedSnapshotRef.current) return;
savingRef.current = true;
setSaving(true);
@@ -158,10 +169,10 @@ export default function WorkflowBuilder() {
setError("Save failed: server returned an unexpected response.");
return;
}
if (JSON.stringify(wfRef.current) === snapshot) {
if (wfRef.current && snapshotOf(wfRef.current) === snapshot) {
// Nothing changed while the request was in flight: adopt the
// server echo as the new saved baseline.
savedSnapshotRef.current = JSON.stringify(updated);
savedSnapshotRef.current = snapshotOf(updated);
setWf(updated);
} else {
// The user edited again mid-flight. Keep their newer state and
@@ -177,7 +188,7 @@ export default function WorkflowBuilder() {
setSaving(false);
// If edits arrived during the save (or a concurrent save was
// skipped), persist them on the next tick.
if (wfRef.current && JSON.stringify(wfRef.current) !== savedSnapshotRef.current) {
if (wfRef.current && snapshotOf(wfRef.current) !== savedSnapshotRef.current) {
setTimeout(() => save(), 0);
}
}
@@ -638,7 +649,7 @@ export default function WorkflowBuilder() {
onSaved={(w) => {
// The modal already persisted w; sync the snapshot so
// autosave doesn't fire a redundant follow-up save.
savedSnapshotRef.current = JSON.stringify(w);
savedSnapshotRef.current = snapshotOf(w);
setWf(w);
}}
onClose={() => setEditWorkflowOpen(false)}