admin: fix drink-order reorder race under rapid clicks
BarDrinkEditor derived selected drink order straight from the bar prop, which only refreshes once a fire-and-forget PATCH's reload() lands. A second reorder/add/remove click before that round-trip completed re-derived from the same stale array the first click started from, so whichever PATCH the server applied last silently won and discarded the rest. Track our own in-flight edits in local pendingIds state so back-to-back clicks chain off each other instead of the lagging prop; it resets to null (defer to the prop) whenever a fresh bar.drink_ids comes back, whether that's our own round-trip landing or an edit from elsewhere.
This commit is contained in:
parent
f576fbde3e
commit
a589f88cd8
1 changed files with 24 additions and 4 deletions
|
|
@ -211,7 +211,23 @@ function BarDrinkEditor({
|
||||||
onChange: (ids: number[]) => void;
|
onChange: (ids: number[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const byId = new Map(allDrinks.map(d => [d.id, d]));
|
const byId = new Map(allDrinks.map(d => [d.id, d]));
|
||||||
const selected = bar.drink_ids.filter(id => {
|
|
||||||
|
// `onChange` fires a fire-and-forget PATCH (see Bars.patch) that only
|
||||||
|
// resolves into a fresh `bar` prop once its `reload()` completes. Deriving
|
||||||
|
// `selected` straight from `bar.drink_ids` meant a second click before that
|
||||||
|
// round-trip landed re-derived its reorder/add/remove from the same stale
|
||||||
|
// array the first click started from — whichever PATCH the server applied
|
||||||
|
// last won, silently discarding the rest. `pendingIds` tracks our own
|
||||||
|
// in-flight optimistic state so back-to-back clicks chain off each other
|
||||||
|
// instead of the lagging prop; it's cleared whenever the server sends back
|
||||||
|
// a fresh `bar.drink_ids` (our own round-trip landing, or an edit from
|
||||||
|
// elsewhere), deferring to that as the new source of truth.
|
||||||
|
const [pendingIds, setPendingIds] = useState<number[] | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
setPendingIds(null);
|
||||||
|
}, [bar.drink_ids]);
|
||||||
|
|
||||||
|
const selected = (pendingIds ?? bar.drink_ids).filter(id => {
|
||||||
const d = byId.get(id);
|
const d = byId.get(id);
|
||||||
return d && !d.archived;
|
return d && !d.archived;
|
||||||
});
|
});
|
||||||
|
|
@ -220,18 +236,22 @@ function BarDrinkEditor({
|
||||||
const dragFrom = useRef<number | null>(null);
|
const dragFrom = useRef<number | null>(null);
|
||||||
const [dragOver, setDragOver] = useState<number | null>(null);
|
const [dragOver, setDragOver] = useState<number | null>(null);
|
||||||
|
|
||||||
|
function commit(next: number[]) {
|
||||||
|
setPendingIds(next);
|
||||||
|
onChange(next);
|
||||||
|
}
|
||||||
function remove(idx: number) {
|
function remove(idx: number) {
|
||||||
onChange(selected.filter((_, i) => i !== idx));
|
commit(selected.filter((_, i) => i !== idx));
|
||||||
}
|
}
|
||||||
function add(id: number) {
|
function add(id: number) {
|
||||||
onChange([...selected, id]);
|
commit([...selected, id]);
|
||||||
}
|
}
|
||||||
function reorder(from: number, to: number) {
|
function reorder(from: number, to: number) {
|
||||||
if (from === to || from < 0 || to < 0 || from >= selected.length || to >= selected.length) return;
|
if (from === to || from < 0 || to < 0 || from >= selected.length || to >= selected.length) return;
|
||||||
const next = selected.slice();
|
const next = selected.slice();
|
||||||
const [moved] = next.splice(from, 1);
|
const [moved] = next.splice(from, 1);
|
||||||
next.splice(to, 0, moved!);
|
next.splice(to, 0, moved!);
|
||||||
onChange(next);
|
commit(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue