feat: add contacts phase 2, advanced search, vacation responder, Docker & TOTP 2FA
- Contact groups/lists, vCard import/export (RFC 6350), bulk operations - Advanced search with JMAP filter panel, search chips, cross-mailbox queries - Vacation responder with JMAP VacationResponse, settings tab, sidebar indicator - TOTP two-factor authentication support - Docker multi-stage build with standalone output and docker-compose - CSP Report-Only headers and security headers via proxy middleware - Virtual scrolling for large email lists - Structured server-side logger (text/JSON, configurable level) - 450+ tests (contacts, vCard, threads, headers, identity, components) - Playwright E2E framework setup - Updated README and ROADMAP with all new features
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { create } from 'zustand';
|
||||
import type { JMAPClient } from '@/lib/jmap/client';
|
||||
|
||||
interface VacationStore {
|
||||
isEnabled: boolean;
|
||||
fromDate: string | null;
|
||||
toDate: string | null;
|
||||
subject: string;
|
||||
textBody: string;
|
||||
htmlBody: string | null;
|
||||
isLoading: boolean;
|
||||
isSaving: boolean;
|
||||
error: string | null;
|
||||
isSupported: boolean;
|
||||
|
||||
fetchVacationResponse: (client: JMAPClient) => Promise<void>;
|
||||
updateVacationResponse: (client: JMAPClient, updates: {
|
||||
isEnabled?: boolean;
|
||||
fromDate?: string | null;
|
||||
toDate?: string | null;
|
||||
subject?: string;
|
||||
textBody?: string;
|
||||
htmlBody?: string | null;
|
||||
}) => Promise<void>;
|
||||
setSupported: (supported: boolean) => void;
|
||||
clearState: () => void;
|
||||
}
|
||||
|
||||
export const useVacationStore = create<VacationStore>()((set) => ({
|
||||
isEnabled: false,
|
||||
fromDate: null,
|
||||
toDate: null,
|
||||
subject: '',
|
||||
textBody: '',
|
||||
htmlBody: null,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
error: null,
|
||||
isSupported: false,
|
||||
|
||||
fetchVacationResponse: async (client) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const vacation = await client.getVacationResponse();
|
||||
set({
|
||||
isEnabled: vacation.isEnabled,
|
||||
fromDate: vacation.fromDate,
|
||||
toDate: vacation.toDate,
|
||||
subject: vacation.subject || '',
|
||||
textBody: vacation.textBody || '',
|
||||
htmlBody: vacation.htmlBody,
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'fetch_error',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateVacationResponse: async (client, updates) => {
|
||||
set({ isSaving: true, error: null });
|
||||
try {
|
||||
await client.setVacationResponse(updates);
|
||||
set((state) => ({
|
||||
...state,
|
||||
...updates,
|
||||
isSaving: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
set({
|
||||
isSaving: false,
|
||||
error: error instanceof Error ? error.message : 'save_error',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
setSupported: (supported) => set({ isSupported: supported }),
|
||||
|
||||
clearState: () => set({
|
||||
isEnabled: false,
|
||||
fromDate: null,
|
||||
toDate: null,
|
||||
subject: '',
|
||||
textBody: '',
|
||||
htmlBody: null,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
error: null,
|
||||
isSupported: false,
|
||||
}),
|
||||
}));
|
||||
Reference in New Issue
Block a user