fix(todo): tasks added, ticked, edited, reordered or deleted offline wait in the queue and sync once the connection is back

Every task write went straight to the API, so offline a new task failed with
"Network Error" and was lost, and a tick flipped back. Tasks now go through
todoRepo and the mutation queue like packing items: a temporary row at the
end of the list offline, the edits and the delete of it wait for its real id,
and an online write to a task that still has an older one queued joins the
queue behind it, since task writes carry no version token. A reorder offline
is queued without the tasks that have no server id yet. Desktop and phone
both write through the store, so both are covered.

Refs #2601
This commit is contained in:
Maurice
2026-10-12 18:24:18 +02:00
parent cfdbfb49c0
commit 5dbcc4af40
4 changed files with 333 additions and 6 deletions
@@ -148,3 +148,35 @@ describe('todoSlice', () => {
});
});
});
describe('todoSlice offline (#2601)', () => {
const setOnline = (v: boolean) =>
Object.defineProperty(navigator, 'onLine', { value: v, writable: true, configurable: true });
it('FE-TODO-008: offline a task is added, ticked, edited, reordered and deleted without an error', async () => {
const { clearAll, offlineDb } = await import('../../../src/db/offlineDb');
await clearAll();
const synced = buildTodoItem({ id: 10, trip_id: 1, sort_order: 0 });
await offlineDb.todoItems.put(synced);
seedStore(useTripStore, { todoItems: [synced] });
setOnline(false);
try {
const added = await useTripStore.getState().addTodoItem(1, { name: 'Offline task' });
expect(added.id).toBeLessThan(0);
expect(useTripStore.getState().todoItems.map(i => i.name)).toEqual([synced.name, 'Offline task']);
await useTripStore.getState().toggleTodoItem(1, added.id, true);
await useTripStore.getState().updateTodoItem(1, added.id, { name: 'Offline task, renamed' });
expect(useTripStore.getState().todoItems[1]).toMatchObject({ name: 'Offline task, renamed', checked: 1 });
await useTripStore.getState().reorderTodoItems(1, [added.id, 10]);
expect(useTripStore.getState().todoItems.map(i => i.id)).toEqual([added.id, 10]);
await useTripStore.getState().deleteTodoItem(1, added.id);
expect(useTripStore.getState().todoItems.map(i => i.id)).toEqual([10]);
expect((await offlineDb.mutationQueue.orderBy('createdAt').toArray()).map(m => m.method)).toEqual(['POST', 'PUT', 'PUT', 'PUT', 'DELETE']);
} finally {
setOnline(true);
}
});
});