mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
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"
This commit is contained in:
@@ -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(<CalendarLoadErrorNotice what="tasks" onRetry={vi.fn()} />);
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('tasks_load_failed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col h-full">
|
||||
{taskStore.error === TASKS_LOAD_ERROR && (
|
||||
<CalendarLoadErrorNotice what="tasks" onRetry={() => { if (client) void fetchTasksFn(client); }} />
|
||||
)}
|
||||
<TaskToolbar
|
||||
filter={taskStore.filter}
|
||||
showCompleted={taskStore.showCompleted}
|
||||
|
||||
@@ -5,19 +5,21 @@ import { AlertTriangle } from "@/components/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface CalendarLoadErrorNoticeProps {
|
||||
/** What failed to load: the visible range of events, or the task list. */
|
||||
what?: "events" | "tasks";
|
||||
onRetry: () => 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 (
|
||||
<div role="alert" className="flex items-center gap-2 border-b border-border bg-warning/10 px-4 py-2 text-sm">
|
||||
<AlertTriangle className="h-4 w-4 flex-shrink-0 text-warning" />
|
||||
<span className="min-w-0 flex-1">{t("events_load_failed")}</span>
|
||||
<span className="min-w-0 flex-1">{what === "tasks" ? t("tasks_load_failed") : t("events_load_failed")}</span>
|
||||
<Button variant="outline" size="sm" onClick={onRetry}>
|
||||
{t("retry")}
|
||||
</Button>
|
||||
|
||||
@@ -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<typeof import('../settings-section')>()),
|
||||
SettingsSection: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
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<typeof vi.fn>;
|
||||
|
||||
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(<CalendarManagementSettings />);
|
||||
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] }]);
|
||||
});
|
||||
});
|
||||
@@ -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() {
|
||||
<CalendarEditForm
|
||||
key={cal.id}
|
||||
initial={{ name: cal.name, color }}
|
||||
onSave={(data) => handleUpdate(cal.id, data)}
|
||||
onSave={(data) => handleUpdate(cal.id, data, color)}
|
||||
onCancel={() => setEditingId(null)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
+13
-5
@@ -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) {
|
||||
|
||||
@@ -3071,6 +3071,7 @@
|
||||
"loading_calendars": "جارٍ تحميل التقويمات...",
|
||||
"loading_events": "جارٍ تحميل الأحداث...",
|
||||
"events_load_failed": "تعذّر تحميل الأحداث. قد لا يكون التقويم محدَّثًا.",
|
||||
"tasks_load_failed": "تعذّر تحميل المهام. قد لا تكون القائمة محدَّثة.",
|
||||
"retry": "حاول مرة أخرى"
|
||||
},
|
||||
"quick_create": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -3071,6 +3071,7 @@
|
||||
"loading_calendars": "در حال بارگذاری تقویمها...",
|
||||
"loading_events": "در حال بارگذاری رویدادها...",
|
||||
"events_load_failed": "بارگذاری رویدادها ممکن نشد. ممکن است تقویم بهروز نباشد.",
|
||||
"tasks_load_failed": "بارگذاری وظایف ممکن نشد. ممکن است فهرست بهروز نباشد.",
|
||||
"retry": "دوباره تلاش کنید"
|
||||
},
|
||||
"quick_create": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -2969,6 +2969,7 @@
|
||||
"loading_calendars": "טוען יומנים...",
|
||||
"loading_events": "טוען אירועים...",
|
||||
"events_load_failed": "לא ניתן לטעון את האירועים. ייתכן שהיומן אינו מעודכן.",
|
||||
"tasks_load_failed": "לא ניתן לטעון את המשימות. ייתכן שהרשימה אינה מעודכנת.",
|
||||
"retry": "נסה שוב"
|
||||
},
|
||||
"quick_create": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -3045,6 +3045,7 @@
|
||||
"loading_calendars": "カレンダーを読み込み中...",
|
||||
"loading_events": "予定を読み込み中...",
|
||||
"events_load_failed": "予定を読み込めませんでした。カレンダーが最新でない可能性があります。",
|
||||
"tasks_load_failed": "タスクを読み込めませんでした。一覧が最新でない可能性があります。",
|
||||
"retry": "再試行"
|
||||
},
|
||||
"quick_create": {
|
||||
|
||||
@@ -3045,6 +3045,7 @@
|
||||
"loading_calendars": "캘린더를 불러오는 중...",
|
||||
"loading_events": "일정을 불러오는 중...",
|
||||
"events_load_failed": "일정을 불러오지 못했어요. 캘린더가 최신 상태가 아닐 수 있어요.",
|
||||
"tasks_load_failed": "할 일을 불러오지 못했어요. 목록이 최신 상태가 아닐 수 있어요.",
|
||||
"retry": "다시 시도"
|
||||
},
|
||||
"quick_create": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -3071,6 +3071,7 @@
|
||||
"loading_calendars": "Хуанли ачаалж байна...",
|
||||
"loading_events": "Арга хэмжээг ачаалж байна...",
|
||||
"events_load_failed": "Арга хэмжээг ачаалж чадсангүй. Хуанли шинэчлэгдээгүй байж магадгүй.",
|
||||
"tasks_load_failed": "Даалгавруудыг ачаалж чадсангүй. Жагсаалт шинэчлэгдээгүй байж магадгүй.",
|
||||
"retry": "Дахин оролдоно уу"
|
||||
},
|
||||
"quick_create": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -3045,6 +3045,7 @@
|
||||
"loading_calendars": "Загрузка календарей...",
|
||||
"loading_events": "Загрузка событий...",
|
||||
"events_load_failed": "Не удалось загрузить события. Календарь может быть неактуален.",
|
||||
"tasks_load_failed": "Не удалось загрузить задачи. Список может быть неактуален.",
|
||||
"retry": "Попробовать снова"
|
||||
},
|
||||
"quick_create": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -3045,6 +3045,7 @@
|
||||
"loading_calendars": "Завантаження календарів...",
|
||||
"loading_events": "Завантаження подій...",
|
||||
"events_load_failed": "Не вдалося завантажити події. Календар може бути неактуальним.",
|
||||
"tasks_load_failed": "Не вдалося завантажити завдання. Список може бути неактуальним.",
|
||||
"retry": "Спробуйте знову"
|
||||
},
|
||||
"quick_create": {
|
||||
|
||||
@@ -3071,6 +3071,7 @@
|
||||
"loading_calendars": "正在載入行事曆…",
|
||||
"loading_events": "正在載入活動…",
|
||||
"events_load_failed": "無法載入活動。行事曆可能不是最新狀態。",
|
||||
"tasks_load_failed": "無法載入工作。清單可能不是最新狀態。",
|
||||
"retry": "再試一次"
|
||||
},
|
||||
"quick_create": {
|
||||
|
||||
@@ -3045,6 +3045,7 @@
|
||||
"loading_calendars": "正在加载日历...",
|
||||
"loading_events": "正在加载事件...",
|
||||
"events_load_failed": "无法加载事件。日历可能不是最新的。",
|
||||
"tasks_load_failed": "无法加载任务。列表可能不是最新的。",
|
||||
"retry": "再试一次"
|
||||
},
|
||||
"quick_create": {
|
||||
|
||||
@@ -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<TaskStore>((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 });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user