feat(FEAT-3): reslot on all participant mutation paths

Reslot (stable sort by init desc, tie-break = original array index) now
fires on all 4 paths that can change order:

1. Add participant — sortParticipantsByInitiative([...parts, new], parts)
2. Edit modal save (handleUpdateParticipant) — reslot + syncTurnOrder
3. Drag reorder — splice move (already correct, untouched)
4. Inline init field — reslot (already committed 08c27c1)

Before: add appended (ignored init), edit modal overwrote value without
moving slot. Both caused list order to drift from init order until
startEncounter (sorts once). Now any init change immediately reslots
into correct position. Display + AdminView reflect order.

Stable sort preserves drag order within ties (tie-break = original index
= reflects prior drag). Move-one semantics: only changed element moves.

EditParticipantModal: added htmlFor/id link on Initiative label (was
missing — a11y + testable).

Tests: ReslotAllPaths.test.js (2). RED first (add appended, edit modal
no reslot), green after impl.
This commit is contained in:
david raistrick
2026-07-01 23:00:40 -04:00
parent 36d7186a54
commit d00cc104c9
3 changed files with 86 additions and 3 deletions
+9 -3
View File
@@ -444,9 +444,10 @@ function EditParticipantModal({ participant, onClose, onSave }) {
/>
</div>
<div>
<label className="block text-sm font-medium text-stone-300">Initiative</label>
<label htmlFor="edit-initiative" className="block text-sm font-medium text-stone-300">Initiative</label>
<input
type="number"
id="edit-initiative"
value={initiative}
onChange={(e) => setInitiative(e.target.value)}
className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
@@ -857,7 +858,8 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
try {
await storage.updateDoc(encounterPath, {
participants: [...participants, newParticipant]
participants: sortParticipantsByInitiative([...participants, newParticipant], participants),
...syncTurnOrder([...participants, newParticipant]),
});
logAction(`${nameToAdd} added to encounter (Initiative: ${computedInitiative})`, { encounterName: encounter.name }, {
encounterPath,
@@ -939,9 +941,13 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
const updatedParticipants = participants.map(p =>
p.id === editingParticipant.id ? { ...p, ...updatedData } : p
);
const reslotted = sortParticipantsByInitiative(updatedParticipants, participants);
try {
await storage.updateDoc(encounterPath, { participants: updatedParticipants });
await storage.updateDoc(encounterPath, {
participants: reslotted,
...syncTurnOrder(reslotted),
});
setEditingParticipant(null);
} catch (err) {
console.error("Error updating participant:", err);