feat(redux): create email state slices package

This commit is contained in:
2026-01-23 19:33:25 +00:00
parent 6d86b37d3b
commit 8d52f641b2
7 changed files with 1147 additions and 0 deletions

37
redux/email/package.json Normal file
View File

@@ -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"
]
}

29
redux/email/src/index.ts Normal file
View File

@@ -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'

View File

@@ -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<string, unknown>)[field] = value
state.currentDraft.updatedAt = Date.now()
}
},
// Update multiple draft fields
updateDraftMultiple: (
state,
action: PayloadAction<Partial<EmailDraft>>
) => {
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<string>) => {
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<string>) => {
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<EmailDraft | null>) => {
state.currentDraft = action.payload
},
// Clear current draft
clearDraft: (state) => {
state.currentDraft = null
state.error = null
state.successMessage = null
},
// Delete draft
deleteDraft: (state, action: PayloadAction<string>) => {
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

View File

@@ -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<EmailDetail | null>) => {
state.selectedEmail = action.payload
},
// Add message to thread
addToThread: (state, action: PayloadAction<ThreadMessage>) => {
state.threadMessages.push(action.payload)
},
// Clear thread
clearThread: (state) => {
state.threadMessages = []
},
// Update email read status
setEmailRead: (state, action: PayloadAction<boolean>) => {
if (state.selectedEmail) {
state.selectedEmail.isRead = action.payload
}
},
// Update email starred status
setEmailStarred: (state, action: PayloadAction<boolean>) => {
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

View File

@@ -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<EmailFilter>) => {
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<EmailFilter>) => {
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<string>) => {
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<string | null>) => {
state.activeFilterId = action.payload
},
// Add to recent searches
addSearchQuery: (state, action: PayloadAction<string>) => {
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<string>) => {
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<string | null>) => {
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

View File

@@ -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<string | null>) => {
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<string>) => {
state.searchQuery = action.payload
state.pagination.currentPage = 1
},
// Clear search
clearSearchQuery: (state) => {
state.searchQuery = ''
state.pagination.currentPage = 1
},
// Pagination
setCurrentPage: (state, action: PayloadAction<number>) => {
state.pagination.currentPage = action.payload
},
setPageSize: (state, action: PayloadAction<number>) => {
state.pagination.pageSize = action.payload
state.pagination.currentPage = 1
},
// Message flag updates
toggleMessageRead: (state, action: PayloadAction<string>) => {
const message = state.messages.find((m) => m.id === action.payload)
if (message) {
message.isRead = !message.isRead
}
},
toggleMessageStarred: (state, action: PayloadAction<string>) => {
const message = state.messages.find((m) => m.id === action.payload)
if (message) {
message.isStarred = !message.isStarred
}
},
// Remove message from list
removeMessage: (state, action: PayloadAction<string>) => {
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

View File

@@ -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'