Uh oh!
There was an error while loading. Please reload this page.
OpenConceptLab/ocl_issues#2188 | Mapper auto save - #56
Conversation
askanter
commented
Aug 20, 2026
As said on the call... I think anything that touches the concept or map should autosave... so even manual maps. However, "environmental" changes such as regenerating candidates perhaps should not. However, as soon as the environment is used to change actual data on a row, then that would generate a whole project save... |
paynejd
left a comment
There was a problem hiding this comment.
Review — Mapper auto save
Overall a sound, well-scoped implementation that meets the issue's ACs. The ref-mirroring of logs/projectLogs is the right way to read fresh state from a debounced setTimeout, and the config-diff-on-close heuristic is false-negative-safe. One correctness item worth fixing before merge, plus a few minor notes.
Correctness
⚠️ StaleisSavingin the debounce closure (main concern).scheduleAutoSaveis recreated each render, and itssetTimeoutcallback closes over that render'sisSaving,project, andonSave. The reschedule branch callsscheduleAutoSave(reason), which resolves to the same render's binding — so it keeps observing the schedule-timeisSavingforever. If a schedule happens whileisSaving === true(a save in flight), the timer sees staletrue, reschedules, and never observes the save completing → autosave can silently wedge until the next fresh trigger. Recommend backing this with a ref:constisSavingRef=React.useRef(false)// wherever you setIsSaving(x): isSavingRef.current = x; setIsSaving(x)// in the timer: if (isSavingRef.current) { scheduleAutoSave(reason); return }
Same reasoning applies to
project?.idread inside the timer — a ref avoids a stale null right after an initial create.Double
setIsSaving(false)(benign). The.thensets it and the new.finallysets it again on success. Harmless — and the.finallyis a real improvement, sinceisSavingpreviously could sticktrueon a rejected save.Remaining
setConfigure(false)calls (infetchAndSetProjectand the post-save success block) correctly bypasscloseConfiguration— those are load/save paths, not a user closing the drawer, so no config-diff autosave should fire. The two user-facing close points and both childsetConfigureprops were switched tosetConfigureWithAutosave/closeConfiguration. 👍
Style
onSaveevent-vs-options overload is a bit fragile.const saveOptions = options?.preventDefault ? {} : optionssniffs a clickSyntheticEventvs an options object viapreventDefault. It works and is guarded, but a thinonManualSave = () => onSave({source: 'manual'})for theonClick={onSave}bindings would read cleaner. Minor.'Auto Saved'→.toLowerCase()→'auto saved'matches bothProjectLogschecks — consistent with the other lowercase action comparisons. ✅
Test coverage
No new tests cover the autosave logic — debounce coalescing, project?.id gating, the isSaving reschedule path, or manual-save cancelling a pending autosave. A couple of fake-timer unit tests around scheduleAutoSave would be high-value and would have surfaced the stale-isSaving case.
Performance / security
Negligible perf impact (one debounce timer, torn down on unmount). No security concerns — same authenticated logs/ endpoint and payload shape; the 5s debounce keeps write frequency bounded.
Verdict: Approve modulo the isSaving ref fix (cheap) + ideally a fake-timer test or two.
paynejd
left a comment
There was a problem hiding this comment.
Tested the updated PR end-to-end in a signed-in browser (project 127): config-change autosave fires silently with an "Auto saved changes" log entry, a no-op config close does not save, an aborted AutoMatch does not save, and manual save stays distinct ("Updated"). Unit tests 7/7, eslint clean. The extracted createAutosaveScheduler + ref-getters is a clean fix for the stale-isSaving wedge. ✅ Two things to address, rest optional.
🔴 Must fix — rebase on main (blocks login)
This branch predates the OpenConceptLab/ocl_online#131 PKCE commits now required by the prod OIDC. As-is, sign-in fails with invalid_request: Missing parameter: code_challenge_method. A merge of origin/main is clean (no conflicts) and restores login — please rebase before merging.
🐛 Bug — "Created" history line shows a raw user id after any save
After a save reloads the project, the Created entry in Project History flips from the username to the numeric created_by (e.g. jon → 2456), because getTitle reads project.created_by off the PUT response. Autosave makes saves frequent, so users now hit this constantly. Prefer the log's own recorded user — src/components/map-projects/ProjectLogs.jsx:110 (line is outside this PR's diff, so pasting the fix rather than a one-click suggestion):
- created_by: project.created_by || '',+ created_by: log.user || project.created_by || '',Optional (consider)
onSaveevent-vs-options overload —const saveOptions = options?.preventDefault ? {} : optionssniffs a clickSyntheticEvent. A thinonManualSave = () => onSave({source: 'manual'})for theonClick={onSave}bindings would read cleaner than guarding insideonSave.- Double
setIsSaving(false)(.then+ new.finally) — harmless; keep the.finally, it fixesisSavingstickingtrueon a rejected save.
Linked Issue
ClosesOpenConceptLab/ocl_issues#2188
Summary
Main tasks
Autosave runs for:
Autosave does not run for:
Test Plan