Phase 3 (security): - P3.1: Feature gate server-side enforcement (403 on disabled features) - P3.2: Unified auth error interceptor (401→logout) - P3.3: Store-level state isolation via StoreSnapshot contract (added message-list-tabs + task stores to snapshot/restore cycle) - P3.4: Push event bus extraction — email-store no longer imports calendar/contact/filter/file stores directly - P1.3: Auth localStorage AES-GCM encryption via custom Zustand adapter Phase 4 (polish): - P4.1: Offline write queue — pending operations in localStorage, auto-retry on reconnect, offline-queue-indicator banner - P4.2: Identity spoofing — fromOverrideEmail domain validation - P4.3: WebSocket push for Electron via main-process IPC bridge (ws package with Authorization headers)
137 lines
5.0 KiB
TypeScript
137 lines
5.0 KiB
TypeScript
import { create } from 'zustand';
|
|
import type { CalendarTask } from '@/lib/jmap/types';
|
|
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
|
import { debug } from '@/lib/debug';
|
|
import { enqueueOperation, isNetworkError } from '@/lib/offline-write-queue';
|
|
import { useAccountStore } from '@/stores/account-store';
|
|
|
|
export type TaskViewFilter = 'all' | 'pending' | 'completed' | 'overdue';
|
|
|
|
interface TaskStore {
|
|
tasks: CalendarTask[];
|
|
selectedTaskId: string | null;
|
|
filter: TaskViewFilter;
|
|
showCompleted: boolean;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
setTasks: (tasks: CalendarTask[]) => void;
|
|
setSelectedTaskId: (id: string | null) => void;
|
|
setFilter: (filter: TaskViewFilter) => void;
|
|
setShowCompleted: (show: boolean) => void;
|
|
fetchTasks: (client: IJMAPClient, calendarIds?: string[]) => Promise<void>;
|
|
createTask: (client: IJMAPClient, task: Partial<CalendarTask>) => Promise<CalendarTask>;
|
|
updateTask: (client: IJMAPClient, id: string, updates: Partial<CalendarTask>) => Promise<void>;
|
|
deleteTask: (client: IJMAPClient, id: string) => Promise<void>;
|
|
toggleTaskComplete: (client: IJMAPClient, task: CalendarTask) => Promise<void>;
|
|
clearTasks: () => void;
|
|
}
|
|
|
|
export const useTaskStore = create<TaskStore>((set, get) => ({
|
|
tasks: [],
|
|
selectedTaskId: null,
|
|
filter: 'all',
|
|
showCompleted: false,
|
|
isLoading: false,
|
|
error: null,
|
|
setTasks: (tasks) => set({ tasks }),
|
|
setSelectedTaskId: (id) => set({ selectedTaskId: id }),
|
|
setFilter: (filter) => set({ filter }),
|
|
setShowCompleted: (show) => set({ showCompleted: show }),
|
|
|
|
fetchTasks: async (client, calendarIds) => {
|
|
debug.log('tasks', 'TaskStore/fetchTasks start', { calendarIds: calendarIds || 'all' });
|
|
set({ isLoading: true, error: null });
|
|
try {
|
|
const tasks = await client.getCalendarTasks(calendarIds);
|
|
debug.log('tasks', 'TaskStore/fetchTasks received', tasks.length, 'tasks');
|
|
tasks.forEach((t, i) => {
|
|
debug.log('tasks', `TaskStore/fetchTasks [${i}]`, {
|
|
id: t.id, uid: t.uid, '@type': t['@type'],
|
|
title: t.title, due: t.due, progress: t.progress,
|
|
showWithoutTime: t.showWithoutTime, calendarIds: t.calendarIds,
|
|
});
|
|
});
|
|
set({ tasks, isLoading: false });
|
|
} catch (error) {
|
|
debug.error('TaskStore/fetchTasks failed', error);
|
|
set({ isLoading: false, error: 'Failed to fetch tasks' });
|
|
}
|
|
},
|
|
|
|
createTask: async (client, task) => {
|
|
debug.log('tasks', 'TaskStore/createTask', task);
|
|
try {
|
|
const created = await client.createCalendarTask(task);
|
|
debug.log('tasks', 'TaskStore/createTask result', { id: created.id, uid: created.uid, title: created.title });
|
|
set({ tasks: [...get().tasks, created] });
|
|
return created;
|
|
} catch (error) {
|
|
debug.error('TaskStore/createTask failed', error);
|
|
if (isNetworkError(error)) {
|
|
const accountId = useAccountStore.getState().activeAccountId;
|
|
if (accountId) {
|
|
enqueueOperation({ type: 'createTask', accountId, payload: task });
|
|
}
|
|
}
|
|
set({ error: 'Failed to create task' });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
updateTask: async (client, id, updates) => {
|
|
try {
|
|
await client.updateCalendarTask(id, updates);
|
|
set({
|
|
tasks: get().tasks.map(t => t.id === id ? { ...t, ...updates, updated: new Date().toISOString() } : t),
|
|
});
|
|
} catch (error) {
|
|
debug.error('TaskStore/updateTask failed', error);
|
|
if (isNetworkError(error)) {
|
|
const accountId = useAccountStore.getState().activeAccountId;
|
|
if (accountId) {
|
|
enqueueOperation({ type: 'updateTask', accountId, payload: { id, updates } });
|
|
}
|
|
}
|
|
set({ error: 'Failed to update task' });
|
|
}
|
|
},
|
|
|
|
deleteTask: async (client, id) => {
|
|
try {
|
|
await client.deleteCalendarTask(id);
|
|
set({
|
|
tasks: get().tasks.filter(t => t.id !== id),
|
|
selectedTaskId: get().selectedTaskId === id ? null : get().selectedTaskId,
|
|
});
|
|
} catch (error) {
|
|
debug.error('TaskStore/deleteTask failed', error);
|
|
if (isNetworkError(error)) {
|
|
const accountId = useAccountStore.getState().activeAccountId;
|
|
if (accountId) {
|
|
enqueueOperation({ type: 'deleteTask', accountId, payload: { id } });
|
|
}
|
|
}
|
|
set({ error: 'Failed to delete task' });
|
|
}
|
|
},
|
|
|
|
toggleTaskComplete: async (client, task) => {
|
|
try {
|
|
const newProgress = task.progress === 'completed' ? 'needs-action' : 'completed';
|
|
const updates: Partial<CalendarTask> = {
|
|
progress: newProgress,
|
|
progressUpdated: new Date().toISOString(),
|
|
};
|
|
await client.updateCalendarTask(task.id, updates);
|
|
set({
|
|
tasks: get().tasks.map(t => t.id === task.id ? { ...t, ...updates, updated: new Date().toISOString() } : t),
|
|
});
|
|
} catch (error) {
|
|
debug.error('TaskStore/toggleTaskComplete failed', error);
|
|
set({ error: 'Failed to update task' });
|
|
}
|
|
},
|
|
|
|
clearTasks: () => set({ tasks: [], selectedTaskId: null, error: null }),
|
|
}));
|