From 9041cbcd0845ed6504dc011fe691e2c17eb18273 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Fri, 25 Sep 2026 19:41:00 +0200 Subject: [PATCH] fix: keep iCal subscriptions on refresh, rename calendars of any colour - send a calendar's colour only when the user changed it: Stalwart 1.0 accepts only #rgb, #rrggbb or a CSS name, so renaming a calendar that another client coloured with an 8-digit hex (Apple Calendar) failed - "Refresh cached data" kept nothing of the calendar store's persisted state, so it deleted the user's iCal subscriptions, which exist only there; keep them and drop the rest - show a notice with a retry when the task list fails to load; it keeps the tasks it had - treat a method-level error on creating a task as a failure with its JMAP type instead of "no id returned" --- .../calendar-load-error-notice.test.tsx | 5 ++ components/calendar/calendar-app.tsx | 5 +- .../calendar/calendar-load-error-notice.tsx | 10 ++-- .../calendar-management-settings.test.tsx | 56 +++++++++++++++++++ .../settings/calendar-management-settings.tsx | 16 +++++- lib/__tests__/clear-cached-data.test.ts | 18 ++++++ lib/__tests__/jmap-calendar-tasks.test.ts | 12 ++++ lib/clear-cached-data.ts | 31 ++++++++-- lib/jmap/client.ts | 18 ++++-- locales/ar/common.json | 1 + locales/ca/common.json | 1 + locales/cs/common.json | 1 + locales/da/common.json | 1 + locales/de/common.json | 1 + locales/en/common.json | 1 + locales/es/common.json | 1 + locales/fa/common.json | 1 + locales/fr/common.json | 1 + locales/he/common.json | 1 + locales/hu/common.json | 1 + locales/it/common.json | 1 + locales/ja/common.json | 1 + locales/ko/common.json | 1 + locales/lv/common.json | 1 + locales/mn/common.json | 1 + locales/nb/common.json | 1 + locales/nl/common.json | 1 + locales/pl/common.json | 1 + locales/pt/common.json | 1 + locales/ro/common.json | 1 + locales/ru/common.json | 1 + locales/sk/common.json | 1 + locales/tr/common.json | 1 + locales/uk/common.json | 1 + locales/zh-TW/common.json | 1 + locales/zh/common.json | 1 + stores/task-store.ts | 8 ++- 37 files changed, 188 insertions(+), 18 deletions(-) create mode 100644 components/settings/__tests__/calendar-management-settings.test.tsx diff --git a/components/calendar/__tests__/calendar-load-error-notice.test.tsx b/components/calendar/__tests__/calendar-load-error-notice.test.tsx index 33523e071..edb44dd0e 100644 --- a/components/calendar/__tests__/calendar-load-error-notice.test.tsx +++ b/components/calendar/__tests__/calendar-load-error-notice.test.tsx @@ -14,4 +14,9 @@ describe('CalendarLoadErrorNotice', () => { fireEvent.click(screen.getByRole('button', { name: 'retry' })); expect(onRetry).toHaveBeenCalledTimes(1); }); + + it('says the task list may be out of date for a failed task load', () => { + render(); + expect(screen.getByRole('alert')).toHaveTextContent('tasks_load_failed'); + }); }); diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx index 0cd62db5f..3699acac4 100644 --- a/components/calendar/calendar-app.tsx +++ b/components/calendar/calendar-app.tsx @@ -65,7 +65,7 @@ import { getClientByLocalAccountId } from "@/stores/client-registry"; import { useCalendarErrorMessage } from "@/hooks/use-calendar-error-message"; import { getEventStartDate } from "@/lib/calendar-utils"; import { displayNow } from "@/lib/timezone"; -import { useTaskStore } from "@/stores/task-store"; +import { TASKS_LOAD_ERROR, useTaskStore } from "@/stores/task-store"; import { useContactStore } from "@/stores/contact-store"; import { cn } from "@/lib/utils"; import type { Calendar, CalendarEvent, CalendarParticipant, CalendarRights } from "@/lib/jmap/types"; @@ -1542,6 +1542,9 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = case "tasks": return (
+ {taskStore.error === TASKS_LOAD_ERROR && ( + { if (client) void fetchTasksFn(client); }} /> + )} void; } /** - * Shown above the grid while the visible range could not be loaded. The grid - * keeps the events it had, so this says why they may be out of date. + * Shown above the grid (or the task list) while it could not be loaded. It + * keeps what it had, so this says why that may be out of date. */ -export function CalendarLoadErrorNotice({ onRetry }: CalendarLoadErrorNoticeProps) { +export function CalendarLoadErrorNotice({ what = "events", onRetry }: CalendarLoadErrorNoticeProps) { const t = useTranslations("calendar.status"); return (
- {t("events_load_failed")} + {what === "tasks" ? t("tasks_load_failed") : t("events_load_failed")} diff --git a/components/settings/__tests__/calendar-management-settings.test.tsx b/components/settings/__tests__/calendar-management-settings.test.tsx new file mode 100644 index 000000000..850bb4d76 --- /dev/null +++ b/components/settings/__tests__/calendar-management-settings.test.tsx @@ -0,0 +1,56 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { CalendarManagementSettings } from '../calendar-management-settings'; +import { useCalendarStore } from '@/stores/calendar-store'; +import { useAuthStore } from '@/stores/auth-store'; +import { CALENDAR_COLORS } from '@/lib/shared-calendar-colors'; +import type { Calendar } from '@/lib/jmap/types'; + +// Stalwart 1.0 accepts a calendar colour only as #rgb, #rrggbb or a CSS name +// (calendar/set.rs); 0.16 took anything shorter than 16 characters. Renaming +// a calendar another client coloured with an 8-digit hex (Apple Calendar) +// sent that colour back and failed on 1.0. + +vi.mock('../settings-section', async (importOriginal) => ({ + ...(await importOriginal()), + SettingsSection: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +const appleColored = { + id: 'cal', name: 'Home', color: '#FF2968FF', isDefault: true, isShared: false, shareWith: null, + myRights: { mayShare: true, mayDelete: true, mayWriteAll: true }, +} as unknown as Calendar; + +describe('CalendarManagementSettings rename', () => { + let updateCalendar: ReturnType; + + beforeEach(() => { + updateCalendar = vi.fn().mockResolvedValue(undefined); + // No server URL: the CalDAV discovery stays out of it. + useAuthStore.setState({ client: { getAccountId: () => 'acct-1' }, serverUrl: null, username: null } as never); + useCalendarStore.setState({ calendars: [appleColored], icalSubscriptions: [], updateCalendar } as never); + }); + + function openEditor() { + render(); + fireEvent.click(screen.getByTitle('edit')); + } + + it('sends the new name without the colour it did not change, for Stalwart 1.0', async () => { + openEditor(); + fireEvent.change(screen.getByDisplayValue('Home'), { target: { value: 'Family' } }); + fireEvent.click(screen.getByText('save')); + + await waitFor(() => expect(updateCalendar).toHaveBeenCalledTimes(1)); + expect(updateCalendar.mock.calls[0].slice(1)).toEqual(['cal', { name: 'Family' }]); + }); + + it('sends a colour the user picked', async () => { + openEditor(); + fireEvent.click(screen.getByLabelText(CALENDAR_COLORS[0])); + fireEvent.click(screen.getByText('save')); + + await waitFor(() => expect(updateCalendar).toHaveBeenCalledTimes(1)); + expect(updateCalendar.mock.calls[0].slice(1)).toEqual(['cal', { name: 'Home', color: CALENDAR_COLORS[0] }]); + }); +}); diff --git a/components/settings/calendar-management-settings.tsx b/components/settings/calendar-management-settings.tsx index 1dfddc219..64b436480 100644 --- a/components/settings/calendar-management-settings.tsx +++ b/components/settings/calendar-management-settings.tsx @@ -315,11 +315,21 @@ export function CalendarManagementSettings() { } }; - const handleUpdate = async (calendarId: string, data: { name: string; color: string }) => { + const handleUpdate = async ( + calendarId: string, + data: { name: string; color: string }, + shownColor: string, + ) => { if (!client) return; setIsLoading(true); try { - await updateCalendar(client, calendarId, { name: data.name, color: data.color }); + // The colour only when the user changed it: Stalwart 1.0 accepts only + // #rgb, #rrggbb or a CSS colour name, so sending back one another + // client stored (an 8-digit hex from Apple Calendar) failed the rename. + await updateCalendar(client, calendarId, { + name: data.name, + ...(data.color !== shownColor ? { color: data.color } : {}), + }); setEditingId(null); toast.success(t('calendar_updated')); } catch { @@ -417,7 +427,7 @@ export function CalendarManagementSettings() { handleUpdate(cal.id, data)} + onSave={(data) => handleUpdate(cal.id, data, color)} onCancel={() => setEditingId(null)} isLoading={isLoading} /> diff --git a/lib/__tests__/clear-cached-data.test.ts b/lib/__tests__/clear-cached-data.test.ts index 9c8ac8e02..1830c7a33 100644 --- a/lib/__tests__/clear-cached-data.test.ts +++ b/lib/__tests__/clear-cached-data.test.ts @@ -49,4 +49,22 @@ describe('clearCachedData', () => { clearCachedData(); expect(reload).toHaveBeenCalledOnce(); }); + + it('keeps the iCal subscriptions, which exist nowhere else, and drops the rest of the calendar state', () => { + const subscription = { id: 's1', url: 'https://example.com/holidays.ics', calendarId: 'c9', name: 'Holidays', color: '#ff0000', refreshInterval: 60, lastRefreshed: null }; + localStorage.setItem('calendar-storage', JSON.stringify({ + state: { selectedCalendarIds: ['c1', 'c9'], viewMode: 'week', icalSubscriptions: [subscription] }, + version: 0, + })); + + clearCachedData(); + + expect(JSON.parse(localStorage.getItem('calendar-storage')!)).toEqual({ state: { icalSubscriptions: [subscription] }, version: 0 }); + }); + + it('removes calendar state without subscriptions entirely', () => { + localStorage.setItem('calendar-storage', JSON.stringify({ state: { viewMode: 'week', icalSubscriptions: [] }, version: 0 })); + clearCachedData(); + expect(localStorage.getItem('calendar-storage')).toBeNull(); + }); }); diff --git a/lib/__tests__/jmap-calendar-tasks.test.ts b/lib/__tests__/jmap-calendar-tasks.test.ts index 9be08d8e9..e5e69b974 100644 --- a/lib/__tests__/jmap-calendar-tasks.test.ts +++ b/lib/__tests__/jmap-calendar-tasks.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { JMAPClient } from '../jmap/client'; import type { CalendarTask } from '../jmap/types'; +import { jmapErrorType } from '../jmap/jmap-error'; function makeSession() { return { @@ -158,4 +159,15 @@ describe('JMAPClient task operations (#958)', () => { await expect(client.getCalendarTasks()).rejects.toThrow(/Store unavailable/); }); + + it('createCalendarTask fails with the error type when the server refuses the whole call', async () => { + vi.mocked(globalThis.fetch).mockImplementation(async () => jsonResponse({ + methodResponses: [['error', { type: 'invalidArguments', description: 'Invalid calendar id' }, '0']], + })); + + const error = await client.createCalendarTask({ title: 'New Task' }).then(() => null, (e: unknown) => e); + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain('Invalid calendar id'); + expect(jmapErrorType(error)).toBe('invalidArguments'); + }); }); diff --git a/lib/clear-cached-data.ts b/lib/clear-cached-data.ts index 412a573c4..c7cc8219c 100644 --- a/lib/clear-cached-data.ts +++ b/lib/clear-cached-data.ts @@ -16,12 +16,34 @@ const CACHE_STORAGE_KEYS = [ 'calendar-notification-storage', ]; +/** + * The user's iCal subscriptions exist only in the calendar store's persisted + * state, next to its caches: keep them and drop the rest. + */ +function clearCalendarStorage(storage: Storage): void { + let persisted: { state?: { icalSubscriptions?: unknown }; version?: unknown } | null = null; + try { + persisted = JSON.parse(storage.getItem('calendar-storage') ?? 'null'); + } catch { + /* not ours to read: drop it */ + } + const subscriptions = persisted?.state?.icalSubscriptions; + if (!Array.isArray(subscriptions) || subscriptions.length === 0) { + storage.removeItem('calendar-storage'); + return; + } + storage.setItem('calendar-storage', JSON.stringify({ + state: { icalSubscriptions: subscriptions }, + version: persisted?.version ?? 0, + })); +} + /** * Clear cached, server-derived data (contacts, calendars, identities, and the * in-memory per-account snapshots), then reload so everything is re-fetched - * fresh for the active account. Accounts and sessions are preserved — this is - * the non-destructive alternative to the browser's "clear site data", which - * also wipes the account list. + * fresh for the active account. Accounts, sessions and iCal subscriptions are + * preserved — this is the non-destructive alternative to the browser's + * "clear site data", which also wipes the account list. */ export function clearCachedData(): void { // Drop the in-memory per-account store snapshots so a reload can't restore @@ -36,7 +58,8 @@ export function clearCachedData(): void { for (const key of CACHE_STORAGE_KEYS) { try { - window.localStorage.removeItem(key); + if (key === 'calendar-storage') clearCalendarStorage(window.localStorage); + else window.localStorage.removeItem(key); } catch { /* ignore storage access errors */ } diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 7b32f243c..d4814c6f5 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -7953,18 +7953,26 @@ export class JMAPClient implements IJMAPClient { }, "0"] ], this.calendarUsing()); - const result = response.methodResponses?.[0]?.[1]; + let result: JMAPResponseResult; + try { + // A method-level error (unknown calendar, missing capability) means + // nothing was created. + result = methodResult(response, "0", "CalendarEvent/set"); + } catch (error) { + debug.groupEnd(); + throw error; + } debug.log('tasks', 'CalendarTask/create raw set response', result); - if (result?.notCreated?.["new-task"]) { + if (result.notCreated?.["new-task"]) { const error = result.notCreated["new-task"]; debug.warn('tasks', 'CalendarTask/create REJECTED by server', error); debug.groupEnd(); - throw new Error(error.description || "Failed to create task"); + throw jmapSetError(error.description || "Failed to create task", error); } - const createdId = result?.created?.["new-task"]?.id; - const serverCreated = result?.created?.["new-task"]; + const createdId = result.created?.["new-task"]?.id; + const serverCreated = result.created?.["new-task"]; debug.log('tasks', 'CalendarTask/create server acknowledged', { createdId, serverCreated }); if (!createdId) { diff --git a/locales/ar/common.json b/locales/ar/common.json index a75e5198f..25a5c3964 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "جارٍ تحميل التقويمات...", "loading_events": "جارٍ تحميل الأحداث...", "events_load_failed": "تعذّر تحميل الأحداث. قد لا يكون التقويم محدَّثًا.", + "tasks_load_failed": "تعذّر تحميل المهام. قد لا تكون القائمة محدَّثة.", "retry": "حاول مرة أخرى" }, "quick_create": { diff --git a/locales/ca/common.json b/locales/ca/common.json index ceaec0b8e..38dabfb69 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "Carregant els calendaris...", "loading_events": "Carregant els esdeveniments...", "events_load_failed": "No s'han pogut carregar els esdeveniments. És possible que el calendari no estigui actualitzat.", + "tasks_load_failed": "No s'han pogut carregar les tasques. És possible que la llista no estigui actualitzada.", "retry": "Torna-ho a provar" }, "quick_create": { diff --git a/locales/cs/common.json b/locales/cs/common.json index 52a7c877d..85fe64886 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -3045,6 +3045,7 @@ "loading_calendars": "Načítání kalendářů...", "loading_events": "Načítání událostí...", "events_load_failed": "Události se nepodařilo načíst. Kalendář nemusí být aktuální.", + "tasks_load_failed": "Úkoly se nepodařilo načíst. Seznam nemusí být aktuální.", "retry": "Zkusit znovu" }, "quick_create": { diff --git a/locales/da/common.json b/locales/da/common.json index 5b07e88c5..53a58c991 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "Indlæser kalendere...", "loading_events": "Indlæser begivenheder...", "events_load_failed": "Begivenhederne kunne ikke indlæses. Kalenderen er muligvis ikke opdateret.", + "tasks_load_failed": "Opgaverne kunne ikke indlæses. Listen er muligvis ikke opdateret.", "retry": "Prøv igen" }, "quick_create": { diff --git a/locales/de/common.json b/locales/de/common.json index 4e5262ca7..df2d42ec5 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -3045,6 +3045,7 @@ "loading_calendars": "Kalender werden geladen...", "loading_events": "Termine werden geladen...", "events_load_failed": "Termine konnten nicht geladen werden. Der Kalender ist möglicherweise nicht aktuell.", + "tasks_load_failed": "Aufgaben konnten nicht geladen werden. Die Liste ist möglicherweise nicht aktuell.", "retry": "Erneut versuchen" }, "quick_create": { diff --git a/locales/en/common.json b/locales/en/common.json index 919dd147f..52eefc50b 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "Loading calendars...", "loading_events": "Loading events...", "events_load_failed": "Couldn't load events. The calendar may be out of date.", + "tasks_load_failed": "Couldn't load tasks. The list may be out of date.", "retry": "Try again" }, "quick_create": { diff --git a/locales/es/common.json b/locales/es/common.json index f2a848a46..705415c35 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -3045,6 +3045,7 @@ "loading_calendars": "Cargando calendarios...", "loading_events": "Cargando eventos...", "events_load_failed": "No se pudieron cargar los eventos. Es posible que el calendario no esté actualizado.", + "tasks_load_failed": "No se pudieron cargar las tareas. Es posible que la lista no esté actualizada.", "retry": "Intentar de nuevo" }, "quick_create": { diff --git a/locales/fa/common.json b/locales/fa/common.json index 2292961c2..f08eb51de 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "در حال بارگذاری تقویم‌ها...", "loading_events": "در حال بارگذاری رویدادها...", "events_load_failed": "بارگذاری رویدادها ممکن نشد. ممکن است تقویم به‌روز نباشد.", + "tasks_load_failed": "بارگذاری وظایف ممکن نشد. ممکن است فهرست به‌روز نباشد.", "retry": "دوباره تلاش کنید" }, "quick_create": { diff --git a/locales/fr/common.json b/locales/fr/common.json index 2cc140888..2d68d7204 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "Chargement des calendriers...", "loading_events": "Chargement des événements...", "events_load_failed": "Impossible de charger les événements. Le calendrier n'est peut-être pas à jour.", + "tasks_load_failed": "Impossible de charger les tâches. La liste n'est peut-être pas à jour.", "retry": "Réessayer" }, "quick_create": { diff --git a/locales/he/common.json b/locales/he/common.json index 4958619b6..ab69ac736 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -2969,6 +2969,7 @@ "loading_calendars": "טוען יומנים...", "loading_events": "טוען אירועים...", "events_load_failed": "לא ניתן לטעון את האירועים. ייתכן שהיומן אינו מעודכן.", + "tasks_load_failed": "לא ניתן לטעון את המשימות. ייתכן שהרשימה אינה מעודכנת.", "retry": "נסה שוב" }, "quick_create": { diff --git a/locales/hu/common.json b/locales/hu/common.json index b3fecb20d..2cc40554d 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "Naptárak betöltése...", "loading_events": "Események betöltése...", "events_load_failed": "Nem sikerült betölteni az eseményeket. Előfordulhat, hogy a naptár nem naprakész.", + "tasks_load_failed": "Nem sikerült betölteni a feladatokat. Előfordulhat, hogy a lista nem naprakész.", "retry": "Próbáld újra" }, "quick_create": { diff --git a/locales/it/common.json b/locales/it/common.json index 84cb05800..4c637500c 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -3045,6 +3045,7 @@ "loading_calendars": "Caricamento calendari...", "loading_events": "Caricamento eventi...", "events_load_failed": "Impossibile caricare gli eventi. Il calendario potrebbe non essere aggiornato.", + "tasks_load_failed": "Impossibile caricare le attività. L'elenco potrebbe non essere aggiornato.", "retry": "Riprova" }, "quick_create": { diff --git a/locales/ja/common.json b/locales/ja/common.json index 1ecf7b320..2930237b0 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -3045,6 +3045,7 @@ "loading_calendars": "カレンダーを読み込み中...", "loading_events": "予定を読み込み中...", "events_load_failed": "予定を読み込めませんでした。カレンダーが最新でない可能性があります。", + "tasks_load_failed": "タスクを読み込めませんでした。一覧が最新でない可能性があります。", "retry": "再試行" }, "quick_create": { diff --git a/locales/ko/common.json b/locales/ko/common.json index 4e816ea0f..be73a9872 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -3045,6 +3045,7 @@ "loading_calendars": "캘린더를 불러오는 중...", "loading_events": "일정을 불러오는 중...", "events_load_failed": "일정을 불러오지 못했어요. 캘린더가 최신 상태가 아닐 수 있어요.", + "tasks_load_failed": "할 일을 불러오지 못했어요. 목록이 최신 상태가 아닐 수 있어요.", "retry": "다시 시도" }, "quick_create": { diff --git a/locales/lv/common.json b/locales/lv/common.json index 7c5121a1e..a6b26715b 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -3044,6 +3044,7 @@ "loading_calendars": "Ielādē kalendārus...", "loading_events": "Ielādē pasākumus...", "events_load_failed": "Neizdevās ielādēt pasākumus. Kalendārs var nebūt aktuāls.", + "tasks_load_failed": "Neizdevās ielādēt uzdevumus. Saraksts var nebūt aktuāls.", "retry": "Mēģināt vēlreiz" }, "quick_create": { diff --git a/locales/mn/common.json b/locales/mn/common.json index 33154e0a3..dd8fcfc05 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "Хуанли ачаалж байна...", "loading_events": "Арга хэмжээг ачаалж байна...", "events_load_failed": "Арга хэмжээг ачаалж чадсангүй. Хуанли шинэчлэгдээгүй байж магадгүй.", + "tasks_load_failed": "Даалгавруудыг ачаалж чадсангүй. Жагсаалт шинэчлэгдээгүй байж магадгүй.", "retry": "Дахин оролдоно уу" }, "quick_create": { diff --git a/locales/nb/common.json b/locales/nb/common.json index 26bef2980..7044ae101 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "Laster inn kalendere...", "loading_events": "Laster inn hendelser...", "events_load_failed": "Kunne ikke laste inn hendelser. Kalenderen er kanskje ikke oppdatert.", + "tasks_load_failed": "Kunne ikke laste inn oppgaver. Listen er kanskje ikke oppdatert.", "retry": "Prøv igjen" }, "quick_create": { diff --git a/locales/nl/common.json b/locales/nl/common.json index 533b76bed..4f596d618 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -3045,6 +3045,7 @@ "loading_calendars": "Agenda's laden...", "loading_events": "Evenementen laden...", "events_load_failed": "Evenementen konden niet worden geladen. De agenda is mogelijk niet actueel.", + "tasks_load_failed": "Taken konden niet worden geladen. De lijst is mogelijk niet actueel.", "retry": "Opnieuw proberen" }, "quick_create": { diff --git a/locales/pl/common.json b/locales/pl/common.json index ba195f5df..29eed7c2b 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -3045,6 +3045,7 @@ "loading_calendars": "Ładowanie kalendarzy...", "loading_events": "Ładowanie wydarzeń...", "events_load_failed": "Nie udało się wczytać wydarzeń. Kalendarz może być nieaktualny.", + "tasks_load_failed": "Nie udało się wczytać zadań. Lista może być nieaktualna.", "retry": "Spróbuj ponownie" }, "quick_create": { diff --git a/locales/pt/common.json b/locales/pt/common.json index 0c73acc33..5643626e3 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "Carregando calendários...", "loading_events": "Carregando eventos...", "events_load_failed": "Não foi possível carregar os eventos. O calendário pode estar desatualizado.", + "tasks_load_failed": "Não foi possível carregar as tarefas. A lista pode estar desatualizada.", "retry": "Tentar novamente" }, "quick_create": { diff --git a/locales/ro/common.json b/locales/ro/common.json index 2dc5fe5c9..092e3ed62 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "Se încarcă calendarele...", "loading_events": "Se încarcă evenimente...", "events_load_failed": "Nu s-au putut încărca evenimentele. Este posibil ca calendarul să nu fie actualizat.", + "tasks_load_failed": "Nu s-au putut încărca sarcinile. Este posibil ca lista să nu fie actualizată.", "retry": "Încercați din nou" }, "quick_create": { diff --git a/locales/ru/common.json b/locales/ru/common.json index 7a233801e..f4e5df702 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -3045,6 +3045,7 @@ "loading_calendars": "Загрузка календарей...", "loading_events": "Загрузка событий...", "events_load_failed": "Не удалось загрузить события. Календарь может быть неактуален.", + "tasks_load_failed": "Не удалось загрузить задачи. Список может быть неактуален.", "retry": "Попробовать снова" }, "quick_create": { diff --git a/locales/sk/common.json b/locales/sk/common.json index 31c9c88a6..c7416e7f7 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "Načítavanie kalendárov...", "loading_events": "Načítavanie udalostí...", "events_load_failed": "Udalosti sa nepodarilo načítať. Kalendár nemusí byť aktuálny.", + "tasks_load_failed": "Úlohy sa nepodarilo načítať. Zoznam nemusí byť aktuálny.", "retry": "Skúsiť znova" }, "quick_create": { diff --git a/locales/tr/common.json b/locales/tr/common.json index 4a1e7b9fc..36c45d35e 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "Takvimler yükleniyor...", "loading_events": "Etkinlikler yükleniyor...", "events_load_failed": "Etkinlikler yüklenemedi. Takvim güncel olmayabilir.", + "tasks_load_failed": "Görevler yüklenemedi. Liste güncel olmayabilir.", "retry": "Tekrar dene" }, "quick_create": { diff --git a/locales/uk/common.json b/locales/uk/common.json index 7f264821e..3d5de4209 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -3045,6 +3045,7 @@ "loading_calendars": "Завантаження календарів...", "loading_events": "Завантаження подій...", "events_load_failed": "Не вдалося завантажити події. Календар може бути неактуальним.", + "tasks_load_failed": "Не вдалося завантажити завдання. Список може бути неактуальним.", "retry": "Спробуйте знову" }, "quick_create": { diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index ddbb81b9c..820853d73 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -3071,6 +3071,7 @@ "loading_calendars": "正在載入行事曆…", "loading_events": "正在載入活動…", "events_load_failed": "無法載入活動。行事曆可能不是最新狀態。", + "tasks_load_failed": "無法載入工作。清單可能不是最新狀態。", "retry": "再試一次" }, "quick_create": { diff --git a/locales/zh/common.json b/locales/zh/common.json index 9fb901cd5..9e25f42eb 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -3045,6 +3045,7 @@ "loading_calendars": "正在加载日历...", "loading_events": "正在加载事件...", "events_load_failed": "无法加载事件。日历可能不是最新的。", + "tasks_load_failed": "无法加载任务。列表可能不是最新的。", "retry": "再试一次" }, "quick_create": { diff --git a/stores/task-store.ts b/stores/task-store.ts index b14f9e191..b526d651f 100644 --- a/stores/task-store.ts +++ b/stores/task-store.ts @@ -5,6 +5,12 @@ import { debug } from '@/lib/debug'; export type TaskViewFilter = 'all' | 'pending' | 'completed' | 'overdue'; +/** + * The `error` a failed task load leaves. The tasks loaded before stay; the + * task list shows a notice for it. + */ +export const TASKS_LOAD_ERROR = 'Failed to fetch tasks'; + interface TaskStore { tasks: CalendarTask[]; selectedTaskId: string | null; @@ -52,7 +58,7 @@ export const useTaskStore = create((set, get) => ({ set({ tasks, isLoading: false }); } catch (error) { debug.error('TaskStore/fetchTasks failed', error); - set({ isLoading: false, error: 'Failed to fetch tasks' }); + set({ isLoading: false, error: TASKS_LOAD_ERROR }); } },