mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
bdd8c08dd4
The day plan wrote straight to the API, so offline a place dropped on a day snapped back with "Error assigning place", a renamed day or a new day note failed, and a moved stop, a leg's travel mode or who joins a visit were lost. Visits (put on a day, taken off, reordered, moved, travellers, leg mode), the day's name, notes text and travel mode, and the day notes (add, edit, delete, move) now go through assignmentRepo, dayRepo and dayNoteRepo and the mutation queue: the cached day changes at once, a visit or note made offline gets a temporary id that the edits after it wait for, and an online write held behind an older queued one joins the queue, since none of these carry a version token. A new order of a day that names a visit made offline is sent with its real id, or without it when its create failed. Desktop, the phone timeline and note sheet all write through the store, so both are covered. The replay reads each answer for what it names: the day answer keeps the cached notes and visits, the participants answer lands on its visit, and a visit the server refuses with 409 (a Tour already on that day) fails instead of waiting as a conflict no retry can solve. The trip's name, description, reminder and the archive flag wait in the queue too, from the planner, the phone and the dashboard. What only the server can do says so instead of failing: new dates, a day count, a currency or a cover, a new trip, a copy or a delete, members, guests, share and invite links, and a new order of the days, which moves the dates of the bookings. Refs #2601
131 lines
4.5 KiB
TypeScript
131 lines
4.5 KiB
TypeScript
import type { DayNoteCreateRequest, DayNoteUpdateRequest } from '@trek/shared';
|
|
import { dayNotesApi } from '../api/client';
|
|
import { offlineDb } from '../db/offlineDb';
|
|
import { generateUUID, mutationQueue, nextTempId } from '../sync/mutationQueue';
|
|
import { isEffectivelyOffline } from '../sync/networkMode';
|
|
import type { DayNote } from '../types';
|
|
import { cacheAfterWrite } from './dayRepo';
|
|
|
|
/**
|
|
* The notes of a day live on the cached day (`notes_items`), the way the day
|
|
* list answers them. Writes the server already took land there too, without
|
|
* waiting: once the server has the note, a failing IndexedDB must not report
|
|
* the save as failed.
|
|
*/
|
|
function cacheNote(dayId: number, note: DayNote, replaceId: number = note.id): Promise<void> {
|
|
return offlineDb.days
|
|
.where('id')
|
|
.equals(dayId)
|
|
.modify((day) => {
|
|
const notes = day.notes_items ?? [];
|
|
day.notes_items = notes.some((n) => n.id === replaceId)
|
|
? notes.map((n) => (n.id === replaceId ? { ...n, ...note } : n))
|
|
: [...notes, note];
|
|
})
|
|
.then(() => undefined);
|
|
}
|
|
|
|
function uncacheNote(dayId: number, noteId: number): Promise<void> {
|
|
return offlineDb.days
|
|
.where('id')
|
|
.equals(dayId)
|
|
.modify((day) => {
|
|
day.notes_items = (day.notes_items ?? []).filter((n) => n.id !== noteId);
|
|
})
|
|
.then(() => undefined);
|
|
}
|
|
|
|
function noteUrl(tripId: number | string, dayId: number, id: number): string {
|
|
return `/trips/${tripId}/days/${dayId}/notes/${id < 0 ? '{id}' : id}`;
|
|
}
|
|
|
|
/** Queue a write to one note, held behind any older write to it (a create offline included). */
|
|
async function enqueueNoteWrite(
|
|
tripId: number | string,
|
|
dayId: number,
|
|
id: number,
|
|
method: 'PUT' | 'DELETE',
|
|
body: unknown
|
|
): Promise<void> {
|
|
await mutationQueue.enqueue({
|
|
id: generateUUID(),
|
|
tripId: Number(tripId),
|
|
method,
|
|
url: noteUrl(tripId, dayId, id),
|
|
body,
|
|
resource: 'dayNotes',
|
|
entityId: id,
|
|
...(id < 0 ? { tempEntityId: id } : {}),
|
|
});
|
|
mutationQueue.sendSoon();
|
|
}
|
|
|
|
/**
|
|
* The notes of a day through the offline core. A note written offline lands on
|
|
* the cached day and in the mutation queue and replays once the connection is
|
|
* back. Notes carry no version token, so an online write to a note that still
|
|
* has an older one queued joins the queue behind it (mustQueue).
|
|
*/
|
|
export const dayNoteRepo = {
|
|
async create(tripId: number | string, dayId: number, data: DayNoteCreateRequest): Promise<{ note: DayNote }> {
|
|
if (!isEffectivelyOffline()) {
|
|
const result: { note: DayNote } = await dayNotesApi.create(tripId, dayId, data);
|
|
cacheAfterWrite(cacheNote(dayId, result.note), 'adding a day note');
|
|
return result;
|
|
}
|
|
const tempId = nextTempId();
|
|
const note: DayNote = {
|
|
id: tempId,
|
|
day_id: dayId,
|
|
trip_id: Number(tripId),
|
|
text: data.text,
|
|
time: data.time ?? null,
|
|
icon: data.icon ?? null,
|
|
color: data.color ?? null,
|
|
...(data.sort_order === undefined ? {} : { sort_order: data.sort_order }),
|
|
created_at: new Date().toISOString(),
|
|
};
|
|
await offlineDb.transaction('rw', offlineDb.days, offlineDb.mutationQueue, async () => {
|
|
await cacheNote(dayId, note);
|
|
await mutationQueue.enqueue({
|
|
id: generateUUID(),
|
|
tripId: Number(tripId),
|
|
method: 'POST',
|
|
url: `/trips/${tripId}/days/${dayId}/notes`,
|
|
body: data,
|
|
resource: 'dayNotes',
|
|
tempId,
|
|
});
|
|
});
|
|
return { note };
|
|
},
|
|
|
|
/** `before` is the note as it stands, so an offline edit can show the whole note. */
|
|
async update(
|
|
tripId: number | string,
|
|
dayId: number,
|
|
before: DayNote,
|
|
data: DayNoteUpdateRequest
|
|
): Promise<{ note: DayNote }> {
|
|
if (!(await mutationQueue.mustQueue('dayNotes', before.id))) {
|
|
const result: { note: DayNote } = await dayNotesApi.update(tripId, dayId, before.id, data);
|
|
cacheAfterWrite(cacheNote(dayId, result.note), 'editing a day note');
|
|
return result;
|
|
}
|
|
const note = { ...before, ...data } as DayNote;
|
|
await cacheNote(dayId, note);
|
|
await enqueueNoteWrite(tripId, dayId, before.id, 'PUT', data);
|
|
return { note };
|
|
},
|
|
|
|
async delete(tripId: number | string, dayId: number, id: number): Promise<void> {
|
|
if (!(await mutationQueue.mustQueue('dayNotes', id))) {
|
|
await dayNotesApi.delete(tripId, dayId, id);
|
|
cacheAfterWrite(uncacheNote(dayId, id), 'deleting a day note');
|
|
return;
|
|
}
|
|
await uncacheNote(dayId, id);
|
|
await enqueueNoteWrite(tripId, dayId, id, 'DELETE', undefined);
|
|
},
|
|
};
|