From 8d52f641b23d0398e036dbd53b4e679fcf73bf92 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 23 Jan 2026 19:33:25 +0000 Subject: [PATCH] feat(redux): create email state slices package --- redux/email/package.json | 37 ++ redux/email/src/index.ts | 29 ++ redux/email/src/slices/emailComposeSlice.ts | 363 ++++++++++++++++++++ redux/email/src/slices/emailDetailSlice.ts | 205 +++++++++++ redux/email/src/slices/emailFiltersSlice.ts | 197 +++++++++++ redux/email/src/slices/emailListSlice.ts | 224 ++++++++++++ redux/email/src/slices/index.ts | 92 +++++ 7 files changed, 1147 insertions(+) create mode 100644 redux/email/package.json create mode 100644 redux/email/src/index.ts create mode 100644 redux/email/src/slices/emailComposeSlice.ts create mode 100644 redux/email/src/slices/emailDetailSlice.ts create mode 100644 redux/email/src/slices/emailFiltersSlice.ts create mode 100644 redux/email/src/slices/emailListSlice.ts create mode 100644 redux/email/src/slices/index.ts diff --git a/redux/email/package.json b/redux/email/package.json new file mode 100644 index 000000000..5e9b8ef41 --- /dev/null +++ b/redux/email/package.json @@ -0,0 +1,37 @@ +{ + "name": "@metabuilder/redux-email", + "version": "1.0.0", + "description": "Redux slices for email client state management (lists, details, compose, filters)", + "main": "src/index.ts", + "types": "src/index.d.ts", + "scripts": { + "build": "tsc --declaration --emitDeclarationOnly", + "lint": "eslint src/", + "typecheck": "tsc --noEmit" + }, + "keywords": [ + "redux", + "email", + "client", + "state-management", + "slices", + "toolkit" + ], + "author": "MetaBuilder", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-redux": "^8.0.0 || ^9.0.0", + "@reduxjs/toolkit": "^1.9.7 || ^2.5.2" + }, + "devDependencies": { + "@types/react": "^18.2.0 || ^19.0.0", + "@reduxjs/toolkit": "^2.5.2", + "react-redux": "^9.1.2", + "typescript": "^5.0.0" + }, + "files": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/redux/email/src/index.ts b/redux/email/src/index.ts new file mode 100644 index 000000000..cd4cd1032 --- /dev/null +++ b/redux/email/src/index.ts @@ -0,0 +1,29 @@ +/** + * @metabuilder/redux-email + * + * Redux state management package for email client functionality. + * Provides slices for email lists, details, composition, and filtering. + * + * Usage: + * ```typescript + * import { configureStore } from '@reduxjs/toolkit' + * import { + * emailListSlice, + * emailDetailSlice, + * emailComposeSlice, + * emailFiltersSlice + * } from '@metabuilder/redux-email' + * + * const store = configureStore({ + * reducer: { + * emailList: emailListSlice.reducer, + * emailDetail: emailDetailSlice.reducer, + * emailCompose: emailComposeSlice.reducer, + * emailFilters: emailFiltersSlice.reducer, + * } + * }) + * ``` + */ + +// Re-export all slices and their types +export * from './slices' diff --git a/redux/email/src/slices/emailComposeSlice.ts b/redux/email/src/slices/emailComposeSlice.ts new file mode 100644 index 000000000..b354b14de --- /dev/null +++ b/redux/email/src/slices/emailComposeSlice.ts @@ -0,0 +1,363 @@ +/** + * Redux Slice for Email Compose/Draft State Management + * Handles draft creation, editing, and storage + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' + +export interface EmailDraft { + id: string + to: string[] + cc: string[] + bcc: string[] + subject: string + textBody: string + htmlBody?: string + isDraft: boolean + isSending: boolean + createdAt: number + updatedAt: number + inReplyTo?: string + attachmentIds?: string[] +} + +export interface ComposeDraftState { + currentDraft: EmailDraft | null + drafts: EmailDraft[] + isLoading: boolean + isSaving: boolean + error: string | null + successMessage: string | null +} + +const initialState: ComposeDraftState = { + currentDraft: null, + drafts: [], + isLoading: false, + isSaving: false, + error: null, + successMessage: null +} + +/** + * Async thunk to save draft to server + */ +export const saveDraftAsync = createAsyncThunk< + EmailDraft, + EmailDraft, + { rejectValue: string } +>( + 'emailCompose/saveDraftAsync', + async (draft, { rejectWithValue }) => { + try { + const response = await fetch('/api/v1/email/drafts', { + method: draft.id ? 'PUT' : 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(draft) + }) + + if (!response.ok) { + throw new Error('Failed to save draft') + } + + return await response.json() + } catch (error) { + return rejectWithValue( + error instanceof Error ? error.message : 'Unknown error occurred' + ) + } + } +) + +/** + * Async thunk to send email + */ +export const sendEmailAsync = createAsyncThunk< + { success: boolean; messageId: string }, + EmailDraft, + { rejectValue: string } +>( + 'emailCompose/sendEmailAsync', + async (draft, { rejectWithValue }) => { + try { + const response = await fetch('/api/v1/email/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...draft, + isDraft: false, + isSending: true + }) + }) + + if (!response.ok) { + throw new Error('Failed to send email') + } + + return await response.json() + } catch (error) { + return rejectWithValue( + error instanceof Error ? error.message : 'Unknown error occurred' + ) + } + } +) + +/** + * Async thunk to fetch drafts + */ +export const fetchDrafts = createAsyncThunk< + EmailDraft[], + void, + { rejectValue: string } +>( + 'emailCompose/fetchDrafts', + async (_, { rejectWithValue }) => { + try { + const response = await fetch('/api/v1/email/drafts') + if (!response.ok) { + throw new Error('Failed to fetch drafts') + } + return await response.json() + } catch (error) { + return rejectWithValue( + error instanceof Error ? error.message : 'Unknown error occurred' + ) + } + } +) + +export const emailComposeSlice = createSlice({ + name: 'emailCompose', + initialState, + reducers: { + // Create new draft + createDraft: (state) => { + const newDraft: EmailDraft = { + id: `draft_${Date.now()}`, + to: [], + cc: [], + bcc: [], + subject: '', + textBody: '', + isDraft: true, + isSending: false, + createdAt: Date.now(), + updatedAt: Date.now() + } + state.currentDraft = newDraft + state.drafts.push(newDraft) + }, + + // Update draft field + updateDraft: ( + state, + action: PayloadAction<{ + field: keyof EmailDraft + value: unknown + }> + ) => { + if (state.currentDraft) { + const { field, value } = action.payload + ;(state.currentDraft as Record)[field] = value + state.currentDraft.updatedAt = Date.now() + } + }, + + // Update multiple draft fields + updateDraftMultiple: ( + state, + action: PayloadAction> + ) => { + if (state.currentDraft) { + Object.assign(state.currentDraft, action.payload) + state.currentDraft.updatedAt = Date.now() + } + }, + + // Add recipient (to, cc, bcc) + addRecipient: ( + state, + action: PayloadAction<{ + type: 'to' | 'cc' | 'bcc' + email: string + }> + ) => { + if (state.currentDraft) { + const { type, email } = action.payload + if (!state.currentDraft[type].includes(email)) { + state.currentDraft[type].push(email) + state.currentDraft.updatedAt = Date.now() + } + } + }, + + // Remove recipient + removeRecipient: ( + state, + action: PayloadAction<{ + type: 'to' | 'cc' | 'bcc' + email: string + }> + ) => { + if (state.currentDraft) { + const { type, email } = action.payload + state.currentDraft[type] = state.currentDraft[type].filter((e) => e !== email) + state.currentDraft.updatedAt = Date.now() + } + }, + + // Add attachment + addAttachment: (state, action: PayloadAction) => { + if (state.currentDraft) { + if (!state.currentDraft.attachmentIds) { + state.currentDraft.attachmentIds = [] + } + if (!state.currentDraft.attachmentIds.includes(action.payload)) { + state.currentDraft.attachmentIds.push(action.payload) + state.currentDraft.updatedAt = Date.now() + } + } + }, + + // Remove attachment + removeAttachment: (state, action: PayloadAction) => { + if (state.currentDraft) { + state.currentDraft.attachmentIds = ( + state.currentDraft.attachmentIds || [] + ).filter((id) => id !== action.payload) + state.currentDraft.updatedAt = Date.now() + } + }, + + // Set current draft + setCurrentDraft: (state, action: PayloadAction) => { + state.currentDraft = action.payload + }, + + // Clear current draft + clearDraft: (state) => { + state.currentDraft = null + state.error = null + state.successMessage = null + }, + + // Delete draft + deleteDraft: (state, action: PayloadAction) => { + state.drafts = state.drafts.filter((d) => d.id !== action.payload) + if (state.currentDraft?.id === action.payload) { + state.currentDraft = null + } + }, + + // Clear error + clearError: (state) => { + state.error = null + }, + + // Clear success message + clearSuccessMessage: (state) => { + state.successMessage = null + } + }, + + extraReducers: (builder) => { + builder + // Save draft - pending + .addCase(saveDraftAsync.pending, (state) => { + state.isSaving = true + state.error = null + }) + // Save draft - fulfilled + .addCase(saveDraftAsync.fulfilled, (state, action) => { + state.isSaving = false + state.currentDraft = action.payload + state.drafts = state.drafts.map((d) => + d.id === action.payload.id ? action.payload : d + ) + state.successMessage = 'Draft saved successfully' + }) + // Save draft - rejected + .addCase(saveDraftAsync.rejected, (state, action) => { + state.isSaving = false + state.error = action.payload || 'Failed to save draft' + }) + // Send email - pending + .addCase(sendEmailAsync.pending, (state) => { + state.isLoading = true + state.error = null + if (state.currentDraft) { + state.currentDraft.isSending = true + } + }) + // Send email - fulfilled + .addCase(sendEmailAsync.fulfilled, (state) => { + state.isLoading = false + state.currentDraft = null + state.successMessage = 'Email sent successfully' + }) + // Send email - rejected + .addCase(sendEmailAsync.rejected, (state, action) => { + state.isLoading = false + state.error = action.payload || 'Failed to send email' + if (state.currentDraft) { + state.currentDraft.isSending = false + } + }) + // Fetch drafts - pending + .addCase(fetchDrafts.pending, (state) => { + state.isLoading = true + state.error = null + }) + // Fetch drafts - fulfilled + .addCase(fetchDrafts.fulfilled, (state, action) => { + state.isLoading = false + state.drafts = action.payload + }) + // Fetch drafts - rejected + .addCase(fetchDrafts.rejected, (state, action) => { + state.isLoading = false + state.error = action.payload || 'Failed to fetch drafts' + state.drafts = [] + }) + } +}) + +export const { + createDraft, + updateDraft, + updateDraftMultiple, + addRecipient, + removeRecipient, + addAttachment, + removeAttachment, + setCurrentDraft, + clearDraft, + deleteDraft, + clearError, + clearSuccessMessage +} = emailComposeSlice.actions + +// Selectors +export const selectCurrentDraft = (state: { emailCompose: ComposeDraftState }) => + state.emailCompose.currentDraft + +export const selectDrafts = (state: { emailCompose: ComposeDraftState }) => + state.emailCompose.drafts + +export const selectIsLoading = (state: { emailCompose: ComposeDraftState }) => + state.emailCompose.isLoading + +export const selectIsSaving = (state: { emailCompose: ComposeDraftState }) => + state.emailCompose.isSaving + +export const selectError = (state: { emailCompose: ComposeDraftState }) => + state.emailCompose.error + +export const selectSuccessMessage = (state: { emailCompose: ComposeDraftState }) => + state.emailCompose.successMessage + +export const selectDraftCount = (state: { emailCompose: ComposeDraftState }) => + state.emailCompose.drafts.length + +export default emailComposeSlice.reducer diff --git a/redux/email/src/slices/emailDetailSlice.ts b/redux/email/src/slices/emailDetailSlice.ts new file mode 100644 index 000000000..e1741ca7c --- /dev/null +++ b/redux/email/src/slices/emailDetailSlice.ts @@ -0,0 +1,205 @@ +/** + * Redux Slice for Email Detail/Thread View State Management + * Handles selected email details and conversation threads + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' + +export interface EmailDetail { + id: string + from: string + to: string[] + cc?: string[] + bcc?: string[] + subject: string + textBody?: string + htmlBody?: string + receivedAt: number + isRead: boolean + isStarred: boolean + attachments?: Array<{ + id: string + filename: string + mimeType: string + size: number + downloadUrl?: string + }> + conversationId?: string + replyTo?: string +} + +export interface ThreadMessage extends EmailDetail { + isReply: boolean +} + +export interface EmailDetailState { + selectedEmail: EmailDetail | null + threadMessages: ThreadMessage[] + isLoading: boolean + error: string | null +} + +const initialState: EmailDetailState = { + selectedEmail: null, + threadMessages: [], + isLoading: false, + error: null +} + +/** + * Async thunk to fetch email details + */ +export const fetchEmailDetail = createAsyncThunk< + EmailDetail, + string, + { rejectValue: string } +>( + 'emailDetail/fetchEmailDetail', + async (messageId, { rejectWithValue }) => { + try { + const response = await fetch(`/api/v1/email/messages/${messageId}`) + if (!response.ok) { + throw new Error('Failed to fetch email details') + } + return await response.json() + } catch (error) { + return rejectWithValue( + error instanceof Error ? error.message : 'Unknown error occurred' + ) + } + } +) + +/** + * Async thunk to fetch conversation thread + */ +export const fetchConversationThread = createAsyncThunk< + ThreadMessage[], + string, + { rejectValue: string } +>( + 'emailDetail/fetchConversationThread', + async (conversationId, { rejectWithValue }) => { + try { + const response = await fetch(`/api/v1/email/conversations/${conversationId}`) + if (!response.ok) { + throw new Error('Failed to fetch conversation thread') + } + return await response.json() + } catch (error) { + return rejectWithValue( + error instanceof Error ? error.message : 'Unknown error occurred' + ) + } + } +) + +export const emailDetailSlice = createSlice({ + name: 'emailDetail', + initialState, + reducers: { + // Set selected email + setSelectedEmail: (state, action: PayloadAction) => { + state.selectedEmail = action.payload + }, + + // Add message to thread + addToThread: (state, action: PayloadAction) => { + state.threadMessages.push(action.payload) + }, + + // Clear thread + clearThread: (state) => { + state.threadMessages = [] + }, + + // Update email read status + setEmailRead: (state, action: PayloadAction) => { + if (state.selectedEmail) { + state.selectedEmail.isRead = action.payload + } + }, + + // Update email starred status + setEmailStarred: (state, action: PayloadAction) => { + if (state.selectedEmail) { + state.selectedEmail.isStarred = action.payload + } + }, + + // Clear selected email and thread + clearEmailDetail: (state) => { + state.selectedEmail = null + state.threadMessages = [] + }, + + // Error handling + clearError: (state) => { + state.error = null + } + }, + + extraReducers: (builder) => { + builder + // Fetch email detail - pending + .addCase(fetchEmailDetail.pending, (state) => { + state.isLoading = true + state.error = null + }) + // Fetch email detail - fulfilled + .addCase(fetchEmailDetail.fulfilled, (state, action) => { + state.isLoading = false + state.selectedEmail = action.payload + }) + // Fetch email detail - rejected + .addCase(fetchEmailDetail.rejected, (state, action) => { + state.isLoading = false + state.error = action.payload || 'Failed to fetch email details' + state.selectedEmail = null + }) + // Fetch conversation thread - pending + .addCase(fetchConversationThread.pending, (state) => { + state.isLoading = true + state.error = null + }) + // Fetch conversation thread - fulfilled + .addCase(fetchConversationThread.fulfilled, (state, action) => { + state.isLoading = false + state.threadMessages = action.payload + }) + // Fetch conversation thread - rejected + .addCase(fetchConversationThread.rejected, (state, action) => { + state.isLoading = false + state.error = action.payload || 'Failed to fetch conversation thread' + state.threadMessages = [] + }) + } +}) + +export const { + setSelectedEmail, + addToThread, + clearThread, + setEmailRead, + setEmailStarred, + clearEmailDetail, + clearError +} = emailDetailSlice.actions + +// Selectors +export const selectSelectedEmail = (state: { emailDetail: EmailDetailState }) => + state.emailDetail.selectedEmail + +export const selectThreadMessages = (state: { emailDetail: EmailDetailState }) => + state.emailDetail.threadMessages + +export const selectIsLoading = (state: { emailDetail: EmailDetailState }) => + state.emailDetail.isLoading + +export const selectError = (state: { emailDetail: EmailDetailState }) => + state.emailDetail.error + +export const selectConversationThread = (state: { emailDetail: EmailDetailState }) => + state.emailDetail.threadMessages + +export default emailDetailSlice.reducer diff --git a/redux/email/src/slices/emailFiltersSlice.ts b/redux/email/src/slices/emailFiltersSlice.ts new file mode 100644 index 000000000..bb86c3fb7 --- /dev/null +++ b/redux/email/src/slices/emailFiltersSlice.ts @@ -0,0 +1,197 @@ +/** + * Redux Slice for Email Filters State Management + * Handles saved filters and search query management + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +export interface EmailFilter { + id: string + name: string + description?: string + criteria: { + from?: string + to?: string + subject?: string + hasAttachments?: boolean + isRead?: boolean + isStarred?: boolean + dateFrom?: number + dateTo?: number + sizeMin?: number + sizeMax?: number + } + color?: string + createdAt: number + updatedAt: number +} + +export interface SearchQuery { + id: string + text: string + timestamp: number + frequency: number +} + +export interface EmailFilterState { + savedFilters: EmailFilter[] + recentSearches: SearchQuery[] + activeFilterId: string | null + isLoading: boolean + error: string | null +} + +const initialState: EmailFilterState = { + savedFilters: [], + recentSearches: [], + activeFilterId: null, + isLoading: false, + error: null +} + +export const emailFiltersSlice = createSlice({ + name: 'emailFilters', + initialState, + reducers: { + // Add new saved filter + addFilter: (state, action: PayloadAction) => { + const newFilter = { + ...action.payload, + id: action.payload.id || `filter_${Date.now()}`, + createdAt: action.payload.createdAt || Date.now(), + updatedAt: action.payload.updatedAt || Date.now() + } + state.savedFilters.push(newFilter) + }, + + // Update existing filter + updateFilter: (state, action: PayloadAction) => { + const index = state.savedFilters.findIndex((f) => f.id === action.payload.id) + if (index !== -1) { + state.savedFilters[index] = { + ...action.payload, + updatedAt: Date.now() + } + } + }, + + // Delete filter + removeFilter: (state, action: PayloadAction) => { + state.savedFilters = state.savedFilters.filter((f) => f.id !== action.payload) + if (state.activeFilterId === action.payload) { + state.activeFilterId = null + } + }, + + // Delete all filters + clearAllFilters: (state) => { + state.savedFilters = [] + state.activeFilterId = null + }, + + // Set active filter + setActiveFilter: (state, action: PayloadAction) => { + state.activeFilterId = action.payload + }, + + // Add to recent searches + addSearchQuery: (state, action: PayloadAction) => { + const existing = state.recentSearches.find((s) => s.text === action.payload) + + if (existing) { + // Update frequency if search already exists + existing.frequency += 1 + existing.timestamp = Date.now() + } else { + // Add new search to beginning of list + state.recentSearches.unshift({ + id: `search_${Date.now()}`, + text: action.payload, + timestamp: Date.now(), + frequency: 1 + }) + + // Keep only last 20 searches + if (state.recentSearches.length > 20) { + state.recentSearches.pop() + } + } + }, + + // Remove search from history + removeSearchQuery: (state, action: PayloadAction) => { + state.recentSearches = state.recentSearches.filter((s) => s.id !== action.payload) + }, + + // Clear search history + clearSearchHistory: (state) => { + state.recentSearches = [] + }, + + // Reset to initial state + resetFilters: (state) => { + state.savedFilters = [] + state.activeFilterId = null + state.recentSearches = [] + }, + + // Error handling + setError: (state, action: PayloadAction) => { + state.error = action.payload + }, + + clearError: (state) => { + state.error = null + } + } +}) + +export const { + addFilter, + updateFilter, + removeFilter, + clearAllFilters, + setActiveFilter, + addSearchQuery, + removeSearchQuery, + clearSearchHistory, + resetFilters, + setError, + clearError +} = emailFiltersSlice.actions + +// Selectors +export const selectSavedFilters = (state: { emailFilters: EmailFilterState }) => + state.emailFilters.savedFilters + +export const selectFilterById = (state: { emailFilters: EmailFilterState }, id: string) => + state.emailFilters.savedFilters.find((f) => f.id === id) + +export const selectActiveFilterId = (state: { emailFilters: EmailFilterState }) => + state.emailFilters.activeFilterId + +export const selectActiveFilter = (state: { emailFilters: EmailFilterState }) => { + if (!state.emailFilters.activeFilterId) return null + return state.emailFilters.savedFilters.find( + (f) => f.id === state.emailFilters.activeFilterId + ) || null +} + +export const selectRecentSearches = (state: { emailFilters: EmailFilterState }) => + state.emailFilters.recentSearches + +export const selectTopSearches = (state: { emailFilters: EmailFilterState }, limit: number = 5) => + state.emailFilters.recentSearches + .sort((a, b) => b.frequency - a.frequency) + .slice(0, limit) + +export const selectFilterCount = (state: { emailFilters: EmailFilterState }) => + state.emailFilters.savedFilters.length + +export const selectSearchHistoryCount = (state: { emailFilters: EmailFilterState }) => + state.emailFilters.recentSearches.length + +export const selectError = (state: { emailFilters: EmailFilterState }) => + state.emailFilters.error + +export default emailFiltersSlice.reducer diff --git a/redux/email/src/slices/emailListSlice.ts b/redux/email/src/slices/emailListSlice.ts new file mode 100644 index 000000000..2dd79e70d --- /dev/null +++ b/redux/email/src/slices/emailListSlice.ts @@ -0,0 +1,224 @@ +/** + * Redux Slice for Email List State Management + * Handles message list display, selection, filtering, and pagination + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' + +export interface EmailMessage { + id: string + from: string + subject: string + preview: string + receivedAt: number + isRead: boolean + isStarred: boolean + attachmentCount: number +} + +export interface PaginationState { + currentPage: number + pageSize: number + totalMessages: number +} + +export interface EmailListState { + messages: EmailMessage[] + selectedMessageId: string | null + filter: 'all' | 'unread' | 'starred' | 'spam' | 'custom' + searchQuery: string + pagination: PaginationState + isLoading: boolean + error: string | null +} + +const initialState: EmailListState = { + messages: [], + selectedMessageId: null, + filter: 'all', + searchQuery: '', + pagination: { + currentPage: 1, + pageSize: 20, + totalMessages: 0 + }, + isLoading: false, + error: null +} + +/** + * Async thunk to fetch messages from API + * Handles pagination and filtering + */ +export const fetchMessages = createAsyncThunk< + { messages: EmailMessage[]; total: number }, + { + folderId: string + page?: number + pageSize?: number + filter?: string + search?: string + }, + { rejectValue: string } +>( + 'emailList/fetchMessages', + async (params, { rejectWithValue }) => { + try { + const query = new URLSearchParams({ + folderId: params.folderId, + page: (params.page || 1).toString(), + pageSize: (params.pageSize || 20).toString(), + ...(params.filter && { filter: params.filter }), + ...(params.search && { search: params.search }) + }) + + const response = await fetch(`/api/v1/email/messages?${query}`) + if (!response.ok) { + throw new Error('Failed to fetch messages') + } + + return await response.json() + } catch (error) { + return rejectWithValue( + error instanceof Error ? error.message : 'Unknown error occurred' + ) + } + } +) + +export const emailListSlice = createSlice({ + name: 'emailList', + initialState, + reducers: { + // Message selection + setSelectedMessage: (state, action: PayloadAction) => { + state.selectedMessageId = action.payload + }, + + // Filter management + setFilter: (state, action: PayloadAction<'all' | 'unread' | 'starred' | 'spam' | 'custom'>) => { + state.filter = action.payload + state.pagination.currentPage = 1 + }, + + // Search functionality + setSearchQuery: (state, action: PayloadAction) => { + state.searchQuery = action.payload + state.pagination.currentPage = 1 + }, + + // Clear search + clearSearchQuery: (state) => { + state.searchQuery = '' + state.pagination.currentPage = 1 + }, + + // Pagination + setCurrentPage: (state, action: PayloadAction) => { + state.pagination.currentPage = action.payload + }, + + setPageSize: (state, action: PayloadAction) => { + state.pagination.pageSize = action.payload + state.pagination.currentPage = 1 + }, + + // Message flag updates + toggleMessageRead: (state, action: PayloadAction) => { + const message = state.messages.find((m) => m.id === action.payload) + if (message) { + message.isRead = !message.isRead + } + }, + + toggleMessageStarred: (state, action: PayloadAction) => { + const message = state.messages.find((m) => m.id === action.payload) + if (message) { + message.isStarred = !message.isStarred + } + }, + + // Remove message from list + removeMessage: (state, action: PayloadAction) => { + state.messages = state.messages.filter((m) => m.id !== action.payload) + if (state.selectedMessageId === action.payload) { + state.selectedMessageId = null + } + }, + + // Clear all messages + clearMessages: (state) => { + state.messages = [] + state.selectedMessageId = null + }, + + // Error handling + clearError: (state) => { + state.error = null + } + }, + + extraReducers: (builder) => { + builder + // Fetch messages - pending + .addCase(fetchMessages.pending, (state) => { + state.isLoading = true + state.error = null + }) + // Fetch messages - fulfilled + .addCase(fetchMessages.fulfilled, (state, action) => { + state.isLoading = false + state.messages = action.payload.messages + state.pagination.totalMessages = action.payload.total + }) + // Fetch messages - rejected + .addCase(fetchMessages.rejected, (state, action) => { + state.isLoading = false + state.error = action.payload || 'Failed to fetch messages' + state.messages = [] + }) + } +}) + +export const { + setSelectedMessage, + setFilter, + setSearchQuery, + clearSearchQuery, + setCurrentPage, + setPageSize, + toggleMessageRead, + toggleMessageStarred, + removeMessage, + clearMessages, + clearError +} = emailListSlice.actions + +// Selectors +export const selectMessages = (state: { emailList: EmailListState }) => + state.emailList.messages + +export const selectSelectedMessageId = (state: { emailList: EmailListState }) => + state.emailList.selectedMessageId + +export const selectFilter = (state: { emailList: EmailListState }) => + state.emailList.filter + +export const selectSearchQuery = (state: { emailList: EmailListState }) => + state.emailList.searchQuery + +export const selectPagination = (state: { emailList: EmailListState }) => + state.emailList.pagination + +export const selectIsLoading = (state: { emailList: EmailListState }) => + state.emailList.isLoading + +export const selectError = (state: { emailList: EmailListState }) => + state.emailList.error + +export const selectSelectedMessage = (state: { emailList: EmailListState }) => { + if (!state.emailList.selectedMessageId) return null + return state.emailList.messages.find((m) => m.id === state.emailList.selectedMessageId) || null +} + +export default emailListSlice.reducer diff --git a/redux/email/src/slices/index.ts b/redux/email/src/slices/index.ts new file mode 100644 index 000000000..5fac91fdc --- /dev/null +++ b/redux/email/src/slices/index.ts @@ -0,0 +1,92 @@ +/** + * Email Redux Slices - Barrel Export + * Exports all email slices, actions, and selectors + */ + +// Email List Slice +export { + emailListSlice, + fetchMessages, + setSelectedMessage, + setFilter, + setSearchQuery, + clearSearchQuery, + setCurrentPage, + setPageSize, + toggleMessageRead, + toggleMessageStarred, + removeMessage, + clearMessages, + selectMessages, + selectSelectedMessageId, + selectFilter, + selectSearchQuery, + selectPagination, + selectIsLoading, + selectError, + selectSelectedMessage +} from './emailListSlice' +export type { EmailListState, EmailMessage, PaginationState } from './emailListSlice' + +// Email Detail Slice +export { + emailDetailSlice, + fetchEmailDetail, + fetchConversationThread, + setSelectedEmail, + addToThread, + clearThread, + setEmailRead, + setEmailStarred, + clearEmailDetail, + selectSelectedEmail, + selectThreadMessages, + selectConversationThread +} from './emailDetailSlice' +export type { EmailDetailState, EmailDetail, ThreadMessage } from './emailDetailSlice' + +// Email Compose Slice +export { + emailComposeSlice, + saveDraftAsync, + sendEmailAsync, + fetchDrafts, + createDraft, + updateDraft, + updateDraftMultiple, + addRecipient, + removeRecipient, + addAttachment, + removeAttachment, + setCurrentDraft, + clearDraft, + deleteDraft, + selectCurrentDraft, + selectDrafts, + selectDraftCount, + selectSuccessMessage +} from './emailComposeSlice' +export type { ComposeDraftState, EmailDraft } from './emailComposeSlice' + +// Email Filters Slice +export { + emailFiltersSlice, + addFilter, + updateFilter, + removeFilter, + clearAllFilters, + setActiveFilter, + addSearchQuery, + removeSearchQuery, + clearSearchHistory, + resetFilters, + selectSavedFilters, + selectFilterById, + selectActiveFilterId, + selectActiveFilter, + selectRecentSearches, + selectTopSearches, + selectFilterCount, + selectSearchHistoryCount +} from './emailFiltersSlice' +export type { EmailFilterState, EmailFilter, SearchQuery } from './emailFiltersSlice'