mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 00:03:23 +00:00
Remove email outbox collection and process job; refactor email templates with rich text support and slug generation
This commit is contained in:
208
dev/README.md
Normal file
208
dev/README.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# PayloadCMS Mailing Plugin - Development Setup
|
||||
|
||||
This directory contains a complete PayloadCMS application for testing and developing the mailing plugin.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Start development server (with in-memory MongoDB):**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**Alternative startup methods:**
|
||||
```bash
|
||||
# Force in-memory database (from root directory)
|
||||
npm run dev:memory
|
||||
|
||||
# With startup script and helpful info
|
||||
npm run dev:start
|
||||
|
||||
# From dev directory
|
||||
cd dev && npm run dev
|
||||
```
|
||||
|
||||
3. **Optional: Set up environment file:**
|
||||
```bash
|
||||
cp ../.env.example .env
|
||||
# Edit .env if you want to use external MongoDB
|
||||
```
|
||||
|
||||
4. **Access the application:**
|
||||
- Admin Panel: http://localhost:3000/admin
|
||||
- Mailing Test Page: http://localhost:3000/mailing-test
|
||||
- GraphQL Playground: http://localhost:3000/api/graphql-playground
|
||||
|
||||
## Features Included
|
||||
|
||||
### ✅ **Mailing Plugin Integration**
|
||||
- Configured with test email transport
|
||||
- Example email templates automatically created
|
||||
- Collections: `email-templates` and `email-outbox`
|
||||
|
||||
### ✅ **Test Interface**
|
||||
- Web UI at `/mailing-test` for testing emails
|
||||
- Send emails immediately or schedule for later
|
||||
- View outbox status and email history
|
||||
- Process outbox manually
|
||||
|
||||
### ✅ **API Endpoints**
|
||||
- `POST /api/test-email` - Send/schedule test emails
|
||||
- `GET /api/test-email` - Get templates and outbox status
|
||||
- `POST /api/process-outbox` - Manually process outbox
|
||||
- `GET /api/process-outbox` - Get outbox statistics
|
||||
|
||||
### ✅ **Example Templates**
|
||||
1. **Welcome Email** - New user onboarding
|
||||
2. **Order Confirmation** - E-commerce order receipt
|
||||
3. **Password Reset** - Security password reset
|
||||
|
||||
## Email Testing
|
||||
|
||||
### Option 1: MailHog (Recommended)
|
||||
```bash
|
||||
# Install MailHog
|
||||
go install github.com/mailhog/MailHog@latest
|
||||
|
||||
# Run MailHog
|
||||
MailHog
|
||||
```
|
||||
- SMTP: localhost:1025 (configured in dev)
|
||||
- Web UI: http://localhost:8025
|
||||
|
||||
### Option 2: Console Logs
|
||||
The dev environment uses `testEmailAdapter` which logs emails to console.
|
||||
|
||||
## Testing the Plugin
|
||||
|
||||
1. **Via Web Interface:**
|
||||
- Go to http://localhost:3000/mailing-test
|
||||
- Select a template
|
||||
- Fill in variables
|
||||
- Send or schedule email
|
||||
|
||||
2. **Via Admin Panel:**
|
||||
- Go to http://localhost:3000/admin
|
||||
- Navigate to "Mailing > Email Templates"
|
||||
- Create/edit templates
|
||||
- Navigate to "Mailing > Email Outbox"
|
||||
- View scheduled/sent emails
|
||||
|
||||
3. **Via API:**
|
||||
```bash
|
||||
# Send welcome email
|
||||
curl -X POST http://localhost:3000/api/test-email \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"type": "send",
|
||||
"templateId": "TEMPLATE_ID",
|
||||
"to": "test@example.com",
|
||||
"variables": {
|
||||
"firstName": "John",
|
||||
"siteName": "Test Site"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Make changes to plugin source** (`../src/`)
|
||||
2. **Rebuild plugin:** `npm run build` (in root)
|
||||
3. **Restart dev server:** The dev server watches for changes
|
||||
4. **Test changes** via web interface or API
|
||||
|
||||
## Database Configuration
|
||||
|
||||
The development setup automatically uses **MongoDB in-memory database** by default - no MongoDB installation required!
|
||||
|
||||
### 🚀 **In-Memory MongoDB (Default)**
|
||||
- ✅ **Zero setup** - Works out of the box
|
||||
- ✅ **No installation** required
|
||||
- ✅ **Fast startup** - Ready in seconds
|
||||
- ⚠️ **Data resets** on server restart
|
||||
- 💾 **Perfect for development** and testing
|
||||
|
||||
### 🔧 **Database Options**
|
||||
|
||||
1. **In-Memory (Recommended for Development):**
|
||||
```bash
|
||||
# Automatic - just start the server
|
||||
npm run dev
|
||||
|
||||
# Or explicitly enable
|
||||
USE_MEMORY_DB=true npm run dev
|
||||
```
|
||||
|
||||
2. **Local MongoDB:**
|
||||
```bash
|
||||
# Install MongoDB locally, then:
|
||||
DATABASE_URI=mongodb://localhost:27017/payload-mailing-dev npm run dev
|
||||
```
|
||||
|
||||
3. **Remote MongoDB:**
|
||||
```bash
|
||||
# Set your connection string:
|
||||
DATABASE_URI=mongodb+srv://user:pass@cluster.mongodb.net/dbname npm run dev
|
||||
```
|
||||
|
||||
### 💡 **Database Startup Messages**
|
||||
When you start the dev server, you'll see helpful messages:
|
||||
```
|
||||
🚀 Starting MongoDB in-memory database...
|
||||
✅ MongoDB in-memory database started
|
||||
📊 Database URI: mongodb://***@localhost:port/payload-mailing-dev
|
||||
```
|
||||
|
||||
## Jobs Processing
|
||||
|
||||
The plugin automatically processes the outbox every 5 minutes and retries failed emails every 30 minutes. You can also trigger manual processing via:
|
||||
- Web interface "Process Outbox" button
|
||||
- API endpoint `POST /api/process-outbox`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Email not sending:
|
||||
1. Check MailHog is running on port 1025
|
||||
2. Check console logs for errors
|
||||
3. Verify template variables are correct
|
||||
4. Check outbox collection for error messages
|
||||
|
||||
### Plugin not loading:
|
||||
1. Ensure plugin is built: `npm run build` in root
|
||||
2. Check console for initialization message
|
||||
3. Verify plugin configuration in `payload.config.ts`
|
||||
|
||||
### Templates not appearing:
|
||||
1. Check seed function ran successfully
|
||||
2. Verify database connection
|
||||
3. Check admin panel collections
|
||||
|
||||
## Plugin API Usage
|
||||
|
||||
```javascript
|
||||
import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing'
|
||||
|
||||
// Send immediate email
|
||||
const emailId = await sendEmail(payload, {
|
||||
templateId: 'welcome-template-id',
|
||||
to: 'user@example.com',
|
||||
variables: {
|
||||
firstName: 'John',
|
||||
siteName: 'My App'
|
||||
}
|
||||
})
|
||||
|
||||
// Schedule email
|
||||
const scheduledId = await scheduleEmail(payload, {
|
||||
templateId: 'reminder-template-id',
|
||||
to: 'user@example.com',
|
||||
scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
|
||||
variables: {
|
||||
eventName: 'Product Launch'
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -1,9 +1,51 @@
|
||||
import { BeforeDashboardClient as BeforeDashboardClient_fc6e7dd366b9e2c8ce77d31252122343 } from 'temp-project/client'
|
||||
import { BeforeDashboardServer as BeforeDashboardServer_c4406fcca100b2553312c5a3d7520a3f } from 'temp-project/rsc'
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
|
||||
export const importMap = {
|
||||
'temp-project/client#BeforeDashboardClient':
|
||||
BeforeDashboardClient_fc6e7dd366b9e2c8ce77d31252122343,
|
||||
'temp-project/rsc#BeforeDashboardServer':
|
||||
BeforeDashboardServer_c4406fcca100b2553312c5a3d7520a3f,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864
|
||||
}
|
||||
|
||||
75
dev/app/api/create-test-user/route.ts
Normal file
75
dev/app/api/create-test-user/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
const body = await request.json()
|
||||
|
||||
// Generate random user data if not provided
|
||||
const userData = {
|
||||
email: body.email || `user-${Date.now()}@example.com`,
|
||||
password: body.password || 'TestPassword123!',
|
||||
firstName: body.firstName || 'Test',
|
||||
lastName: body.lastName || 'User',
|
||||
}
|
||||
|
||||
// Create the user
|
||||
const user = await payload.create({
|
||||
collection: 'users',
|
||||
data: userData,
|
||||
})
|
||||
|
||||
// Check if email was queued
|
||||
await new Promise(resolve => setTimeout(resolve, 500)) // Brief delay for email processing
|
||||
|
||||
const { docs: emails } = await payload.find({
|
||||
collection: 'emails' as const,
|
||||
where: {
|
||||
to: {
|
||||
equals: userData.email,
|
||||
},
|
||||
},
|
||||
limit: 1,
|
||||
sort: '-createdAt',
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
},
|
||||
emailQueued: emails.length > 0,
|
||||
email: emails.length > 0 ? {
|
||||
id: emails[0].id,
|
||||
subject: emails[0].subject,
|
||||
status: emails[0].status,
|
||||
} : null,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error creating test user:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to create user',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return Response.json({
|
||||
message: 'Use POST to create a test user',
|
||||
example: {
|
||||
email: 'optional@example.com',
|
||||
password: 'optional',
|
||||
firstName: 'optional',
|
||||
lastName: 'optional',
|
||||
},
|
||||
note: 'All fields are optional. Random values will be generated if not provided.',
|
||||
})
|
||||
}
|
||||
75
dev/app/api/process-emails/route.ts
Normal file
75
dev/app/api/process-emails/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// Queue the combined email queue processing job
|
||||
const job = await payload.jobs.queue({
|
||||
task: 'process-email-queue',
|
||||
input: {},
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: 'Email queue processing job queued successfully (will process both pending and failed emails)',
|
||||
jobId: job.id,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Process emails error:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to process emails',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// Get email queue statistics
|
||||
const pending = await payload.count({
|
||||
collection: 'emails' as const,
|
||||
where: { status: { equals: 'pending' } },
|
||||
})
|
||||
|
||||
const processing = await payload.count({
|
||||
collection: 'emails' as const,
|
||||
where: { status: { equals: 'processing' } },
|
||||
})
|
||||
|
||||
const sent = await payload.count({
|
||||
collection: 'emails' as const,
|
||||
where: { status: { equals: 'sent' } },
|
||||
})
|
||||
|
||||
const failed = await payload.count({
|
||||
collection: 'emails' as const,
|
||||
where: { status: { equals: 'failed' } },
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
statistics: {
|
||||
pending: pending.totalDocs,
|
||||
processing: processing.totalDocs,
|
||||
sent: sent.totalDocs,
|
||||
failed: failed.totalDocs,
|
||||
total: pending.totalDocs + processing.totalDocs + sent.totalDocs + failed.totalDocs,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get email stats error:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to get email statistics',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
86
dev/app/api/test-email/route.ts
Normal file
86
dev/app/api/test-email/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
const body = await request.json()
|
||||
const { type = 'send', templateSlug, to, variables, scheduledAt } = body
|
||||
|
||||
let result
|
||||
if (type === 'send') {
|
||||
// Send immediately
|
||||
result = await sendEmail(payload, {
|
||||
templateSlug,
|
||||
to,
|
||||
variables,
|
||||
})
|
||||
} else if (type === 'schedule') {
|
||||
// Schedule for later
|
||||
result = await scheduleEmail(payload, {
|
||||
templateSlug,
|
||||
to,
|
||||
variables,
|
||||
scheduledAt: scheduledAt ? new Date(scheduledAt) : new Date(Date.now() + 60000), // Default to 1 minute
|
||||
})
|
||||
} else {
|
||||
return Response.json({ error: 'Invalid type. Use "send" or "schedule"' }, { status: 400 })
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
emailId: result,
|
||||
message: type === 'send' ? 'Email sent successfully' : 'Email scheduled successfully',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Test email error:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to send email',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// Get email templates
|
||||
const { docs: templates } = await payload.find({
|
||||
collection: 'email-templates' as const,
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
// Get email queue status
|
||||
const { docs: queuedEmails, totalDocs } = await payload.find({
|
||||
collection: 'emails' as const,
|
||||
limit: 10,
|
||||
sort: '-createdAt',
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
templates,
|
||||
outbox: {
|
||||
emails: queuedEmails,
|
||||
total: totalDocs,
|
||||
},
|
||||
mailing: {
|
||||
pluginActive: !!(payload as any).mailing,
|
||||
service: !!(payload as any).mailing?.service,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get mailing status error:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to get mailing status',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
344
dev/app/mailing-test/page.tsx
Normal file
344
dev/app/mailing-test/page.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface Template {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
subject: string
|
||||
variables?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
description?: string
|
||||
}>
|
||||
previewData?: Record<string, any>
|
||||
}
|
||||
|
||||
interface QueuedEmail {
|
||||
id: string
|
||||
subject: string
|
||||
to: string[]
|
||||
status: string
|
||||
createdAt: string
|
||||
sentAt?: string
|
||||
attempts: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export default function MailingTestPage() {
|
||||
const [templates, setTemplates] = useState<Template[]>([])
|
||||
const [queuedEmails, setQueuedEmails] = useState<QueuedEmail[]>([])
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('')
|
||||
const [toEmail, setToEmail] = useState<string>('test@example.com')
|
||||
const [variables, setVariables] = useState<Record<string, any>>({})
|
||||
const [emailType, setEmailType] = useState<'send' | 'schedule'>('send')
|
||||
const [scheduleDate, setScheduleDate] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [message, setMessage] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/test-email')
|
||||
const data = await response.json()
|
||||
setTemplates(data.templates || [])
|
||||
setQueuedEmails(data.outbox?.emails || [])
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTemplateChange = (templateSlug: string) => {
|
||||
setSelectedTemplate(templateSlug)
|
||||
const template = templates.find(t => t.slug === templateSlug)
|
||||
if (template?.previewData) {
|
||||
setVariables(template.previewData)
|
||||
}
|
||||
}
|
||||
|
||||
const sendTestEmail = async () => {
|
||||
if (!selectedTemplate || !toEmail) {
|
||||
setMessage('Please select a template and enter an email address')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setMessage('')
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/test-email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: emailType,
|
||||
templateSlug: selectedTemplate,
|
||||
to: toEmail,
|
||||
variables,
|
||||
scheduledAt: emailType === 'schedule' ? scheduleDate : undefined,
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
setMessage(`✅ ${result.message} (ID: ${result.emailId})`)
|
||||
fetchData() // Refresh email queue
|
||||
} else {
|
||||
setMessage(`❌ Error: ${result.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const processEmailQueue = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/process-emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
setMessage(result.success ? `✅ ${result.message}` : `❌ ${result.error}`)
|
||||
|
||||
setTimeout(() => {
|
||||
fetchData() // Refresh after a delay to see status changes
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
setMessage(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTemplateData = templates.find(t => t.slug === selectedTemplate)
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<h1>📧 PayloadCMS Mailing Plugin Test</h1>
|
||||
|
||||
{message && (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
margin: '10px 0',
|
||||
backgroundColor: message.startsWith('✅') ? '#d4edda' : '#f8d7da',
|
||||
border: `1px solid ${message.startsWith('✅') ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
borderRadius: '4px',
|
||||
}}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
|
||||
{/* Send Email Form */}
|
||||
<div style={{ border: '1px solid #ddd', padding: '20px', borderRadius: '8px' }}>
|
||||
<h2>Send Test Email</h2>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>Email Template:</label>
|
||||
<select
|
||||
value={selectedTemplate}
|
||||
onChange={(e) => handleTemplateChange(e.target.value)}
|
||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
>
|
||||
<option value="">Select a template...</option>
|
||||
{templates.map(template => (
|
||||
<option key={template.id} value={template.slug}>{template.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>To Email:</label>
|
||||
<input
|
||||
type="email"
|
||||
value={toEmail}
|
||||
onChange={(e) => setToEmail(e.target.value)}
|
||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>Type:</label>
|
||||
<label style={{ marginRight: '15px' }}>
|
||||
<input
|
||||
type="radio"
|
||||
value="send"
|
||||
checked={emailType === 'send'}
|
||||
onChange={(e) => setEmailType(e.target.value as 'send' | 'schedule')}
|
||||
/>
|
||||
Send Now
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="schedule"
|
||||
checked={emailType === 'schedule'}
|
||||
onChange={(e) => setEmailType(e.target.value as 'send' | 'schedule')}
|
||||
/>
|
||||
Schedule
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{emailType === 'schedule' && (
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>Schedule Date:</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={scheduleDate}
|
||||
onChange={(e) => setScheduleDate(e.target.value)}
|
||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTemplateData?.variables && (
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<h3>Template Variables:</h3>
|
||||
{selectedTemplateData.variables.map(variable => (
|
||||
<div key={variable.name} style={{ marginBottom: '10px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>
|
||||
{variable.name} {variable.required && <span style={{ color: 'red' }}>*</span>}
|
||||
{variable.description && <small style={{ color: '#666' }}> - {variable.description}</small>}
|
||||
</label>
|
||||
<input
|
||||
type={variable.type === 'number' ? 'number' : variable.type === 'date' ? 'datetime-local' : 'text'}
|
||||
value={variables[variable.name] || ''}
|
||||
onChange={(e) => setVariables({
|
||||
...variables,
|
||||
[variable.name]: variable.type === 'number' ? Number(e.target.value) :
|
||||
variable.type === 'boolean' ? e.target.checked :
|
||||
e.target.value
|
||||
})}
|
||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={sendTestEmail}
|
||||
disabled={loading || !selectedTemplate || !toEmail}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
backgroundColor: loading ? '#ccc' : '#007bff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
{loading ? 'Sending...' : emailType === 'send' ? 'Send Email' : 'Schedule Email'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={processEmailQueue}
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
backgroundColor: loading ? '#ccc' : '#17a2b8',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
marginTop: '10px',
|
||||
}}
|
||||
>
|
||||
{loading ? 'Queuing...' : 'Process Email Queue (Pending + Failed)'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Email Queue */}
|
||||
<div style={{ border: '1px solid #ddd', padding: '20px', borderRadius: '8px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
|
||||
<h2>Email Queue</h2>
|
||||
<button
|
||||
onClick={fetchData}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#6c757d',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{queuedEmails.length === 0 ? (
|
||||
<p style={{ color: '#666' }}>No emails in queue</p>
|
||||
) : (
|
||||
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
|
||||
{queuedEmails.map(email => (
|
||||
<div
|
||||
key={email.id}
|
||||
style={{
|
||||
border: '1px solid #eee',
|
||||
padding: '12px',
|
||||
marginBottom: '10px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: email.status === 'sent' ? '#f8f9fa' :
|
||||
email.status === 'failed' ? '#fff3cd' :
|
||||
email.status === 'processing' ? '#e3f2fd' : 'white',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '5px' }}>{email.subject}</div>
|
||||
<div style={{ fontSize: '14px', color: '#666' }}>
|
||||
To: {Array.isArray(email.to) ? email.to.join(', ') : email.to} | Status: <strong>{email.status}</strong> | Attempts: {email.attempts}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
Created: {new Date(email.createdAt).toLocaleString()}
|
||||
{email.sentAt && ` | Sent: ${new Date(email.sentAt).toLocaleString()}`}
|
||||
</div>
|
||||
{email.error && (
|
||||
<div style={{ fontSize: '12px', color: '#dc3545', marginTop: '5px' }}>
|
||||
Error: {email.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Templates Overview */}
|
||||
<div style={{ marginTop: '30px', border: '1px solid #ddd', padding: '20px', borderRadius: '8px' }}>
|
||||
<h2>Available Templates</h2>
|
||||
{templates.length === 0 ? (
|
||||
<p style={{ color: '#666' }}>No templates available</p>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '15px' }}>
|
||||
{templates.map(template => (
|
||||
<div key={template.id} style={{ border: '1px solid #eee', padding: '15px', borderRadius: '4px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0' }}>{template.name}</h3>
|
||||
<p style={{ margin: '0 0 10px 0', color: '#666', fontSize: '14px' }}>{template.subject}</p>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
Variables: {template.variables?.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export const devUser = {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
firstName: 'Dev',
|
||||
lastName: 'User',
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import config from '@payload-config'
|
||||
import { createPayloadRequest, getPayload } from 'payload'
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||
|
||||
import { customEndpointHandler } from '../src/endpoints/customEndpointHandler.js'
|
||||
// import { customEndpointHandler } from '../src/endpoints/customEndpointHandler.js'
|
||||
|
||||
let payload: Payload
|
||||
|
||||
@@ -17,19 +17,11 @@ beforeAll(async () => {
|
||||
})
|
||||
|
||||
describe('Plugin integration tests', () => {
|
||||
test('should query custom endpoint added by plugin', async () => {
|
||||
const request = new Request('http://localhost:3000/api/my-plugin-endpoint', {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
const payloadRequest = await createPayloadRequest({ config, request })
|
||||
const response = await customEndpointHandler(payloadRequest)
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const data = await response.json()
|
||||
expect(data).toMatchObject({
|
||||
message: 'Hello from custom endpoint',
|
||||
})
|
||||
test('should have mailing plugin initialized', async () => {
|
||||
expect(payload).toBeDefined()
|
||||
expect((payload as any).mailing).toBeDefined()
|
||||
expect((payload as any).mailing.service).toBeDefined()
|
||||
expect((payload as any).mailing.config).toBeDefined()
|
||||
})
|
||||
|
||||
test('can create post with custom text field added by plugin', async () => {
|
||||
|
||||
1
dev/next-env.d.ts
vendored
1
dev/next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -6,25 +6,85 @@
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
users: User;
|
||||
posts: Post;
|
||||
media: Media;
|
||||
'plugin-collection': PluginCollection;
|
||||
users: User;
|
||||
'email-templates': EmailTemplate;
|
||||
emails: Email;
|
||||
'payload-jobs': PayloadJob;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
'plugin-collection': PluginCollectionSelect<false> | PluginCollectionSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'email-templates': EmailTemplatesSelect<false> | EmailTemplatesSelect<true>;
|
||||
emails: EmailsSelect<false> | EmailsSelect<true>;
|
||||
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
@@ -39,7 +99,13 @@ export interface Config {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
tasks: {
|
||||
'process-email-queue': ProcessEmailQueueJob;
|
||||
inline: {
|
||||
input: unknown;
|
||||
output: unknown;
|
||||
};
|
||||
};
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
@@ -61,13 +127,38 @@ export interface UserAuthOperations {
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
sessions?:
|
||||
| {
|
||||
id: string;
|
||||
createdAt?: string | null;
|
||||
expiresAt: string;
|
||||
}[]
|
||||
| null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
addedByPlugin?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -91,29 +182,225 @@ export interface Media {
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "plugin-collection".
|
||||
* via the `definition` "email-templates".
|
||||
*/
|
||||
export interface PluginCollection {
|
||||
export interface EmailTemplate {
|
||||
id: string;
|
||||
/**
|
||||
* A descriptive name for this email template
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Unique identifier for this template (e.g., "welcome-email", "password-reset")
|
||||
*/
|
||||
slug: string;
|
||||
/**
|
||||
* Email subject line. You can use Handlebars variables like {{firstName}} or {{siteName}}.
|
||||
*/
|
||||
subject: string;
|
||||
/**
|
||||
* Email content with rich text formatting. Supports Handlebars variables like {{firstName}} and helpers like {{formatDate createdAt "long"}}. Content is converted to HTML and plain text automatically.
|
||||
*/
|
||||
content: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* Email delivery and status tracking
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "emails".
|
||||
*/
|
||||
export interface Email {
|
||||
id: string;
|
||||
/**
|
||||
* Email template used (optional if custom content provided)
|
||||
*/
|
||||
template?: (string | null) | EmailTemplate;
|
||||
/**
|
||||
* Template slug used for this email
|
||||
*/
|
||||
templateSlug?: string | null;
|
||||
/**
|
||||
* Recipient email address(es), comma-separated
|
||||
*/
|
||||
to: string;
|
||||
/**
|
||||
* CC email address(es), comma-separated
|
||||
*/
|
||||
cc?: string | null;
|
||||
/**
|
||||
* BCC email address(es), comma-separated
|
||||
*/
|
||||
bcc?: string | null;
|
||||
/**
|
||||
* Sender email address (optional, uses default if not provided)
|
||||
*/
|
||||
from?: string | null;
|
||||
/**
|
||||
* Reply-to email address
|
||||
*/
|
||||
replyTo?: string | null;
|
||||
/**
|
||||
* Email subject line
|
||||
*/
|
||||
subject: string;
|
||||
/**
|
||||
* Rendered HTML content of the email
|
||||
*/
|
||||
html: string;
|
||||
/**
|
||||
* Plain text version of the email
|
||||
*/
|
||||
text?: string | null;
|
||||
/**
|
||||
* Template variables used to render this email
|
||||
*/
|
||||
variables?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
/**
|
||||
* When this email should be sent (leave empty for immediate)
|
||||
*/
|
||||
scheduledAt?: string | null;
|
||||
/**
|
||||
* When this email was actually sent
|
||||
*/
|
||||
sentAt?: string | null;
|
||||
/**
|
||||
* Current status of this email
|
||||
*/
|
||||
status: 'pending' | 'processing' | 'sent' | 'failed';
|
||||
/**
|
||||
* Number of send attempts made
|
||||
*/
|
||||
attempts?: number | null;
|
||||
/**
|
||||
* When the last send attempt was made
|
||||
*/
|
||||
lastAttemptAt?: string | null;
|
||||
/**
|
||||
* Last error message if send failed
|
||||
*/
|
||||
error?: string | null;
|
||||
/**
|
||||
* Email priority (1=highest, 10=lowest)
|
||||
*/
|
||||
priority?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
* via the `definition` "payload-jobs".
|
||||
*/
|
||||
export interface User {
|
||||
export interface PayloadJob {
|
||||
id: string;
|
||||
/**
|
||||
* Input data provided to the job
|
||||
*/
|
||||
input?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
taskStatus?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
completedAt?: string | null;
|
||||
totalTried?: number | null;
|
||||
/**
|
||||
* If hasError is true this job will not be retried
|
||||
*/
|
||||
hasError?: boolean | null;
|
||||
/**
|
||||
* If hasError is true, this is the error that caused it
|
||||
*/
|
||||
error?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
/**
|
||||
* Task execution log
|
||||
*/
|
||||
log?:
|
||||
| {
|
||||
executedAt: string;
|
||||
completedAt: string;
|
||||
taskSlug: 'inline' | 'process-email-queue';
|
||||
taskID: string;
|
||||
input?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
output?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
state: 'failed' | 'succeeded';
|
||||
error?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
taskSlug?: ('inline' | 'process-email-queue') | null;
|
||||
queue?: string | null;
|
||||
waitUntil?: string | null;
|
||||
processing?: boolean | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -122,6 +409,10 @@ export interface User {
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
@@ -131,12 +422,16 @@ export interface PayloadLockedDocument {
|
||||
value: string | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'plugin-collection';
|
||||
value: string | PluginCollection;
|
||||
relationTo: 'email-templates';
|
||||
value: string | EmailTemplate;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
relationTo: 'emails';
|
||||
value: string | Email;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'payload-jobs';
|
||||
value: string | PayloadJob;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
@@ -180,12 +475,35 @@ export interface PayloadMigration {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
firstName?: T;
|
||||
lastName?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
sessions?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
createdAt?: T;
|
||||
expiresAt?: T;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts_select".
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
addedByPlugin?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
@@ -208,27 +526,72 @@ export interface MediaSelect<T extends boolean = true> {
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "plugin-collection_select".
|
||||
* via the `definition` "email-templates_select".
|
||||
*/
|
||||
export interface PluginCollectionSelect<T extends boolean = true> {
|
||||
id?: T;
|
||||
export interface EmailTemplatesSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
slug?: T;
|
||||
subject?: T;
|
||||
content?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
* via the `definition` "emails_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
export interface EmailsSelect<T extends boolean = true> {
|
||||
template?: T;
|
||||
templateSlug?: T;
|
||||
to?: T;
|
||||
cc?: T;
|
||||
bcc?: T;
|
||||
from?: T;
|
||||
replyTo?: T;
|
||||
subject?: T;
|
||||
html?: T;
|
||||
text?: T;
|
||||
variables?: T;
|
||||
scheduledAt?: T;
|
||||
sentAt?: T;
|
||||
status?: T;
|
||||
attempts?: T;
|
||||
lastAttemptAt?: T;
|
||||
error?: T;
|
||||
priority?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-jobs_select".
|
||||
*/
|
||||
export interface PayloadJobsSelect<T extends boolean = true> {
|
||||
input?: T;
|
||||
taskStatus?: T;
|
||||
completedAt?: T;
|
||||
totalTried?: T;
|
||||
hasError?: T;
|
||||
error?: T;
|
||||
log?:
|
||||
| T
|
||||
| {
|
||||
executedAt?: T;
|
||||
completedAt?: T;
|
||||
taskSlug?: T;
|
||||
taskID?: T;
|
||||
input?: T;
|
||||
output?: T;
|
||||
state?: T;
|
||||
error?: T;
|
||||
id?: T;
|
||||
};
|
||||
taskSlug?: T;
|
||||
queue?: T;
|
||||
waitUntil?: T;
|
||||
processing?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -262,6 +625,14 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "ProcessEmailQueueJob".
|
||||
*/
|
||||
export interface ProcessEmailQueueJob {
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import {
|
||||
BlocksFeature,
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
HorizontalRuleFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalHTML,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import { MongoMemoryReplSet } from 'mongodb-memory-server'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload'
|
||||
import { tempProject } from 'temp-project'
|
||||
import sharp from 'sharp'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { testEmailAdapter } from './helpers/testEmailAdapter.js'
|
||||
import { seed } from './seed.js'
|
||||
import { seed, seedUser } from './seed.js'
|
||||
import mailingPlugin from "../src/plugin.js"
|
||||
import { sendEmail } from "../src/utils/helpers.js"
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -18,15 +27,32 @@ if (!process.env.ROOT_DIR) {
|
||||
}
|
||||
|
||||
const buildConfigWithMemoryDB = async () => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// Use in-memory MongoDB for development and testing
|
||||
if (process.env.NODE_ENV === 'test' || process.env.USE_MEMORY_DB === 'true' || !process.env.DATABASE_URI) {
|
||||
console.log('🚀 Starting MongoDB in-memory database...')
|
||||
|
||||
const memoryDB = await MongoMemoryReplSet.create({
|
||||
replSet: {
|
||||
count: 3,
|
||||
dbName: 'payloadmemory',
|
||||
count: 1, // Single instance for dev (faster startup)
|
||||
dbName: process.env.NODE_ENV === 'test' ? 'payloadmemory' : 'payload-mailing-dev',
|
||||
storageEngine: 'wiredTiger',
|
||||
},
|
||||
})
|
||||
|
||||
process.env.DATABASE_URI = `${memoryDB.getUri()}&retryWrites=true`
|
||||
const uri = `${memoryDB.getUri()}&retryWrites=true`
|
||||
process.env.DATABASE_URI = uri
|
||||
|
||||
console.log('✅ MongoDB in-memory database started')
|
||||
console.log(`📊 Database URI: ${uri.replace(/mongodb:\/\/[^@]*@/, 'mongodb://***@')}`)
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('🛑 Stopping MongoDB in-memory database...')
|
||||
await memoryDB.stop()
|
||||
process.exit(0)
|
||||
})
|
||||
} else {
|
||||
console.log(`🔗 Using external MongoDB: ${process.env.DATABASE_URI?.replace(/mongodb:\/\/[^@]*@/, 'mongodb://***@')}`)
|
||||
}
|
||||
|
||||
return buildConfig({
|
||||
@@ -36,6 +62,52 @@ const buildConfigWithMemoryDB = async () => {
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'firstName',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
afterChange: [
|
||||
async ({ doc, operation, req, previousDoc }) => {
|
||||
// Only send welcome email on user creation, not updates
|
||||
if (operation === 'create' && doc.email) {
|
||||
try {
|
||||
console.log('📧 Queuing welcome email for new user:', doc.email)
|
||||
|
||||
// Queue the welcome email using template slug
|
||||
const emailId = await sendEmail(req.payload, {
|
||||
templateSlug: 'welcome-email',
|
||||
to: doc.email,
|
||||
variables: {
|
||||
firstName: doc.firstName || doc.email?.split('@')?.[0],
|
||||
siteName: 'PayloadCMS Mailing Demo',
|
||||
createdAt: new Date().toISOString(),
|
||||
isPremium: false,
|
||||
dashboardUrl: 'http://localhost:3000/admin',
|
||||
},
|
||||
})
|
||||
|
||||
console.log('✅ Welcome email queued successfully. Email ID:', emailId)
|
||||
} catch (error) {
|
||||
console.error('❌ Error queuing welcome email:', error)
|
||||
// Don't throw - we don't want to fail user creation if email fails
|
||||
}
|
||||
}
|
||||
|
||||
return doc
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'posts',
|
||||
fields: [],
|
||||
@@ -58,9 +130,93 @@ const buildConfigWithMemoryDB = async () => {
|
||||
await seed(payload)
|
||||
},
|
||||
plugins: [
|
||||
tempProject({
|
||||
collections: {
|
||||
posts: true,
|
||||
mailingPlugin({
|
||||
defaultFrom: 'noreply@test.com',
|
||||
initOrder: 'after',
|
||||
transport: {
|
||||
host: 'localhost',
|
||||
port: 1025, // MailHog port for dev
|
||||
secure: false,
|
||||
auth: {
|
||||
user: 'test',
|
||||
pass: 'test',
|
||||
},
|
||||
},
|
||||
retryAttempts: 3,
|
||||
retryDelay: 60000, // 1 minute for dev
|
||||
queue: 'email-queue',
|
||||
|
||||
// Optional: Custom rich text editor configuration
|
||||
// Comment out to use default lexical editor
|
||||
richTextEditor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
// Example: Add custom features for email templates
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3'] }),
|
||||
HorizontalRuleFeature(),
|
||||
// You can add more features like:
|
||||
// BlocksFeature({ blocks: [...] }),
|
||||
// LinkFeature({ ... }),
|
||||
// etc.
|
||||
],
|
||||
}),
|
||||
|
||||
emailWrapper: (email) => {
|
||||
// Example: wrap email content in a custom layout
|
||||
const wrappedHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${email.subject}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
||||
.container { max-width: 600px; margin: 0 auto; background: white; border-radius: 8px; overflow: hidden; }
|
||||
.header { background: #007bff; color: white; padding: 20px; text-align: center; }
|
||||
.content { padding: 30px; }
|
||||
.footer { background: #f8f9fa; padding: 15px; text-align: center; font-size: 12px; color: #6c757d; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>My Company</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
${email.html}
|
||||
</div>
|
||||
<div class="footer">
|
||||
This email was sent from My Company. If you have questions, contact support@mycompany.com
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
const wrappedText = `
|
||||
MY COMPANY
|
||||
==========
|
||||
|
||||
${email.text || email.html?.replace(/<[^>]*>/g, '')}
|
||||
|
||||
---
|
||||
This email was sent from My Company.
|
||||
If you have questions, contact support@mycompany.com
|
||||
`
|
||||
|
||||
return {
|
||||
...email,
|
||||
html: wrappedHtml,
|
||||
text: wrappedText.trim(),
|
||||
}
|
||||
},
|
||||
|
||||
// Called after mailing plugin is fully initialized
|
||||
onReady: async (payload) => {
|
||||
await seedUser(payload)
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
210
dev/seed.ts
210
dev/seed.ts
@@ -3,8 +3,214 @@ import type { Payload } from 'payload'
|
||||
import { devUser } from './helpers/credentials.js'
|
||||
|
||||
export const seed = async (payload: Payload) => {
|
||||
// Create example email template
|
||||
const { totalDocs: templateCount } = await payload.count({
|
||||
collection: 'email-templates' as const,
|
||||
})
|
||||
|
||||
if (templateCount === 0) {
|
||||
// Simple welcome email template
|
||||
await payload.create({
|
||||
collection: 'email-templates' as const,
|
||||
data: {
|
||||
name: 'Welcome Email',
|
||||
slug: 'welcome-email',
|
||||
subject: 'Welcome to {{siteName}}, {{firstName}}!',
|
||||
content: {
|
||||
root: {
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Welcome {{firstName}}! 🎉',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
tag: 'h1',
|
||||
type: 'heading',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: "We're thrilled to have you join {{siteName}}! This email demonstrates how easy it is to create beautiful emails using PayloadCMS's rich text editor with Handlebars variables.",
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 1,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'What you can do:',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Create beautiful emails with rich text formatting',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
value: 1,
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Use the emailWrapper hook to add custom layouts',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
value: 2,
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Queue and schedule emails effortlessly',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
value: 3,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
listType: 'bullet',
|
||||
start: 1,
|
||||
tag: 'ul',
|
||||
type: 'list',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Get started by exploring the admin panel and creating your own email templates. Your account was created on {{formatDate createdAt "long"}}.',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Best regards,',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'linebreak',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'The {{siteName}} Team',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'root',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
console.log('✅ Example email template created successfully')
|
||||
}
|
||||
}
|
||||
|
||||
export const seedUser = async (payload: Payload) => {
|
||||
// Create dev user if not exists - called after mailing plugin is initialized
|
||||
const { totalDocs } = await payload.count({
|
||||
collection: 'users',
|
||||
collection: 'users' as const,
|
||||
where: {
|
||||
email: {
|
||||
equals: devUser.email,
|
||||
@@ -14,7 +220,7 @@ export const seed = async (payload: Payload) => {
|
||||
|
||||
if (!totalDocs) {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
collection: 'users' as const,
|
||||
data: devUser,
|
||||
})
|
||||
}
|
||||
|
||||
45
dev/start-dev.js
Executable file
45
dev/start-dev.js
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Development startup script for PayloadCMS Mailing Plugin
|
||||
// This ensures proper environment setup and provides helpful information
|
||||
|
||||
console.log('🚀 PayloadCMS Mailing Plugin - Development Mode')
|
||||
console.log('=' .repeat(50))
|
||||
|
||||
// Set development environment
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development'
|
||||
|
||||
// Enable in-memory MongoDB by default for development
|
||||
if (!process.env.DATABASE_URI) {
|
||||
process.env.USE_MEMORY_DB = 'true'
|
||||
console.log('📦 Using in-memory MongoDB (no installation required)')
|
||||
} else {
|
||||
console.log(`🔗 Using external MongoDB: ${process.env.DATABASE_URI}`)
|
||||
}
|
||||
|
||||
console.log('')
|
||||
console.log('🔧 Starting development server...')
|
||||
console.log('📧 Mailing plugin configured with test transport')
|
||||
console.log('🎯 Test interface will be available at: /mailing-test')
|
||||
console.log('')
|
||||
|
||||
// Import and start Next.js
|
||||
import('next/dist/cli/next-dev.js')
|
||||
.then(({ nextDev }) => {
|
||||
nextDev([])
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Failed to start development server:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('\n🛑 Shutting down development server...')
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n🛑 Shutting down development server...')
|
||||
process.exit(0)
|
||||
})
|
||||
40
dev/test-plugin.mjs
Normal file
40
dev/test-plugin.mjs
Normal file
@@ -0,0 +1,40 @@
|
||||
// Simple test to verify plugin can be imported and initialized
|
||||
import { mailingPlugin, sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing'
|
||||
|
||||
console.log('✅ Plugin imports successfully')
|
||||
console.log('✅ mailingPlugin:', typeof mailingPlugin)
|
||||
console.log('✅ sendEmail:', typeof sendEmail)
|
||||
console.log('✅ scheduleEmail:', typeof scheduleEmail)
|
||||
|
||||
// Test plugin configuration
|
||||
try {
|
||||
const testConfig = {
|
||||
collections: [],
|
||||
db: null,
|
||||
secret: 'test'
|
||||
}
|
||||
|
||||
const pluginFn = mailingPlugin({
|
||||
defaultFrom: 'test@example.com',
|
||||
transport: {
|
||||
host: 'localhost',
|
||||
port: 1025,
|
||||
secure: false,
|
||||
auth: { user: 'test', pass: 'test' }
|
||||
}
|
||||
})
|
||||
|
||||
const configWithPlugin = pluginFn(testConfig)
|
||||
console.log('✅ Plugin configuration works')
|
||||
console.log('✅ Collections added:', configWithPlugin.collections?.length > testConfig.collections.length)
|
||||
console.log('✅ Jobs configured:', !!configWithPlugin.jobs)
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Plugin configuration error:', error.message)
|
||||
}
|
||||
|
||||
console.log('\n🎉 PayloadCMS Mailing Plugin is ready for development!')
|
||||
console.log('\nNext steps:')
|
||||
console.log('1. Run: npm run dev (in dev directory)')
|
||||
console.log('2. Open: http://localhost:3000/admin')
|
||||
console.log('3. Test: http://localhost:3000/mailing-test')
|
||||
@@ -31,5 +31,10 @@
|
||||
},
|
||||
"noEmit": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user