mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
fix(collab): a note, poll, message or link that cannot be saved offline says it needs a connection
Collab, packing bags, packing templates and imports, the category a place form creates and the place imports are not kept on the device, so offline their writes cannot wait in the queue. They failed with a bare "Error" or the generic save message. While TREK is offline the failure now reads "This needs a connection. Try again once TREK is back online.", on desktop and on the phone's Collab notes; online nothing changes. Refs #2601
This commit is contained in:
@@ -14,6 +14,7 @@ import ConfirmDialog from '../shared/ConfirmDialog'
|
||||
import EmptyState from '../shared/EmptyState'
|
||||
import { Tooltip } from '../shared/Tooltip'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import { offlineOr } from '../../utils/needsConnection'
|
||||
|
||||
const FONT = "var(--font-system)"
|
||||
|
||||
@@ -218,7 +219,7 @@ export default function CollabLinks({ tripId }: { tripId: number }) {
|
||||
setLinks(v => [d.link, ...v])
|
||||
} catch (err) {
|
||||
console.error('Failed to add collab link:', err)
|
||||
toast.error(t('common.error'))
|
||||
toast.error(offlineOr(t, t('common.error')))
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -229,7 +230,7 @@ export default function CollabLinks({ tripId }: { tripId: number }) {
|
||||
setLinks(v => v.map(x => x.id === link.id ? d.link : x).sort((a, b) => Number(b.pinned) - Number(a.pinned)))
|
||||
} catch (err) {
|
||||
console.error('Failed to pin collab link:', err)
|
||||
toast.error(t('common.error'))
|
||||
toast.error(offlineOr(t, t('common.error')))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +240,7 @@ export default function CollabLinks({ tripId }: { tripId: number }) {
|
||||
setLinks(v => v.map(x => x.id === link.id ? d.link : x))
|
||||
} catch (err) {
|
||||
console.error('Failed to edit collab link:', err)
|
||||
toast.error(t('common.error'))
|
||||
toast.error(offlineOr(t, t('common.error')))
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -250,7 +251,7 @@ export default function CollabLinks({ tripId }: { tripId: number }) {
|
||||
setLinks(v => v.filter(x => x.id !== id))
|
||||
} catch (err) {
|
||||
console.error('Failed to delete collab link:', err)
|
||||
toast.error(t('common.error'))
|
||||
toast.error(offlineOr(t, t('common.error')))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { FilePreviewPortal } from './CollabNotesFilePreviewPortal'
|
||||
import { AuthedImg } from './CollabNotesAuthedImg'
|
||||
import { safeExternalHref } from '../../utils/safeUrl'
|
||||
import { useIsPhone } from '../../mobile/useIsPhone'
|
||||
import { offlineOr } from '../../utils/needsConnection'
|
||||
|
||||
// ── Main Component ──────────────────────────────────────────────────────────
|
||||
interface CollabNotesProps {
|
||||
@@ -88,7 +89,7 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) {
|
||||
try {
|
||||
created = await collabApi.createNote(tripId, data)
|
||||
} catch (err) {
|
||||
toast.error(t('common.error'))
|
||||
toast.error(offlineOr(t, t('common.error')))
|
||||
throw err
|
||||
}
|
||||
if (created) {
|
||||
@@ -114,7 +115,7 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) {
|
||||
result = await collabApi.updateNote(tripId, noteId, data)
|
||||
} catch (err) {
|
||||
// A batch of writes reports once for the whole run instead of once per note.
|
||||
if (!opts.silent) toast.error(t('common.error'))
|
||||
if (!opts.silent) toast.error(offlineOr(t, t('common.error')))
|
||||
throw err
|
||||
}
|
||||
const updated = result?.note || result
|
||||
@@ -168,7 +169,7 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) {
|
||||
// settings modal has to stay open on the rejection instead of closing on it.
|
||||
if (failed > 0) {
|
||||
await resyncNotes()
|
||||
toast.error(t('common.error'))
|
||||
toast.error(offlineOr(t, t('common.error')))
|
||||
throw new Error(`rename failed for ${failed} of ${toUpdate.length} notes`)
|
||||
}
|
||||
}, [notes, handleUpdateNote, resyncNotes, toast, t])
|
||||
@@ -186,7 +187,7 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) {
|
||||
}, [editingNote, tripId, handleUpdateNote, setNotes, uploadNoteFiles])
|
||||
|
||||
const handleDeleteNoteFile = useCallback(async (noteId, fileId) => {
|
||||
try { await collabApi.deleteNoteFile(tripId, noteId, fileId) } catch { toast.error(t('common.error')) }
|
||||
try { await collabApi.deleteNoteFile(tripId, noteId, fileId) } catch { toast.error(offlineOr(t, t('common.error'))) }
|
||||
announceNoteFilesChanged()
|
||||
}, [tripId, toast, t])
|
||||
|
||||
@@ -194,7 +195,7 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) {
|
||||
try {
|
||||
await collabApi.deleteNote(tripId, noteId)
|
||||
} catch (err) {
|
||||
toast.error(t('common.error'))
|
||||
toast.error(offlineOr(t, t('common.error')))
|
||||
throw err
|
||||
}
|
||||
setNotes(prev => prev.filter(n => n.id !== noteId))
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState, type ChangeEvent } from 'reac
|
||||
import { collabApi } from '../../api/client';
|
||||
import { addListener, removeListener } from '../../api/websocket';
|
||||
import type { useTranslation } from '../../i18n';
|
||||
import { offlineOr } from '../../utils/needsConnection';
|
||||
import type { useToast } from '../shared/Toast';
|
||||
import type { ChatMessage, ChatReaction } from './collabModel';
|
||||
import { MAX_CHAT_IMAGES, useChatImages } from './useChatImages';
|
||||
@@ -143,7 +144,7 @@ export function useCollabChatData({
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
if (toastLoadMoreErrors) toast.error(t('common.error'));
|
||||
if (toastLoadMoreErrors) toast.error(offlineOr(t, t('common.error')));
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
@@ -204,7 +205,7 @@ export function useCollabChatData({
|
||||
isAtBottom.current = true;
|
||||
setTimeout(() => scrollToBottom('smooth'), 50);
|
||||
} catch {
|
||||
toast.error(t('common.error'));
|
||||
toast.error(offlineOr(t, t('common.error')));
|
||||
} finally {
|
||||
setSending(false);
|
||||
setUploadProgress(0);
|
||||
@@ -218,7 +219,7 @@ export function useCollabChatData({
|
||||
await collabApi.deleteMessage(tripId, msgId);
|
||||
setMessages((prev) => prev.map((m) => (m.id === msgId ? { ...m, _deleted: true } : m)));
|
||||
} catch {
|
||||
toast.error(t('common.error'));
|
||||
toast.error(offlineOr(t, t('common.error')));
|
||||
}
|
||||
},
|
||||
[tripId, toast, t]
|
||||
@@ -230,7 +231,7 @@ export function useCollabChatData({
|
||||
const data = (await collabApi.reactMessage(tripId, msgId, emoji)) as { reactions: ChatReaction[] };
|
||||
setMessages((prev) => prev.map((m) => (m.id === msgId ? { ...m, reactions: data.reactions } : m)));
|
||||
} catch {
|
||||
toast.error(t('common.error'));
|
||||
toast.error(offlineOr(t, t('common.error')));
|
||||
}
|
||||
},
|
||||
[tripId, toast, t]
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { collabApi } from '../../api/client';
|
||||
import { addListener, removeListener } from '../../api/websocket';
|
||||
import type { useTranslation } from '../../i18n';
|
||||
import { offlineOr } from '../../utils/needsConnection';
|
||||
import type { useToast } from '../shared/Toast';
|
||||
import type { CollabNoteData } from './collabModel';
|
||||
|
||||
@@ -97,7 +98,7 @@ export function useCollabNotesData<N extends { id: number } = CollabNoteData>({
|
||||
await collabApi.uploadNoteFile(tripId, noteId, fd);
|
||||
} catch (err) {
|
||||
onFileError?.(err);
|
||||
toast.error(t('common.error'));
|
||||
toast.error(offlineOr(t, t('common.error')));
|
||||
}
|
||||
}
|
||||
if (files.length > 0) announceNoteFilesChanged();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { collabApi } from '../../api/client';
|
||||
import { addListener, removeListener } from '../../api/websocket';
|
||||
import type { useTranslation } from '../../i18n';
|
||||
import { offlineOr } from '../../utils/needsConnection';
|
||||
import type { useToast } from '../shared/Toast';
|
||||
import { isPollActive, type CollabPollData } from './collabModel';
|
||||
|
||||
@@ -111,7 +112,7 @@ export function useCollabPolls({
|
||||
const created = readPoll(await collabApi.createPoll(tripId, data));
|
||||
setPolls((prev) => (prev.some((p) => p.id === created.id) ? prev : [created, ...prev]));
|
||||
} catch (err) {
|
||||
toast.error(t('common.error'));
|
||||
toast.error(offlineOr(t, t('common.error')));
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
@@ -125,7 +126,7 @@ export function useCollabPolls({
|
||||
const target = matchVoteByPollId ? pollId : updated.id;
|
||||
setPolls((prev) => prev.map((p) => (p.id === target ? updated : p)));
|
||||
} catch {
|
||||
toast.error(t('common.error'));
|
||||
toast.error(offlineOr(t, t('common.error')));
|
||||
}
|
||||
},
|
||||
[tripId, matchVoteByPollId, toast, t]
|
||||
@@ -142,7 +143,7 @@ export function useCollabPolls({
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
toast.error(t('common.error'));
|
||||
toast.error(offlineOr(t, t('common.error')));
|
||||
}
|
||||
},
|
||||
[tripId, replaceClosedPoll, toast, t]
|
||||
@@ -154,7 +155,7 @@ export function useCollabPolls({
|
||||
await collabApi.deletePoll(tripId, pollId);
|
||||
setPolls((prev) => prev.filter((p) => p.id !== pollId));
|
||||
} catch {
|
||||
toast.error(t('common.error'));
|
||||
toast.error(offlineOr(t, t('common.error')));
|
||||
}
|
||||
},
|
||||
[tripId, toast, t]
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTripStore } from '../../store/tripStore'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { PackingTemplateOption } from './PackingTemplateOption'
|
||||
import { offlineOr } from '../../utils/needsConnection'
|
||||
|
||||
interface Template {
|
||||
id: number
|
||||
@@ -50,7 +51,7 @@ export default function ApplyTemplateButton({ tripId, visibility, style, classNa
|
||||
toast.success(t('packing.templateApplied', { count: data.count }))
|
||||
setOpen(false)
|
||||
} catch {
|
||||
toast.error(t('packing.templateError'))
|
||||
toast.error(offlineOr(t, t('packing.templateError')))
|
||||
} finally {
|
||||
setApplying(false)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { packingApi } from '../../api/client';
|
||||
import { useNetworkMode } from '../../hooks/useNetworkMode';
|
||||
import type { PackingBag } from '../../types';
|
||||
import { offlineOr } from '../../utils/needsConnection';
|
||||
import { BAG_COLORS } from './packingListPanel.constants';
|
||||
import { useBagTotalsPing } from './useBagTotalsPing';
|
||||
|
||||
@@ -74,7 +75,7 @@ export function usePackingBags({
|
||||
setBags((prev) => [...prev, data.bag]);
|
||||
return { bag: data.bag };
|
||||
} catch {
|
||||
toast.error(t('packing.toast.saveError'));
|
||||
toast.error(offlineOr(t, t('packing.toast.saveError')));
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -87,7 +88,7 @@ export function usePackingBags({
|
||||
const result = await packingApi.updateBag(tripId, bagId, data);
|
||||
setBags((prev) => prev.map((b) => (b.id === bagId ? { ...b, ...result.bag } : b)));
|
||||
} catch {
|
||||
toast.error(t('common.error'));
|
||||
toast.error(offlineOr(t, t('common.error')));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,7 +97,7 @@ export function usePackingBags({
|
||||
await packingApi.deleteBag(tripId, bagId);
|
||||
setBags((prev) => prev.filter((b) => b.id !== bagId));
|
||||
} catch {
|
||||
toast.error(t('packing.toast.deleteError'));
|
||||
toast.error(offlineOr(t, t('packing.toast.deleteError')));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -105,7 +106,7 @@ export function usePackingBags({
|
||||
const result = await packingApi.setBagMembers(tripId, bagId, userIds);
|
||||
setBags((prev) => prev.map((b) => (b.id === bagId ? { ...b, members: result.members } : b)));
|
||||
} catch {
|
||||
toast.error(t('common.error'));
|
||||
toast.error(offlineOr(t, t('common.error')));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { packingApi } from '../../api/client';
|
||||
import { offlineOr } from '../../utils/needsConnection';
|
||||
|
||||
type Translate = (key: string, params?: Record<string, string | number>) => string;
|
||||
interface Toaster {
|
||||
@@ -36,7 +37,7 @@ export function usePackingCategoryAssignees({ tripId, t, toast }: { tripId: numb
|
||||
const data = await packingApi.setCategoryAssignees(tripId, category, userIds);
|
||||
setCategoryAssignees((prev) => ({ ...prev, [category]: data.assignees || [] }));
|
||||
} catch {
|
||||
toast.error(t('packing.toast.saveError'));
|
||||
toast.error(offlineOr(t, t('packing.toast.saveError')));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMemo, useState, type ChangeEvent } from 'react';
|
||||
|
||||
import { packingApi } from '../../api/client';
|
||||
import { useTripStore } from '../../store/tripStore';
|
||||
import { offlineOr } from '../../utils/needsConnection';
|
||||
import { parseImportLines } from './packingListPanel.helpers';
|
||||
|
||||
type Translate = (key: string, params?: Record<string, string | number>) => string;
|
||||
@@ -66,7 +67,7 @@ export function usePackingImport({
|
||||
setText('');
|
||||
onImported();
|
||||
} catch {
|
||||
toast.error(t('packing.importError'));
|
||||
toast.error(offlineOr(t, t('packing.importError')));
|
||||
} finally {
|
||||
if (oneAtATime) setImporting(false);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState } from 'react';
|
||||
import { placesApi } from '../../api/client';
|
||||
import type { useTranslation } from '../../i18n';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { offlineOr } from '../../utils/needsConnection';
|
||||
import type { useToast } from '../shared/Toast';
|
||||
|
||||
export type ListImportProvider = 'google' | 'naver';
|
||||
@@ -84,7 +85,9 @@ export function useListImport({ tripId, t, toast, loadTrip, pushUndo, onDone }:
|
||||
onDone?.();
|
||||
} catch (err: unknown) {
|
||||
const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error;
|
||||
toast.error(message || t(provider === 'google' ? 'places.googleListError' : 'places.naverListError'));
|
||||
toast.error(
|
||||
message || offlineOr(t, t(provider === 'google' ? 'places.googleListError' : 'places.naverListError'))
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useRef, useState } from 'react';
|
||||
|
||||
import type { useTranslation } from '../../i18n';
|
||||
import type { Category } from '../../types';
|
||||
import { offlineOr } from '../../utils/needsConnection';
|
||||
import type { useToast } from '../shared/Toast';
|
||||
import type { PlaceDetailsSelection } from './PlaceDetailsColumn';
|
||||
import {
|
||||
@@ -174,7 +175,7 @@ export function useNewCategory({ t, toast, onCreated, variant, create }: NewCate
|
||||
setName('');
|
||||
setOpen(false);
|
||||
} catch {
|
||||
toast.error(t('places.categoryCreateError'));
|
||||
toast.error(offlineOr(t, t('places.categoryCreateError')));
|
||||
} finally {
|
||||
if (sheet) setSaving(false);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useEffectEvent, useState } from 'react';
|
||||
import { placesApi } from '../../api/client';
|
||||
import type { useTranslation } from '../../i18n';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { offlineOr } from '../../utils/needsConnection';
|
||||
import type { useToast } from '../shared/Toast';
|
||||
|
||||
export interface PlacesImportSummary {
|
||||
@@ -230,7 +231,8 @@ export function usePlacesFileImport({
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message =
|
||||
(err as { response?: { data?: { error?: string } } })?.response?.data?.error || t('places.importFileError');
|
||||
(err as { response?: { data?: { error?: string } } })?.response?.data?.error ||
|
||||
offlineOr(t, t('places.importFileError'));
|
||||
errors.push(files.length > 1 ? `${f.name}: ${message}` : message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { collectionsApi } from '../../api/collections';
|
||||
import type { useTranslation } from '../../i18n';
|
||||
import { useTripStore } from '../../store/tripStore';
|
||||
import type { Place } from '../../types';
|
||||
import { offlineOr } from '../../utils/needsConnection';
|
||||
import { useToast } from '../shared/Toast';
|
||||
|
||||
/** What the pool shows: everything, what is not on a day yet, what is, or the tracks. */
|
||||
@@ -135,7 +136,7 @@ export function usePlacesPool({ tripId, places, poolPlaces, toursEnabled, t, sta
|
||||
else toast.success(t('collections.markedVisitedTrip', { count: matchedPlaces ?? 0 }));
|
||||
exitSelectMode();
|
||||
} catch {
|
||||
toast.error(t('common.error'));
|
||||
toast.error(offlineOr(t, t('common.error')));
|
||||
} finally {
|
||||
setMarkVisitedBusy(false);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type CollabNoteData,
|
||||
type CollabNoteFile,
|
||||
} from '../../../../components/Collab/collabModel'
|
||||
import { offlineOr } from '../../../../utils/needsConnection'
|
||||
|
||||
interface MCollabNotesProps {
|
||||
planner: TripPlanner
|
||||
@@ -77,7 +78,7 @@ export default function MCollabNotes({ planner }: MCollabNotesProps) {
|
||||
})) as NoteResponse
|
||||
created = res.note
|
||||
} catch {
|
||||
toast.error(t('common.error'))
|
||||
toast.error(offlineOr(t, t('common.error')))
|
||||
throw new Error('create failed')
|
||||
}
|
||||
if (data.pendingFiles.length > 0) {
|
||||
@@ -99,7 +100,7 @@ export default function MCollabNotes({ planner }: MCollabNotesProps) {
|
||||
const res = (await collabApi.updateNote(tripId, noteId, data)) as NoteResponse
|
||||
updated = res.note
|
||||
} catch {
|
||||
toast.error(t('common.error'))
|
||||
toast.error(offlineOr(t, t('common.error')))
|
||||
throw new Error('update failed')
|
||||
}
|
||||
if (pendingFiles.length > 0) {
|
||||
@@ -117,7 +118,7 @@ export default function MCollabNotes({ planner }: MCollabNotesProps) {
|
||||
setNotes(prev => prev.filter(n => n.id !== noteId))
|
||||
announceNoteFilesChanged()
|
||||
} catch {
|
||||
toast.error(t('common.error'))
|
||||
toast.error(offlineOr(t, t('common.error')))
|
||||
}
|
||||
}, [tripId, toast, t, setNotes])
|
||||
|
||||
@@ -132,7 +133,7 @@ export default function MCollabNotes({ planner }: MCollabNotesProps) {
|
||||
announceNoteFilesChanged()
|
||||
return true
|
||||
} catch {
|
||||
toast.error(t('common.error'))
|
||||
toast.error(offlineOr(t, t('common.error')))
|
||||
return false
|
||||
}
|
||||
}, [tripId, toast, t, setNotes])
|
||||
|
||||
Reference in New Issue
Block a user