feat(web): workflow builder autosave with last-saved status
This commit is contained in:
@@ -43,6 +43,16 @@ function AdhocBadge() {
|
||||
);
|
||||
}
|
||||
|
||||
function timeAgo(date: Date): string {
|
||||
const s = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
if (s < 5) return "just now";
|
||||
if (s < 60) return `${s}s ago`;
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m ago`;
|
||||
const h = Math.floor(m / 60);
|
||||
return `${h}h ago`;
|
||||
}
|
||||
|
||||
export default function WorkflowBuilder() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const id = params.id;
|
||||
@@ -53,6 +63,9 @@ export default function WorkflowBuilder() {
|
||||
const [selected, setSelected] = useState<number | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
||||
const [, setTick] = useState(0);
|
||||
const savedSnapshotRef = useRef<string | null>(null);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
@@ -78,7 +91,10 @@ export default function WorkflowBuilder() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded && !wf) setWf(loaded);
|
||||
if (loaded && !wf) {
|
||||
setWf(loaded);
|
||||
savedSnapshotRef.current = JSON.stringify(loaded);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loaded]);
|
||||
|
||||
@@ -122,7 +138,11 @@ export default function WorkflowBuilder() {
|
||||
setError("Save failed: server returned an unexpected response.");
|
||||
return;
|
||||
}
|
||||
// Record the snapshot BEFORE setWf so the autosave effect sees the
|
||||
// incoming state as already-saved and doesn't re-trigger.
|
||||
savedSnapshotRef.current = JSON.stringify(updated);
|
||||
setWf(updated);
|
||||
setLastSaved(new Date());
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
} finally {
|
||||
@@ -130,6 +150,26 @@ export default function WorkflowBuilder() {
|
||||
}
|
||||
};
|
||||
|
||||
// Autosave: debounce 800ms after any change to the workflow (step added,
|
||||
// removed, reordered, or edited) and persist. Diffing the serialized state
|
||||
// against the last saved snapshot skips no-op saves and the initial load.
|
||||
useEffect(() => {
|
||||
if (!wf || savedSnapshotRef.current === null) return;
|
||||
if (JSON.stringify(wf) === savedSnapshotRef.current) return;
|
||||
const t = setTimeout(() => {
|
||||
save();
|
||||
}, 800);
|
||||
return () => clearTimeout(t);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [wf]);
|
||||
|
||||
// Re-render every 15s so the "Saved … ago" label stays current.
|
||||
useEffect(() => {
|
||||
if (!lastSaved) return;
|
||||
const iv = setInterval(() => setTick((n) => n + 1), 15000);
|
||||
return () => clearInterval(iv);
|
||||
}, [lastSaved]);
|
||||
|
||||
const run = async () => {
|
||||
setRunning(true);
|
||||
setError(null);
|
||||
@@ -315,7 +355,9 @@ export default function WorkflowBuilder() {
|
||||
<div className="flex items-center gap-1.5 text-sm">
|
||||
<span className="text-text-secondary">Workflows /</span>
|
||||
<span className="font-medium text-text-primary">{wf.name}</span>
|
||||
<span className="text-text-secondary">· draft</span>
|
||||
<span className="text-text-secondary">
|
||||
· {saving ? "Saving…" : lastSaved ? `Saved ${timeAgo(lastSaved)}` : "draft"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<span className="rounded-full border border-border bg-surface-2 px-3 py-1 text-xs text-text-secondary">
|
||||
@@ -330,9 +372,6 @@ export default function WorkflowBuilder() {
|
||||
<Button variant="secondary" size="sm" onClick={() => setEditWorkflowOpen(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" loading={saving} onClick={save}>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
loading={running}
|
||||
|
||||
Reference in New Issue
Block a user