commit 9149b013eb1d32e4d5ed413b1480a0411fe5571b
parent 8c3a13f9857f395be8f40ad646048a6262d2f46f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 23:21:19 -0400
deck S6: the On-screen table's reload after a 409 keeps only real edits
loadRows(keep) kept every row's previous draft, edited or not. After a stale
save, "reload saved values" left a row the other writer had changed showing
its OLD value, marked dirty — and the next save sent it, reverting the very
write the stale-token guard exists to protect. A draft is now kept only when
it differs from what the page last read as saved; every other row takes the
newly saved value. Found by onscreen.spec.ts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/umtool/components/projects/OnscreenSection.tsx b/umtool/components/projects/OnscreenSection.tsx
@@ -421,6 +421,11 @@ export default function OnscreenSection({
const [formDirty, setFormDirty] = useState(false);
const [errors, setErrors] = useState<string[]>([]);
const [rows, setRows] = useState<Row[]>([]);
+ // The rows as last saved, for loadRows to tell an edit from an old value.
+ const rowsRef = useRef<Row[]>([]);
+ useEffect(() => {
+ rowsRef.current = rows;
+ }, [rows]);
const [maxChars, setMaxChars] = useState(48);
const [drafts, setDrafts] = useState<Record<string, Draft>>({});
const [note, setNote] = useState<string | null>(null);
@@ -470,6 +475,11 @@ export default function OnscreenSection({
/**
* The table's rows. `keep` keeps every unsaved edit on top of what is now
* saved -- the reload after a 409 must not throw away what was typed.
+ *
+ * An EDIT is a draft that differs from what this page last read as saved.
+ * A row nobody touched here takes the newly saved value: kept as its old
+ * draft it would read as an edit, and the next save would revert the other
+ * writer's change -- the very write the stale token exists to protect.
*/
const loadRows = useCallback(
async (keep: boolean) => {
@@ -479,6 +489,7 @@ export default function OnscreenSection({
setLoadError(String(j.error ?? r.status));
return;
}
+ const before = new Map(rowsRef.current.map((row) => [row.id, draftOf(row.onscreen)]));
token.current = j.token;
setRows(j.rows);
setMaxChars(j.maxChars);
@@ -487,7 +498,8 @@ export default function OnscreenSection({
for (const row of j.rows) {
const saved = draftOf(row.onscreen);
const old = prev[row.id];
- next[row.id] = keep && old ? old : saved;
+ const was = before.get(row.id);
+ next[row.id] = keep && old && was && !sameDraft(old, was) ? old : saved;
}
return next;
});