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 });
}
},