payload-wordpress-migrator 0.0.22 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,36 +1,26 @@
1
- # Payload WordPress Migrator Plugin
1
+ # Payload WordPress Migrator
2
2
 
3
- A comprehensive PayloadCMS plugin for WordPress migration - migrate and manage WordPress content directly in your Payload admin dashboard with a powerful React-based interface.
3
+ A PayloadCMS plugin for migrating WordPress content manage migrations directly from the Payload admin dashboard.
4
4
 
5
- ## 🚀 Features
5
+ ## Features
6
6
 
7
- - 📊 **Interactive Migration Dashboard**: Real-time progress tracking with a modern React interface
8
- - 🔄 **WordPress API Integration**: Secure REST API connectivity with application password authentication
9
- - 📋 **Smart Job Management**: Create, monitor, pause, resume, and retry migration jobs
10
- - 🎯 **Content Type Discovery**: Automatic detection of posts, pages, media, categories, and custom post types
11
- - 🔗 **Advanced Field Mapping**: Visual field mapping interface with dot-notation support
12
- - 🖼️ **Intelligent Media Migration**: Auto-import featured images and content media with duplicate detection
13
- - 🧱 **Gutenberg to Lexical**: Convert WordPress blocks to PayloadCMS Lexical format
14
- - **Performance Optimized**: Configurable batch processing with background job execution
15
- - 🔄 **Resume & Retry**: Intelligent job recovery with progress preservation
16
- - 🔒 **Production Ready**: Comprehensive error handling and SSL certificate management
7
+ - **Migration Dashboard** — real-time progress tracking in the Payload admin UI
8
+ - **WordPress REST API Integration** secure connectivity with Application Passwords
9
+ - **Job Management** create, pause, resume, retry, rollback, and dry-run migration jobs
10
+ - **Content Type Discovery** automatic detection of posts, pages, media, categories, tags, custom post types
11
+ - **Field Mapping** visual mapping interface with dot-notation support
12
+ - **Media Migration** — auto-import featured images and content media with duplicate detection
13
+ - **Gutenberg to Lexical** converts WordPress blocks to PayloadCMS Lexical rich text
14
+ - **Batch Processing** configurable concurrency with background execution
15
+ - **Access Control** configurable role-based access for migration endpoints
17
16
 
18
- ## 📦 Installation
17
+ ## Installation
19
18
 
20
19
  ```bash
21
- # npm
22
20
  npm install payload-wordpress-migrator
23
-
24
- # yarn
25
- yarn add payload-wordpress-migrator
26
-
27
- # pnpm
28
- pnpm add payload-wordpress-migrator
29
21
  ```
30
22
 
31
- ## 🚀 Quick Start
32
-
33
- ### Basic Setup
23
+ ## Quick Start
34
24
 
35
25
  Add the plugin to your Payload config:
36
26
 
@@ -39,12 +29,8 @@ import { buildConfig } from 'payload'
39
29
  import { payloadWordPressMigrator } from 'payload-wordpress-migrator'
40
30
 
41
31
  export default buildConfig({
42
- // ... your existing config
43
32
  plugins: [
44
33
  payloadWordPressMigrator({
45
- wpSiteUrl: process.env.WORDPRESS_API_URL,
46
- wpUsername: process.env.WORDPRESS_USERNAME,
47
- wpPassword: process.env.WORDPRESS_APP_PASSWORD,
48
34
  collections: {
49
35
  posts: { wpPostType: 'post', enableBlocks: true },
50
36
  pages: { wpPostType: 'page', enableBlocks: true },
@@ -57,146 +43,110 @@ export default buildConfig({
57
43
  })
58
44
  ```
59
45
 
60
- ### Environment Variables
61
-
62
- Add these to your `.env` file:
63
-
64
- ```env
65
- WORDPRESS_API_URL=https://your-wordpress-site.com
66
- WORDPRESS_USERNAME=your-username
67
- WORDPRESS_APP_PASSWORD=your-application-password
68
- ```
69
-
70
46
  ### WordPress Setup
71
47
 
72
- #### Application Password Setup
48
+ **Application Password:**
73
49
 
74
- 1. In WordPress admin, go to **Users** **Profile**
75
- 2. Scroll down to **Application Passwords**
76
- 3. Enter a name (e.g., "Payload Migration")
77
- 4. Click **Add New Application Password**
78
- 5. Copy the generated password (save it securely)
79
- 6. Use this password as `WORDPRESS_APP_PASSWORD`
50
+ 1. In WordPress admin, go to **Users > Profile**
51
+ 2. Scroll to **Application Passwords**
52
+ 3. Enter a name (e.g., "Payload Migration") and click **Add New Application Password**
53
+ 4. Copy the generated password — use this as `WORDPRESS_APP_PASSWORD`
80
54
 
81
- #### REST API Requirements
55
+ **Requirements:**
82
56
 
83
57
  - WordPress 5.6+ (for Application Passwords)
84
58
  - REST API enabled (default in most installations)
85
59
  - User with appropriate permissions for content access
86
60
 
87
- ### Configuration Options
61
+ ## Configuration
88
62
 
89
63
  ```typescript
90
64
  type PayloadWordPressMigratorConfig = {
91
65
  // WordPress Connection
92
66
  wpSiteUrl?: string // WordPress site URL
93
67
  wpUsername?: string // WordPress username
94
- wpPassword?: string // WordPress application password
68
+ wpPassword?: string // Application password (not the login password)
95
69
 
96
- // Collection Configuration
70
+ // Collections
97
71
  collections?: Partial<Record<CollectionSlug, WordPressCollectionMapping>>
98
72
 
99
73
  // Plugin Control
100
- disabled?: boolean // Disable plugin functionality
101
- disableDashboard?: boolean // Disable dashboard UI
74
+ disabled?: boolean // Disable plugin (schema still added for DB consistency)
75
+ disableDashboard?: boolean // Hide dashboard UI
102
76
 
103
- // Performance Settings
104
- migrationBatchSize?: number // Batch size (default: 10)
105
- enableAutoSync?: boolean // Auto-sync on startup
77
+ // Performance
78
+ migrationBatchSize?: number // Items per batch (default: 10)
79
+ migrationConcurrency?: number // Parallel items within a batch (default: 1)
80
+ wpRequestDelay?: number // Delay in ms between WP API requests (default: 0)
106
81
 
107
- // Media Migration
108
- enableMediaDownload?: boolean // Download actual files (default: false)
82
+ // Media
83
+ enableMediaDownload?: boolean // Download files from WordPress (default: false)
109
84
  maxMediaFileSize?: number // Max file size in bytes (default: 10MB)
85
+ mediaUploadPath?: string // Custom upload directory for media files
110
86
  allowedMediaTypes?: string[] // Allowed MIME types
111
87
  allowSelfSignedCerts?: boolean // Allow self-signed SSL (dev only)
88
+
89
+ // Access Control
90
+ access?: (args: { req: PayloadRequest }) => boolean | Promise<boolean>
112
91
  }
113
92
 
114
93
  type WordPressCollectionMapping = {
115
- wpPostType: string // WordPress content type
116
- fieldMapping?: Record<string, string> // Custom field mapping
117
- enableBlocks?: boolean // Convert Gutenberg blocks
94
+ wpPostType: string // WordPress content type slug
95
+ fieldMapping?: Record<string, string> // WP field path → Payload field path
96
+ enableBlocks?: boolean // Convert Gutenberg blocks to Lexical
118
97
  customFields?: string[] // ACF field names to migrate
119
- importContentMedia?: boolean // Auto-import content images
98
+ importContentMedia?: boolean // Auto-import images from content HTML
99
+ disableHtmlConversion?: boolean // Skip HTML-to-Lexical conversion
120
100
  }
121
101
  ```
122
102
 
123
- ## 📋 Plugin Architecture
124
-
125
- The plugin creates the following collections and components in your PayloadCMS instance:
126
-
127
- ### Collections Added
128
-
129
- 1. **`wordpress-migration`**: Core collection for managing migration jobs
130
- - Job configuration and status tracking
131
- - Progress monitoring with detailed logs
132
- - Built-in dashboard interface
103
+ ## Plugin Architecture
133
104
 
134
- 2. **Enhanced Target Collections**: Automatically adds WordPress metadata fields to configured collections:
135
- - `migratedFromWordPress.wpPostId`: Original WordPress post ID
136
- - `migratedFromWordPress.wpPostType`: WordPress content type
137
- - `migratedFromWordPress.migrationDate`: Migration timestamp
105
+ ### Collections
138
106
 
139
- ### UI Components
107
+ The plugin creates a `wordpress-migration` collection for job management and adds `migratedFromWordPress` metadata fields to configured target collections:
140
108
 
141
- - **Migration Dashboard**: React-based interface with real-time updates
142
- - **WordPress Site Configuration**: Secure credential management with localStorage persistence
143
- - **Content Type Selector**: Dynamic dropdown populated from WordPress API discovery
144
- - **Field Mapping Interface**: Visual field mapping with source/destination field analysis
145
- - **Job Monitor**: Live progress tracking with success/failure metrics
109
+ - `migratedFromWordPress.wpPostId` original WordPress post ID
110
+ - `migratedFromWordPress.wpPostType` WordPress content type
111
+ - `migratedFromWordPress.migrationDate` migration timestamp
146
112
 
147
113
  ### API Endpoints
148
114
 
149
- The plugin adds these endpoints to your PayloadCMS API:
150
-
151
115
  ```
152
116
  POST /api/wordpress/test-connection # Test WordPress connectivity
153
117
  GET /api/wordpress/migration-summary # Dashboard data with caching
154
118
  POST /api/wordpress/discover-content # Content type discovery
155
- GET /api/wordpress/migration-jobs # Job status retrieval
156
- POST /api/wordpress/migration-jobs # Start migration jobs
119
+ GET /api/wordpress/migration-jobs # Job status
120
+ POST /api/wordpress/migration-jobs # Start/retry/resume/rollback jobs
157
121
  PUT /api/wordpress/migration-jobs # Pause/resume jobs
158
122
  DELETE /api/wordpress/migration-jobs # Delete jobs
159
123
  POST /api/wordpress/content-fields # Analyze WordPress fields
160
124
  GET /api/collections/:slug/fields # Analyze PayloadCMS fields
161
125
  ```
162
126
 
163
- ## 🎯 Content Migration Features
164
-
165
- ### Supported Content Types
166
-
167
- | Content Type | Features | Notes |
168
- | --------------------- | ------------------------------------------------------- | --------------------------------- |
169
- | **Posts** | ✅ Content, metadata, categories, tags, featured images | Full Gutenberg block conversion |
170
- | **Pages** | ✅ Content, metadata, featured images, custom fields | Hierarchical structure support |
171
- | **Media** | ✅ File downloads, metadata, alt text, captions | MIME type filtering & size limits |
172
- | **Categories** | ✅ Names, descriptions, hierarchy, counts | Taxonomy relationships |
173
- | **Tags** | ✅ Names, descriptions, post associations | Tag cloud support |
174
- | **Users** | ✅ User data, roles, capabilities, avatars | Permission mapping |
175
- | **Custom Post Types** | ✅ Auto-discovery, custom fields, metadata | ACF integration |
176
-
177
- ### Advanced Content Processing
178
-
179
- - **HTML to Lexical Conversion**: Sophisticated converter handles complex HTML structures
180
- - **Gutenberg Block Processing**: Converts blocks to PayloadCMS block format
181
- - **ACF Field Migration**: Automatic Advanced Custom Fields detection and migration
182
- - **Media Auto-Import**: Scans content for WordPress media URLs and imports automatically
183
- - **Relationship Preservation**: Maintains post-category, post-tag, and other relationships
184
-
185
- ## 🖼️ Media Migration
127
+ ## Supported Content Types
186
128
 
187
- ### Migration Modes
129
+ | Content Type | Features | Notes |
130
+ | --------------------- | ---------------------------------------------------- | ----------------------------------- |
131
+ | **Posts** | Content, metadata, categories, tags, featured images | Full Gutenberg block conversion |
132
+ | **Pages** | Content, metadata, featured images, custom fields | Hierarchical structure support |
133
+ | **Media** | File downloads, metadata, alt text, captions | MIME type filtering and size limits |
134
+ | **Categories** | Names, descriptions, hierarchy, counts | Taxonomy relationships |
135
+ | **Tags** | Names, descriptions, post associations | — |
136
+ | **Users** | User data, roles, capabilities, avatars | Permission mapping |
137
+ | **Custom Post Types** | Auto-discovery, custom fields, metadata | ACF integration |
188
138
 
189
- The plugin supports two modes for media migration:
139
+ ## Media Migration
190
140
 
191
- 1. **Metadata Only** (default): Migrates media titles, descriptions, alt text, and URLs
192
- 2. **Full File Migration**: Downloads files from WordPress and uploads them to PayloadCMS
141
+ Two modes:
193
142
 
194
- ### Enabling Media File Migration
143
+ 1. **Metadata Only** (default) — migrates titles, descriptions, alt text, and URLs
144
+ 2. **Full File Migration** — downloads files from WordPress and uploads to PayloadCMS
195
145
 
196
146
  ```typescript
197
147
  payloadWordPressMigrator({
198
- enableMediaDownload: true, // Enable file downloads
199
- maxMediaFileSize: 50 * 1024 * 1024, // 50MB limit
148
+ enableMediaDownload: true,
149
+ maxMediaFileSize: 50 * 1024 * 1024, // 50MB
200
150
  allowedMediaTypes: [
201
151
  'image/jpeg',
202
152
  'image/png',
@@ -205,184 +155,75 @@ payloadWordPressMigrator({
205
155
  'application/pdf',
206
156
  'video/mp4',
207
157
  ],
208
- allowSelfSignedCerts: true, // For development only
209
158
  })
210
159
  ```
211
160
 
212
- ### Auto-Import Features
213
-
214
- #### Featured Image Auto-Import
215
-
216
- - Automatically detects and imports featured images using WordPress `_embed` data
217
- - Fallback support when `_embed` data is unavailable
218
- - Creates PayloadCMS media records with actual file downloads
219
- - Links imported media to posts/pages via `featuredImage` field
220
-
221
- #### Content Media Auto-Import
222
-
223
- - Scans HTML content for WordPress media URLs (`wp-content/uploads`)
224
- - Downloads and imports images referenced in post/page content
225
- - Preserves alt text and title attributes
226
- - Smart duplicate detection to avoid re-importing existing files
227
-
228
- #### MediaBlock Conversion
229
-
230
- When migrating content with `enableMediaDownload: true`, the plugin now converts HTML `<img>` tags to proper PayloadCMS MediaBlocks:
231
-
232
- - **HTML Images → MediaBlocks**: Converts inline images to structured MediaBlock components
233
- - **Media Import Integration**: Automatically downloads and links media files to MediaBlocks
234
- - **Rich Editor Support**: MediaBlocks display properly in PayloadCMS Lexical editor
235
- - **Frontend Rendering**: Works seamlessly with PayloadCMS RichText component
236
- - **Metadata Preservation**: Maintains alt text, titles, and dimensions for accessibility
237
-
238
- #### Configuration Example
239
-
240
- ```typescript
241
- collections: {
242
- posts: {
243
- wpPostType: 'post',
244
- importContentMedia: true, // Enable content media auto-import
245
- },
246
- media: {
247
- wpPostType: 'media' // Required for media storage
248
- }
249
- },
250
- enableMediaDownload: true // Required for auto-import
251
- ```
252
-
253
- ### Migration Workflow Options
254
-
255
- 1. **Recommended**: Migrate posts/pages only - media will be auto-imported
256
- 2. **Alternative**: Migrate media first, then posts/pages (avoids duplicates)
161
+ Auto-import features:
257
162
 
258
- ### Performance Considerations
163
+ - **Featured images** — detected via WordPress `_embed` data, downloaded and linked
164
+ - **Content media** — scans HTML for `wp-content/uploads` URLs, imports automatically
165
+ - **MediaBlock conversion** — HTML `<img>` tags converted to PayloadCMS MediaBlocks
259
166
 
260
- - Files processed in configurable batches to prevent memory issues
261
- - Large files streamed to minimize memory usage
262
- - Failed downloads don't stop the entire migration process
263
- - Media auto-import runs in parallel for optimal performance
264
- - Smart duplicate detection prevents re-importing existing media
167
+ **Recommended workflow:** Migrate posts/pages only media referenced in content will be auto-imported.
265
168
 
266
- ## 🔧 Advanced Configuration
169
+ ## Advanced Configuration
267
170
 
268
- ### Complete Configuration Example
171
+ ### Complete Example
269
172
 
270
173
  ```typescript
271
- import { buildConfig } from 'payload'
272
- import { payloadWordPressMigrator } from 'payload-wordpress-migrator'
273
-
274
- export default buildConfig({
275
- plugins: [
276
- payloadWordPressMigrator({
277
- // WordPress Connection
278
- wpSiteUrl: process.env.WORDPRESS_API_URL,
279
- wpUsername: process.env.WORDPRESS_USERNAME,
280
- wpPassword: process.env.WORDPRESS_APP_PASSWORD,
281
-
282
- // Collection Configuration
283
- collections: {
284
- posts: {
285
- wpPostType: 'post',
286
- enableBlocks: true,
287
- importContentMedia: true,
288
- customFields: ['_yoast_wpseo_title', '_yoast_wpseo_metadesc'],
289
- fieldMapping: {
290
- 'title.rendered': 'title',
291
- 'content.rendered': 'content',
292
- 'excerpt.rendered': 'excerpt',
293
- },
294
- },
295
- pages: {
296
- wpPostType: 'page',
297
- enableBlocks: true,
298
- importContentMedia: true,
299
- disableHtmlConversion: false, // Enable HTML to Lexical conversion
300
- },
301
- media: {
302
- wpPostType: 'media',
303
- },
304
- categories: {
305
- wpPostType: 'category',
306
- },
307
- products: {
308
- // Custom post type example
309
- wpPostType: 'product',
310
- enableBlocks: false,
311
- customFields: ['_price', '_stock_status', '_featured'],
312
- },
174
+ payloadWordPressMigrator({
175
+ wpSiteUrl: process.env.WORDPRESS_API_URL,
176
+ wpUsername: process.env.WORDPRESS_USERNAME,
177
+ wpPassword: process.env.WORDPRESS_APP_PASSWORD,
178
+
179
+ collections: {
180
+ posts: {
181
+ wpPostType: 'post',
182
+ enableBlocks: true,
183
+ importContentMedia: true,
184
+ customFields: ['_yoast_wpseo_title', '_yoast_wpseo_metadesc'],
185
+ fieldMapping: {
186
+ 'title.rendered': 'title',
187
+ 'content.rendered': 'content',
188
+ 'excerpt.rendered': 'excerpt',
313
189
  },
190
+ },
191
+ pages: { wpPostType: 'page', enableBlocks: true, importContentMedia: true },
192
+ media: { wpPostType: 'media' },
193
+ categories: { wpPostType: 'category' },
194
+ products: {
195
+ wpPostType: 'product',
196
+ customFields: ['_price', '_stock_status', '_featured'],
197
+ },
198
+ },
314
199
 
315
- // Performance & Batch Settings
316
- migrationBatchSize: 25,
317
- enableAutoSync: false,
200
+ migrationBatchSize: 25,
201
+ migrationConcurrency: 3,
202
+ enableMediaDownload: true,
203
+ maxMediaFileSize: 50 * 1024 * 1024,
204
+ allowSelfSignedCerts: process.env.NODE_ENV === 'development',
318
205
 
319
- // Media Configuration
320
- enableMediaDownload: true,
321
- maxMediaFileSize: 50 * 1024 * 1024, // 50MB
322
- allowedMediaTypes: [
323
- 'image/jpeg',
324
- 'image/png',
325
- 'image/gif',
326
- 'image/webp',
327
- 'image/svg+xml',
328
- 'application/pdf',
329
- 'video/mp4',
330
- 'video/webm',
331
- 'audio/mpeg',
332
- ],
333
-
334
- // Development Settings
335
- allowSelfSignedCerts: process.env.NODE_ENV === 'development',
336
-
337
- // Plugin Control
338
- disabled: false,
339
- disableDashboard: false,
340
- }),
341
- ],
206
+ // Restrict to admin users only
207
+ access: ({ req }) => req.user?.role === 'admin',
342
208
  })
343
209
  ```
344
210
 
345
- ### Field Mapping Configuration
211
+ ### Field Mapping
346
212
 
347
- The plugin supports sophisticated field mapping with dot notation:
213
+ Dot-notation paths are supported:
348
214
 
349
215
  ```typescript
350
216
  fieldMapping: {
351
- // WordPress field path → PayloadCMS field path
352
217
  'title.rendered': 'title',
353
218
  'content.rendered': 'content',
354
219
  'meta._yoast_wpseo_title': 'seo.title',
355
220
  'acf.hero_image': 'hero.image',
356
- 'custom_field_name': 'mappedField'
357
221
  }
358
222
  ```
359
223
 
360
- ## 🎛️ Migration Dashboard
361
-
362
- The plugin provides a comprehensive React-based dashboard accessible at `/admin/collections/wordpress-migration`:
363
-
364
- ### Dashboard Features
365
-
366
- - **Site Configuration**: Secure WordPress credentials management
367
- - **Content Discovery**: Automatic scan and analysis of available content
368
- - **Job Creation**: Visual job setup with field mapping interface
369
- - **Real-time Monitoring**: Live progress updates with success/failure metrics
370
- - **Job Controls**: Start, pause, resume, retry operations
371
- - **Error Reporting**: Detailed logs and error analysis
372
-
373
- ### Dashboard Screenshots
224
+ ## Programmatic API
374
225
 
375
- The interface includes:
376
-
377
- 1. **Configuration Panel**: WordPress site setup and testing
378
- 2. **Content Overview**: Discovered content types with item counts
379
- 3. **Job Management**: Active and completed migration jobs
380
- 4. **Progress Tracking**: Visual progress bars with detailed statistics
381
- 5. **Log Viewer**: Real-time migration logs and error reporting
382
-
383
- ## 🔌 Programmatic API
384
-
385
- The plugin exports utilities that can be used outside the dashboard for scripted or custom migrations.
226
+ The plugin exports utilities for scripted migrations outside the dashboard.
386
227
 
387
228
  ### WordPress Client
388
229
 
@@ -395,192 +236,80 @@ const client = new WordPressClient({
395
236
  wpPassword: 'xxxx xxxx xxxx xxxx',
396
237
  })
397
238
 
398
- // Discover available content types
399
239
  const contentTypes = await client.discoverContent()
400
-
401
- // Fetch fields for a specific content type
402
240
  const fields = await client.fetchContentFields('post')
403
241
  ```
404
242
 
405
- ### Content Transformation
406
-
407
- ```typescript
408
- import { transformWordPressContent } from 'payload-wordpress-migrator/dist/utils/content/index.js'
409
-
410
- const payloadData = await transformWordPressContent(
411
- wpItem, // WordPress REST API item
412
- 'post', // Content type
413
- jobConfig, // Job configuration (batchSize, enableBlocks, fieldMapping)
414
- payload, // Payload instance
415
- pluginOptions // Plugin config
416
- )
417
- ```
418
-
419
- ### HTML to Lexical Conversion
243
+ ### HTML to Lexical
420
244
 
421
245
  ```typescript
422
246
  import { convertHtmlToLexical } from 'payload-wordpress-migrator/dist/utils/lexical/index.js'
423
247
 
424
248
  const lexicalContent = convertHtmlToLexical('<p>Hello <strong>world</strong></p>')
425
- // Returns a Lexical root node ready for PayloadCMS rich text fields
426
249
  ```
427
250
 
428
- ### Field Mapping
251
+ ### Content Transformation
429
252
 
430
253
  ```typescript
431
- import { applyFieldMapping } from 'payload-wordpress-migrator/dist/utils/content/index.js'
254
+ import { transformWordPressContent } from 'payload-wordpress-migrator/dist/utils/content/index.js'
432
255
 
433
- const mappedData = await applyFieldMapping(
434
- payloadData, // Transformed Payload data
435
- wpItem, // Original WordPress item
436
- fieldMapping, // { fieldMappings: [{ sourceField, destinationField }] }
437
- payload // Payload instance (needed for relationship ID conversion)
256
+ const payloadData = await transformWordPressContent(
257
+ wpItem,
258
+ 'post',
259
+ jobConfig,
260
+ payload,
261
+ pluginOptions,
438
262
  )
439
263
  ```
440
264
 
441
- > **Note:** These utilities are imported from internal paths (`dist/utils/...`). The public package exports (`payload-wordpress-migrator`) expose only the plugin function and types. Internal APIs may change between minor versions.
442
-
443
- ## 🛠️ Development & Extension
444
-
445
- ### Plugin Structure
446
-
447
- ```
448
- src/
449
- ├── components/ # React UI components
450
- │ ├── dashboard/ # Dashboard sub-components
451
- │ │ ├── SiteConfigPanel.tsx # WordPress connection form
452
- │ │ ├── StatsOverview.tsx # Summary stat cards
453
- │ │ ├── JobsTable.tsx # Migration jobs table
454
- │ │ ├── JobActionButtons.tsx # Per-job action buttons
455
- │ │ ├── LogViewer.tsx # Combined log display
456
- │ │ ├── useMigrationDashboard.ts # State, effects, and handlers
457
- │ │ └── types.ts # Dashboard-specific types
458
- │ ├── MigrationDashboardClient.tsx # Dashboard composition shell
459
- │ ├── ContentTypeSelect.tsx # Content type picker
460
- │ ├── FieldMappingConfiguration.tsx # Field mapper
461
- │ └── SimpleFieldMapping.tsx # Simple mapping UI
462
- ├── utils/
463
- │ ├── wordpress/client.ts # WordPress REST API client
464
- │ ├── migration/orchestrator.ts # Job execution engine
465
- │ ├── content/transformer.ts # WP → Payload content transform
466
- │ ├── lexical/htmlToLexicalConverter.ts # HTML → Lexical conversion
467
- │ └── ... # Media, fields, helpers, endpoints
468
- └── index.ts # Plugin entry point
469
- ```
470
-
471
- ### Error Handling
472
-
473
- The plugin provides comprehensive error handling:
474
-
475
- - **Connection Errors**: WordPress API connectivity issues
476
- - **Authentication Errors**: Invalid credentials or permissions
477
- - **Content Errors**: Malformed content or missing fields
478
- - **Media Errors**: Download failures or file size limits
479
- - **Database Errors**: PayloadCMS creation failures
480
-
481
- ## 📊 Migration Best Practices
482
-
483
- ### Pre-Migration Checklist
265
+ > **Note:** These are internal paths (`dist/utils/...`). The public package export exposes only the plugin function and config types. Internal APIs may change between minor versions.
484
266
 
485
- 1. **✅ WordPress Setup**
486
- - Generate application password
487
- - Verify REST API access
488
- - Check user permissions
489
- - Test SSL certificate
267
+ ## Migration Best Practices
490
268
 
491
- 2. **✅ PayloadCMS Configuration**
492
- - Configure target collections
493
- - Set up field mappings
494
- - Test media collection setup
495
- - Verify storage configuration
269
+ **Recommended migration order:**
496
270
 
497
- 3. **✅ Content Analysis**
498
- - Run content discovery scan
499
- - Review field mapping options
500
- - Identify custom post types
501
- - Plan migration sequence
271
+ 1. Categories and Tags (establishes taxonomy)
272
+ 2. Media (creates media library)
273
+ 3. Pages (static content)
274
+ 4. Posts (blog content with relationships)
275
+ 5. Users (author information)
276
+ 6. Custom Post Types
502
277
 
503
- ### Migration Strategy
278
+ **Performance tips:**
504
279
 
505
- **Recommended Migration Order:**
280
+ - Start with `migrationBatchSize: 10` for testing, increase for production runs
281
+ - Use `migrationConcurrency: 3-5` for faster processing
282
+ - Use dry run mode to preview before committing
283
+ - Use resume for interrupted migrations
506
284
 
507
- 1. **Categories & Tags** (establishes taxonomy)
508
- 2. **Media Files** (creates media library)
509
- 3. **Pages** (static content first)
510
- 4. **Posts** (blog content with relationships)
511
- 5. **Users** (author information)
512
- 6. **Custom Post Types** (specialized content)
285
+ ## Troubleshooting
513
286
 
514
- **Performance Optimization:**
287
+ | Problem | Cause | Solution |
288
+ | ------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
289
+ | **"Authentication failed"** | Wrong credentials, or REST API disabled | Verify you're using an **Application Password** (not your login password). Confirm the REST API is accessible at `https://your-site.com/wp-json/` |
290
+ | **Content types not discovered** | WordPress permalinks set to "Plain" | In WordPress Admin > Settings > Permalinks, switch to any structure other than "Plain" |
291
+ | **Custom post types not found** | CPT not exposed to REST API | Add `'show_in_rest' => true` to your `register_post_type()` call |
292
+ | **SSL certificate errors** | Self-signed or expired certificate | Set `allowSelfSignedCerts: true` for development. In production, use a valid certificate |
293
+ | **Media download fails** | WordPress behind CDN or firewall | Ensure media file URLs (`wp-content/uploads/...`) are accessible from the server running Payload |
294
+ | **`"The following path cannot be queried: migratedFromWordPress"`** | Target collection not in plugin config | Add the collection to the `collections` option so the metadata fields get injected |
295
+ | **Large migration runs out of memory** | Batch size too high for available RAM | Reduce `migrationBatchSize` (try 5) and `migrationConcurrency` (set to 1) |
296
+ | **Lexical content looks wrong** | HTML conversion edge case | Set `disableHtmlConversion: true` on the collection mapping as a workaround, and report the specific HTML that fails |
297
+ | **Duplicate items after resume** | `migratedFromWordPress.wpPostId` field missing | Ensure the collection is listed in the plugin `collections` config — the field is auto-added |
298
+ | **ACF fields not migrated** | Field mapping not configured | Add ACF field names to `customFields` in the collection mapping, or configure explicit `fieldMapping` in the job |
299
+ | **`"MIME type not allowed"` or `"File too large"`** | Media file exceeds limits | Expand `allowedMediaTypes` and increase `maxMediaFileSize` in plugin config |
515
300
 
516
- - Start with smaller batch sizes (10-25) for initial testing
517
- - Increase batch size (50-100) for production migrations
518
- - Monitor memory usage during large media migrations
519
- - Use resume functionality for interrupted migrations
301
+ ## Requirements
520
302
 
521
- ### Troubleshooting
303
+ - Node.js ^18.20.2 || >=20.9.0
304
+ - PayloadCMS ^3.29.0
305
+ - WordPress 5.6+ with REST API enabled
522
306
 
523
- | Problem | Cause | Solution |
524
- |---------|-------|----------|
525
- | **"Authentication failed"** | Wrong credentials, or REST API disabled | Verify you're using an **Application Password** (not your login password). Confirm the REST API is accessible at `https://your-site.com/wp-json/` |
526
- | **Content types not discovered** | WordPress permalinks set to "Plain" | In WordPress Admin > Settings > Permalinks, switch to any structure other than "Plain" |
527
- | **Custom post types not found** | CPT not exposed to REST API | Add `'show_in_rest' => true` to your `register_post_type()` call |
528
- | **SSL certificate errors** | Self-signed or expired certificate | Set `allowSelfSignedCerts: true` for development. In production, use a valid certificate |
529
- | **Media download fails** | WordPress behind CDN or firewall | Ensure media file URLs (`wp-content/uploads/...`) are accessible from the server running Payload |
530
- | **`"The following path cannot be queried: migratedFromWordPress"`** | Target collection not in plugin config | Add the collection to the `collections` option so the metadata fields get injected |
531
- | **Large migration runs out of memory** | Batch size too high for available RAM | Reduce `migrationBatchSize` (try 5) and `migrationConcurrency` (set to 1) |
532
- | **Lexical content looks wrong** | HTML conversion edge case | Set `disableHtmlConversion: true` on the collection mapping as a workaround, and report the specific HTML that fails |
533
- | **Duplicate items after resume** | `migratedFromWordPress.wpPostId` field missing | Ensure the collection is listed in the plugin `collections` config — the field is auto-added |
534
- | **ACF fields not migrated** | Field mapping not configured | Add ACF field names to `customFields` in the collection mapping, or configure explicit `fieldMapping` in the job |
535
- | **`"MIME type not allowed"` or `"File too large"`** | Media file exceeds limits | Expand `allowedMediaTypes` and increase `maxMediaFileSize` in plugin config |
307
+ ## License
536
308
 
537
- ## 🔧 Technical Requirements
538
-
539
- - **Node.js**: ^18.20.2 || >=20.9.0
540
- - **PayloadCMS**: ^3.29.0
541
- - **Package Manager**: pnpm ^9 || ^10
542
- - **WordPress**: 5.6+ with REST API enabled
543
- - **Memory**: 512MB+ recommended for media migrations
544
- - **Storage**: Adequate space for media file imports
545
-
546
- ## 📈 Performance Considerations
547
-
548
- - **Batch Processing**: Configurable batch sizes prevent memory overflow
549
- - **Background Jobs**: Non-blocking migration execution
550
- - **Progress Persistence**: Resume interrupted migrations
551
- - **Memory Management**: Efficient file streaming for large media
552
- - **Cache Management**: Intelligent caching with invalidation
553
- - **Parallel Processing**: Concurrent media imports for optimal speed
554
-
555
- ## 🤝 Contributing
556
-
557
- We welcome contributions! Areas for enhancement:
558
-
559
- - Additional content type support
560
- - Enhanced field mapping UI
561
- - Performance optimizations
562
- - Advanced content transformations
563
- - Integration tests
564
- - Documentation improvements
565
-
566
- ## 📝 License
567
-
568
- MIT License - see [LICENSE](LICENSE) file for details.
569
-
570
- ## 🎯 Roadmap
571
-
572
- - [ ] WordPress multisite support
573
- - [ ] Advanced Custom Fields Pro support
574
- - [ ] WooCommerce product migration
575
- - [ ] Automated testing suite
576
- - [ ] Docker development environment
577
- - [ ] Migration templates and presets
578
- - [ ] Webhook integration for real-time sync
309
+ MIT see [LICENSE](LICENSE) for details.
579
310
 
580
311
  ---
581
312
 
582
- **Author**: [Igor Abdulovic](mailto:igor.abdulovic@brightscout.com) at [Brightscout](https://brightscout.com)
583
-
584
- **Repository**: [GitHub](https://github.com/Brightscout/payload-wordpress-migrator)
313
+ **Author:** [Igor Abdulovic](mailto:igor.abdulovic@brightscout.com) at [Brightscout](https://brightscout.com)
585
314
 
586
- **Issues**: [GitHub Issues](https://github.com/Brightscout/payload-wordpress-migrator/issues)
315
+ **Repository:** [GitHub](https://github.com/Brightscout/payload-wordpress-migrator)
@@ -63,22 +63,14 @@ const FieldMappingConfiguration = ({ path })=>{
63
63
  });
64
64
  return flattened;
65
65
  };
66
- // Fetch WordPress content type fields
66
+ // Fetch WordPress content type fields (credentials resolved server-side from plugin config)
67
67
  const fetchWordPressFields = useCallback(async (contentType)=>{
68
68
  try {
69
69
  setLoading(true);
70
70
  setError('');
71
- const savedConfig = localStorage.getItem('wp-site-config');
72
- if (!savedConfig) {
73
- throw new Error('WordPress site configuration not found');
74
- }
75
- const config = JSON.parse(savedConfig);
76
71
  const response = await fetch('/api/wordpress/content-fields', {
77
72
  body: JSON.stringify({
78
- contentType,
79
- wpPassword: config.wpPassword,
80
- wpSiteUrl: config.wpSiteUrl,
81
- wpUsername: config.wpUsername
73
+ contentType
82
74
  }),
83
75
  headers: {
84
76
  'Content-Type': 'application/json'
@@ -1 +1 @@
1
- {"version":3,"file":"FieldMappingConfiguration.js","sources":["../../src/components/FieldMappingConfiguration.tsx"],"sourcesContent":["'use client'\n\nimport { useField } from '@payloadcms/ui'\nimport React, { useCallback, useEffect, useState } from 'react'\n\nimport styles from './FieldMappingConfiguration.module.css'\n\ntype WordPressField = {\n label: string\n name: string\n nested?: WordPressField[]\n path: string // Full dot notation path like \"meta.custom_field\"\n type: string\n}\n\ntype PayloadField = {\n label: string\n name: string\n nested?: PayloadField[]\n path: string // Full dot notation path\n type: string\n}\n\ntype FieldMapping = {\n payloadField: string\n wpField: string\n}\n\nconst FieldMappingConfiguration: React.FC<{ path: string }> = ({ path }) => {\n const { setValue, value } = useField({ path })\n const { value: contentTypeValue } = useField({ path: 'contentType' })\n const { value: targetCollectionValue } = useField({ path: 'targetCollection' })\n\n const [wpFields, setWpFields] = useState<WordPressField[]>([])\n const [payloadFields, setPayloadFields] = useState<PayloadField[]>([])\n const [fieldMappings, setFieldMappings] = useState<FieldMapping[]>([])\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState('')\n\n // Parse existing field mappings from JSON value\n useEffect(() => {\n if (value) {\n try {\n const parsed = typeof value === 'string' ? JSON.parse(value) : value\n if (parsed && Array.isArray(parsed.fieldMappings)) {\n setFieldMappings(parsed.fieldMappings)\n }\n } catch (error) {\n console.error('Failed to parse existing field mappings:', error)\n }\n }\n }, [value])\n\n // Flatten nested fields with dot notation paths\n const flattenFields = (fields: any[], prefix = ''): any[] => {\n const flattened: any[] = []\n\n fields.forEach((field) => {\n const fieldPath = prefix ? `${prefix}.${field.name}` : field.name\n\n flattened.push({\n ...field,\n label: field.label || field.name,\n path: fieldPath,\n })\n\n // Handle nested fields\n if (field.fields && Array.isArray(field.fields)) {\n flattened.push(...flattenFields(field.fields, fieldPath))\n }\n\n // Handle array fields with nested schemas\n if (field.type === 'array' && field.of && Array.isArray(field.of)) {\n field.of.forEach((arrayField: any, index: number) => {\n if (arrayField.fields) {\n flattened.push(...flattenFields(arrayField.fields, `${fieldPath}[${index}]`))\n }\n })\n }\n\n // Handle group fields\n if (field.type === 'group' && field.fields) {\n flattened.push(...flattenFields(field.fields, fieldPath))\n }\n })\n\n return flattened\n }\n\n // Fetch WordPress content type fields\n const fetchWordPressFields = useCallback(async (contentType: string) => {\n try {\n setLoading(true)\n setError('')\n\n const savedConfig = localStorage.getItem('wp-site-config')\n if (!savedConfig) {\n throw new Error('WordPress site configuration not found')\n }\n\n const config = JSON.parse(savedConfig)\n\n const response = await fetch('/api/wordpress/content-fields', {\n body: JSON.stringify({\n contentType,\n wpPassword: config.wpPassword,\n wpSiteUrl: config.wpSiteUrl,\n wpUsername: config.wpUsername,\n }),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n const result = await response.json()\n\n if (response.ok && result.success) {\n const flattened = flattenFields(result.fields || [])\n setWpFields(flattened)\n } else {\n throw new Error(result.error || 'Failed to fetch WordPress fields')\n }\n } catch (error) {\n console.error('Error fetching WordPress fields:', error)\n setError(error instanceof Error ? error.message : 'Failed to fetch WordPress fields')\n setWpFields([])\n } finally {\n setLoading(false)\n }\n }, []) // Empty dependency array since the function doesn't depend on any props or state\n\n // Fetch Payload collection fields\n const fetchPayloadFields = useCallback(async (collectionSlug: string) => {\n try {\n const response = await fetch(`/api/collections/${collectionSlug}/fields`)\n const result = await response.json()\n\n if (response.ok) {\n const flattened = flattenFields(result.fields || [])\n setPayloadFields(flattened)\n } else {\n throw new Error('Failed to fetch Payload collection fields')\n }\n } catch (error) {\n console.error('Error fetching Payload fields:', error)\n setPayloadFields([])\n }\n }, []) // Empty dependency array since the function doesn't depend on any props or state\n\n // Load fields when both content type and target collection are selected\n useEffect(() => {\n if (contentTypeValue && targetCollectionValue) {\n void fetchWordPressFields(contentTypeValue as string)\n void fetchPayloadFields(targetCollectionValue as string)\n } else {\n setWpFields([])\n setPayloadFields([])\n setFieldMappings([])\n }\n }, [contentTypeValue, targetCollectionValue, fetchWordPressFields, fetchPayloadFields])\n\n // Add new field mapping\n const addFieldMapping = () => {\n const newMapping: FieldMapping = {\n payloadField: '',\n wpField: '',\n }\n const updatedMappings = [...fieldMappings, newMapping]\n setFieldMappings(updatedMappings)\n updateValue(updatedMappings)\n }\n\n // Remove field mapping\n const removeFieldMapping = (index: number) => {\n const updatedMappings = fieldMappings.filter((_, i) => i !== index)\n setFieldMappings(updatedMappings)\n updateValue(updatedMappings)\n }\n\n // Update field mapping\n const updateFieldMapping = (index: number, field: keyof FieldMapping, value: string) => {\n const updatedMappings = fieldMappings.map((mapping, i) =>\n i === index ? { ...mapping, [field]: value } : mapping,\n )\n setFieldMappings(updatedMappings)\n updateValue(updatedMappings)\n }\n\n // Update the form value\n const updateValue = (mappings: FieldMapping[]) => {\n const mappingConfig = {\n contentType: contentTypeValue,\n fieldMappings: mappings,\n targetCollection: targetCollectionValue,\n }\n setValue(JSON.stringify(mappingConfig, null, 2))\n }\n\n if (!contentTypeValue || !targetCollectionValue) {\n return (\n <div className=\"field-type\">\n <div className=\"field-label\">\n <label>Field Mapping Configuration</label>\n </div>\n <div className=\"field-description\">\n Please select both \"Content Type to Migrate\" and \"Target Payload Collection\" to configure\n field mappings.\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"field-type\">\n <div className=\"field-label\">\n <label>Field Mapping Configuration</label>\n </div>\n\n {loading && <div className=\"field-description\">Loading field schemas...</div>}\n\n {error && (\n <div className=\"field-error\">\n <div className=\"field-error__message\">{error}</div>\n </div>\n )}\n\n {!loading && !error && (wpFields.length > 0 || payloadFields.length > 0) && (\n <div className={styles.fieldMappingContainer}>\n <div className={styles.fieldMappingHeader}>\n <div className={styles.fieldMappingColumns}>\n <div className={styles.fieldMappingColumn}>\n <h4>WordPress Fields ({String(contentTypeValue || '')})</h4>\n <div className={styles.fieldList}>\n {wpFields.map((field) => (\n <div className={styles.fieldItem} key={field.path}>\n <strong>{field.path}</strong>\n <span className={styles.fieldTypeBadge}>{field.type}</span>\n </div>\n ))}\n </div>\n </div>\n\n <div className={styles.fieldMappingColumn}>\n <h4>Payload Fields ({String(targetCollectionValue || '')})</h4>\n <div className={styles.fieldList}>\n {payloadFields.map((field) => (\n <div className={styles.fieldItem} key={field.path}>\n <strong>{field.path}</strong>\n <span className={styles.fieldTypeBadge}>{field.type}</span>\n </div>\n ))}\n </div>\n </div>\n </div>\n </div>\n\n <div className={styles.fieldMappingMappings}>\n <div className={styles.fieldMappingHeader}>\n <h4>Field Mappings</h4>\n <button\n className=\"btn btn--style-secondary btn--size-small\"\n onClick={addFieldMapping}\n type=\"button\"\n >\n Add Mapping\n </button>\n </div>\n\n {fieldMappings.map((mapping, index) => (\n <div className={styles.fieldMappingRow} key={index}>\n <select\n className=\"select\"\n onChange={(e) => updateFieldMapping(index, 'wpField', e.target.value)}\n value={mapping.wpField}\n >\n <option value=\"\">Select WordPress field...</option>\n {wpFields.map((field) => (\n <option key={field.path} value={field.path}>\n {field.path} ({field.type})\n </option>\n ))}\n </select>\n\n <span className={styles.fieldMappingArrow}>→</span>\n\n <select\n className=\"select\"\n onChange={(e) => updateFieldMapping(index, 'payloadField', e.target.value)}\n value={mapping.payloadField}\n >\n <option value=\"\">Select Payload field...</option>\n {payloadFields.map((field) => (\n <option key={field.path} value={field.path}>\n {field.path} ({field.type})\n </option>\n ))}\n </select>\n\n <button\n className=\"btn btn--style-error btn--size-small\"\n onClick={() => removeFieldMapping(index)}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n\n {fieldMappings.length === 0 && (\n <div className={styles.fieldDescription}>\n No field mappings configured. Click \"Add Mapping\" to start mapping fields.\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n )\n}\n\nexport default FieldMappingConfiguration\n"],"names":["path","setFieldMappings","value","flattened","label","field","contentType","wpPassword","wpSiteUrl","wpUsername","setError","setWpFields","setPayloadFields","contentTypeValue","targetCollectionValue","fetchWordPressFields","fetchPayloadFields","fieldMappings","newMapping","setValue","loading","error","className","String","wpFields","payloadFields","onClick","index"],"mappings":";;;;;;AA4BA;AACE;AAAuCA;AAAK;AAC5C;;AAAmE;AACnE;;AAA6E;AAE7E;AACA;AACA;AACA;AACA;;;AAIE;;AAEI;AACA;AACEC;AACF;AACF;;AAEA;AACF;;AACEC;AAAM;;AAGV;AACE;;;AAKEC;AACE;AACAC;;AAEF;;;AAIED;AACF;;AAGA;AACEE;;AAEIF;AACF;AACF;AACF;;AAGA;AACEA;AACF;AACF;;AAGF;;;;;;;AASI;AACE;AACF;;;;AAMIG;AACAC;AACAC;AACAC;AACF;;;AAGA;;AAEF;;AAIA;AACE;;;AAGA;AACF;AACF;;AAEEC;AACAC;;;AAGF;;;;;;;;;AAUI;;;AAGA;AACF;AACF;;AAEEC;AACF;;;;;AAKA;AACE;AACA;;AAEAD;AACAC;AACAX;AACF;;AACEY;AAAkBC;AAAuBC;AAAsBC;AAAmB;;AAGtF;AACE;;;AAGA;AACA;AAA4BC;AAAeC;AAAW;;;AAGxD;;AAGA;AACE;;;AAGF;;;;AAKoB;AAAY;;;;AAIhC;;AAGA;AACE;;;;AAIA;AACAC;AACF;;AAGE;;;;;AAGM;AAAO;;;;;AAE0B;;;;AAMzC;AAEA;;;;;AAGM;AAAO;;;AAGRC;;AAA8C;;AAE9CC;;AAEG;;AAAuCA;;;AAI1C;AACMC;;;AACEA;AACH;AAAKA;;;AACEA;;;;AACC;AAAmBC;AAA+B;;;;AACjDD;AACFE;AACMF;;;AACMjB;;;AACHiB;AAAmCjB;;;AAFJA;;;;;AAQxCiB;;;;AACC;AAAiBC;AAAoC;;;;AACpDD;AACFG;AACMH;;;AACMjB;;;AACHiB;AAAmCjB;;;AAFJA;;;;;;;;AAU5CiB;;;AACEA;;;AACC;;;;;;AAKH;;;;AAKFL;AACMK;;;;;AAIDpB;;;;AAEiB;;AAChBsB;AAC0BtB;;AACtBG;AAAW;AAAGA;AAAW;;AADfA;;;;AAMXiB;AAAqC;;;;;AAKzCpB;;;;AAEiB;;AAChBuB;AAC0BvB;;AACtBG;AAAW;AAAGA;AAAW;;AADfA;;;;;AAQfqB;;AAED;;;AAjC0CC;;AAwCxCL;AAAoC;;;;;;;;AASvD;;"}
1
+ {"version":3,"file":"FieldMappingConfiguration.js","sources":["../../src/components/FieldMappingConfiguration.tsx"],"sourcesContent":["'use client'\n\nimport { useField } from '@payloadcms/ui'\nimport React, { useCallback, useEffect, useState } from 'react'\n\nimport styles from './FieldMappingConfiguration.module.css'\n\ntype WordPressField = {\n label: string\n name: string\n nested?: WordPressField[]\n path: string // Full dot notation path like \"meta.custom_field\"\n type: string\n}\n\ntype PayloadField = {\n label: string\n name: string\n nested?: PayloadField[]\n path: string // Full dot notation path\n type: string\n}\n\ntype FieldMapping = {\n payloadField: string\n wpField: string\n}\n\nconst FieldMappingConfiguration: React.FC<{ path: string }> = ({ path }) => {\n const { setValue, value } = useField({ path })\n const { value: contentTypeValue } = useField({ path: 'contentType' })\n const { value: targetCollectionValue } = useField({ path: 'targetCollection' })\n\n const [wpFields, setWpFields] = useState<WordPressField[]>([])\n const [payloadFields, setPayloadFields] = useState<PayloadField[]>([])\n const [fieldMappings, setFieldMappings] = useState<FieldMapping[]>([])\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState('')\n\n // Parse existing field mappings from JSON value\n useEffect(() => {\n if (value) {\n try {\n const parsed = typeof value === 'string' ? JSON.parse(value) : value\n if (parsed && Array.isArray(parsed.fieldMappings)) {\n setFieldMappings(parsed.fieldMappings)\n }\n } catch (error) {\n console.error('Failed to parse existing field mappings:', error)\n }\n }\n }, [value])\n\n // Flatten nested fields with dot notation paths\n const flattenFields = (fields: any[], prefix = ''): any[] => {\n const flattened: any[] = []\n\n fields.forEach((field) => {\n const fieldPath = prefix ? `${prefix}.${field.name}` : field.name\n\n flattened.push({\n ...field,\n label: field.label || field.name,\n path: fieldPath,\n })\n\n // Handle nested fields\n if (field.fields && Array.isArray(field.fields)) {\n flattened.push(...flattenFields(field.fields, fieldPath))\n }\n\n // Handle array fields with nested schemas\n if (field.type === 'array' && field.of && Array.isArray(field.of)) {\n field.of.forEach((arrayField: any, index: number) => {\n if (arrayField.fields) {\n flattened.push(...flattenFields(arrayField.fields, `${fieldPath}[${index}]`))\n }\n })\n }\n\n // Handle group fields\n if (field.type === 'group' && field.fields) {\n flattened.push(...flattenFields(field.fields, fieldPath))\n }\n })\n\n return flattened\n }\n\n // Fetch WordPress content type fields (credentials resolved server-side from plugin config)\n const fetchWordPressFields = useCallback(async (contentType: string) => {\n try {\n setLoading(true)\n setError('')\n\n const response = await fetch('/api/wordpress/content-fields', {\n body: JSON.stringify({ contentType }),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n const result = await response.json()\n\n if (response.ok && result.success) {\n const flattened = flattenFields(result.fields || [])\n setWpFields(flattened)\n } else {\n throw new Error(result.error || 'Failed to fetch WordPress fields')\n }\n } catch (error) {\n console.error('Error fetching WordPress fields:', error)\n setError(error instanceof Error ? error.message : 'Failed to fetch WordPress fields')\n setWpFields([])\n } finally {\n setLoading(false)\n }\n }, []) // Empty dependency array since the function doesn't depend on any props or state\n\n // Fetch Payload collection fields\n const fetchPayloadFields = useCallback(async (collectionSlug: string) => {\n try {\n const response = await fetch(`/api/collections/${collectionSlug}/fields`)\n const result = await response.json()\n\n if (response.ok) {\n const flattened = flattenFields(result.fields || [])\n setPayloadFields(flattened)\n } else {\n throw new Error('Failed to fetch Payload collection fields')\n }\n } catch (error) {\n console.error('Error fetching Payload fields:', error)\n setPayloadFields([])\n }\n }, []) // Empty dependency array since the function doesn't depend on any props or state\n\n // Load fields when both content type and target collection are selected\n useEffect(() => {\n if (contentTypeValue && targetCollectionValue) {\n void fetchWordPressFields(contentTypeValue as string)\n void fetchPayloadFields(targetCollectionValue as string)\n } else {\n setWpFields([])\n setPayloadFields([])\n setFieldMappings([])\n }\n }, [contentTypeValue, targetCollectionValue, fetchWordPressFields, fetchPayloadFields])\n\n // Add new field mapping\n const addFieldMapping = () => {\n const newMapping: FieldMapping = {\n payloadField: '',\n wpField: '',\n }\n const updatedMappings = [...fieldMappings, newMapping]\n setFieldMappings(updatedMappings)\n updateValue(updatedMappings)\n }\n\n // Remove field mapping\n const removeFieldMapping = (index: number) => {\n const updatedMappings = fieldMappings.filter((_, i) => i !== index)\n setFieldMappings(updatedMappings)\n updateValue(updatedMappings)\n }\n\n // Update field mapping\n const updateFieldMapping = (index: number, field: keyof FieldMapping, value: string) => {\n const updatedMappings = fieldMappings.map((mapping, i) =>\n i === index ? { ...mapping, [field]: value } : mapping,\n )\n setFieldMappings(updatedMappings)\n updateValue(updatedMappings)\n }\n\n // Update the form value\n const updateValue = (mappings: FieldMapping[]) => {\n const mappingConfig = {\n contentType: contentTypeValue,\n fieldMappings: mappings,\n targetCollection: targetCollectionValue,\n }\n setValue(JSON.stringify(mappingConfig, null, 2))\n }\n\n if (!contentTypeValue || !targetCollectionValue) {\n return (\n <div className=\"field-type\">\n <div className=\"field-label\">\n <label>Field Mapping Configuration</label>\n </div>\n <div className=\"field-description\">\n Please select both \"Content Type to Migrate\" and \"Target Payload Collection\" to configure\n field mappings.\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"field-type\">\n <div className=\"field-label\">\n <label>Field Mapping Configuration</label>\n </div>\n\n {loading && <div className=\"field-description\">Loading field schemas...</div>}\n\n {error && (\n <div className=\"field-error\">\n <div className=\"field-error__message\">{error}</div>\n </div>\n )}\n\n {!loading && !error && (wpFields.length > 0 || payloadFields.length > 0) && (\n <div className={styles.fieldMappingContainer}>\n <div className={styles.fieldMappingHeader}>\n <div className={styles.fieldMappingColumns}>\n <div className={styles.fieldMappingColumn}>\n <h4>WordPress Fields ({String(contentTypeValue || '')})</h4>\n <div className={styles.fieldList}>\n {wpFields.map((field) => (\n <div className={styles.fieldItem} key={field.path}>\n <strong>{field.path}</strong>\n <span className={styles.fieldTypeBadge}>{field.type}</span>\n </div>\n ))}\n </div>\n </div>\n\n <div className={styles.fieldMappingColumn}>\n <h4>Payload Fields ({String(targetCollectionValue || '')})</h4>\n <div className={styles.fieldList}>\n {payloadFields.map((field) => (\n <div className={styles.fieldItem} key={field.path}>\n <strong>{field.path}</strong>\n <span className={styles.fieldTypeBadge}>{field.type}</span>\n </div>\n ))}\n </div>\n </div>\n </div>\n </div>\n\n <div className={styles.fieldMappingMappings}>\n <div className={styles.fieldMappingHeader}>\n <h4>Field Mappings</h4>\n <button\n className=\"btn btn--style-secondary btn--size-small\"\n onClick={addFieldMapping}\n type=\"button\"\n >\n Add Mapping\n </button>\n </div>\n\n {fieldMappings.map((mapping, index) => (\n <div className={styles.fieldMappingRow} key={index}>\n <select\n className=\"select\"\n onChange={(e) => updateFieldMapping(index, 'wpField', e.target.value)}\n value={mapping.wpField}\n >\n <option value=\"\">Select WordPress field...</option>\n {wpFields.map((field) => (\n <option key={field.path} value={field.path}>\n {field.path} ({field.type})\n </option>\n ))}\n </select>\n\n <span className={styles.fieldMappingArrow}>→</span>\n\n <select\n className=\"select\"\n onChange={(e) => updateFieldMapping(index, 'payloadField', e.target.value)}\n value={mapping.payloadField}\n >\n <option value=\"\">Select Payload field...</option>\n {payloadFields.map((field) => (\n <option key={field.path} value={field.path}>\n {field.path} ({field.type})\n </option>\n ))}\n </select>\n\n <button\n className=\"btn btn--style-error btn--size-small\"\n onClick={() => removeFieldMapping(index)}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n\n {fieldMappings.length === 0 && (\n <div className={styles.fieldDescription}>\n No field mappings configured. Click \"Add Mapping\" to start mapping fields.\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n )\n}\n\nexport default FieldMappingConfiguration\n"],"names":["path","setFieldMappings","value","flattened","label","field","contentType","setError","setWpFields","setPayloadFields","contentTypeValue","targetCollectionValue","fetchWordPressFields","fetchPayloadFields","fieldMappings","newMapping","setValue","loading","error","className","String","wpFields","payloadFields","onClick","index"],"mappings":";;;;;;AA4BA;AACE;AAAuCA;AAAK;AAC5C;;AAAmE;AACnE;;AAA6E;AAE7E;AACA;AACA;AACA;AACA;;;AAIE;;AAEI;AACA;AACEC;AACF;AACF;;AAEA;AACF;;AACEC;AAAM;;AAGV;AACE;;;AAKEC;AACE;AACAC;;AAEF;;;AAIED;AACF;;AAGA;AACEE;;AAEIF;AACF;AACF;AACF;;AAGA;AACEA;AACF;AACF;;AAGF;;;;;;;;AAS6BG;AAAY;;;AAGnC;;AAEF;;AAIA;AACE;;;AAGA;AACF;AACF;;AAEEC;AACAC;;;AAGF;;;;;;;;;AAUI;;;AAGA;AACF;AACF;;AAEEC;AACF;;;;;AAKA;AACE;AACA;;AAEAD;AACAC;AACAR;AACF;;AACES;AAAkBC;AAAuBC;AAAsBC;AAAmB;;AAGtF;AACE;;;AAGA;AACA;AAA4BC;AAAeC;AAAW;;;AAGxD;;AAGA;AACE;;;AAGF;;;;AAKoB;AAAY;;;;AAIhC;;AAGA;AACE;;;;AAIA;AACAC;AACF;;AAGE;;;;;AAGM;AAAO;;;;;AAE0B;;;;AAMzC;AAEA;;;;;AAGM;AAAO;;;AAGRC;;AAA8C;;AAE9CC;;AAEG;;AAAuCA;;;AAI1C;AACMC;;;AACEA;AACH;AAAKA;;;AACEA;;;;AACC;AAAmBC;AAA+B;;;;AACjDD;AACFE;AACMF;;;AACMd;;;AACHc;AAAmCd;;;AAFJA;;;;;AAQxCc;;;;AACC;AAAiBC;AAAoC;;;;AACpDD;AACFG;AACMH;;;AACMd;;;AACHc;AAAmCd;;;AAFJA;;;;;;;;AAU5Cc;;;AACEA;;;AACC;;;;;;AAKH;;;;AAKFL;AACMK;;;;;AAIDjB;;;;AAEiB;;AAChBmB;AAC0BnB;;AACtBG;AAAW;AAAGA;AAAW;;AADfA;;;;AAMXc;AAAqC;;;;;AAKzCjB;;;;AAEiB;;AAChBoB;AAC0BpB;;AACtBG;AAAW;AAAGA;AAAW;;AADfA;;;;;AAQfkB;;AAED;;;AAjC0CC;;AAwCxCL;AAAoC;;;;;;;;AASvD;;"}
@@ -27,24 +27,15 @@ const SimpleFieldMapping = ({ path })=>{
27
27
  ]);
28
28
  const [loading, setLoading] = useState(false);
29
29
  const [error, setError] = useState('');
30
- // Fetch WordPress content fields
30
+ // Fetch WordPress content fields (credentials resolved server-side from plugin config)
31
31
  const fetchWordPressFields = async (contentType)=>{
32
32
  try {
33
33
  setLoading(true);
34
34
  setError('');
35
- const savedConfig = localStorage.getItem('wp-site-config');
36
- if (!savedConfig) {
37
- throw new Error('WordPress site configuration not found');
38
- }
39
- const config = JSON.parse(savedConfig);
40
- const requestBody = {
41
- contentType,
42
- wpPassword: config.wpPassword,
43
- wpSiteUrl: config.wpSiteUrl,
44
- wpUsername: config.wpUsername
45
- };
46
35
  const response = await fetch('/api/wordpress/content-fields', {
47
- body: JSON.stringify(requestBody),
36
+ body: JSON.stringify({
37
+ contentType
38
+ }),
48
39
  headers: {
49
40
  'Content-Type': 'application/json'
50
41
  },
@@ -1 +1 @@
1
- {"version":3,"file":"SimpleFieldMapping.js","sources":["../../src/components/SimpleFieldMapping.tsx"],"sourcesContent":["'use client'\n\nimport { useField } from '@payloadcms/ui'\nimport React, { useEffect, useRef, useState } from 'react'\n\ntype FieldMapping = {\n destinationField: string\n sourceField: string\n}\n\ntype Option = {\n label: string\n path?: string\n type?: string\n value: string\n}\n\nconst SimpleFieldMapping: React.FC<{ path: string }> = ({ path }) => {\n const { setValue, value } = useField({ path })\n const { value: contentTypeValue } = useField({ path: 'contentType' })\n const { value: targetCollectionValue } = useField({ path: 'targetCollection' })\n\n // Use ref to prevent infinite loops - change to any type to handle different value types\n const isUpdatingRef = useRef(false)\n const lastValueRef = useRef<any>('')\n\n // Dynamic field options from API\n const [sourceFields, setSourceFields] = useState<Option[]>([])\n const [destinationFields, setDestinationFields] = useState<Option[]>([])\n const [fieldMappings, setFieldMappings] = useState<FieldMapping[]>([\n { destinationField: '', sourceField: '' },\n ])\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState('')\n\n // Fetch WordPress content fields\n const fetchWordPressFields = async (contentType: string) => {\n try {\n setLoading(true)\n setError('')\n\n const savedConfig = localStorage.getItem('wp-site-config')\n if (!savedConfig) {\n throw new Error('WordPress site configuration not found')\n }\n\n const config = JSON.parse(savedConfig)\n\n const requestBody = {\n contentType,\n wpPassword: config.wpPassword,\n wpSiteUrl: config.wpSiteUrl,\n wpUsername: config.wpUsername,\n }\n\n const response = await fetch('/api/wordpress/content-fields', {\n body: JSON.stringify(requestBody),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n const result = await response.json()\n\n if (response.ok && result.success) {\n const wpFieldOptions = (result.fields || []).map((field: any) => ({\n type: field.type,\n label: `${field.label} (${field.type})${field.custom ? ' *' : ''}`,\n path: field.path || field.name,\n value: field.path || field.name,\n }))\n setSourceFields(wpFieldOptions)\n } else {\n console.error('API returned error:', result)\n throw new Error(result.error || 'Failed to fetch WordPress fields')\n }\n } catch (error) {\n console.error('Error fetching WordPress fields:', error)\n setError(error instanceof Error ? error.message : 'Failed to fetch WordPress fields')\n setSourceFields([])\n }\n }\n\n // Fetch PayloadCMS collection fields\n const fetchPayloadFields = async (collectionSlug: string) => {\n try {\n const response = await fetch(`/api/collections/${collectionSlug}/fields`)\n\n const result = await response.json()\n\n if (response.ok && result.success) {\n const payloadFieldOptions = (result.fields || []).map((field: any) => ({\n type: field.type,\n label: `${field.label} (${field.type})`,\n path: field.path || field.name,\n value: field.path || field.name,\n }))\n setDestinationFields(payloadFieldOptions)\n } else {\n console.error('Payload API returned error:', result)\n throw new Error(result.error || 'Failed to fetch Payload collection fields')\n }\n } catch (error) {\n console.error('Error fetching Payload fields:', error)\n setDestinationFields([])\n }\n }\n\n // Fetch fields when both content type and target collection are selected\n useEffect(() => {\n const fetchFields = async () => {\n if (contentTypeValue && targetCollectionValue) {\n setLoading(true)\n try {\n await Promise.all([\n fetchWordPressFields(contentTypeValue as string),\n fetchPayloadFields(targetCollectionValue as string),\n ])\n } catch (error) {\n console.error('Error fetching fields:', error)\n } finally {\n setLoading(false)\n }\n } else {\n setSourceFields([])\n setDestinationFields([])\n setFieldMappings([{ destinationField: '', sourceField: '' }])\n setError('')\n }\n }\n\n void fetchFields()\n }, [contentTypeValue, targetCollectionValue])\n\n // Parse existing field mappings from JSON value - only when value actually changes\n useEffect(() => {\n if (isUpdatingRef.current) {\n return\n } // Prevent processing our own updates\n\n // Convert value to string for comparison\n const currentValue = typeof value === 'string' ? value : JSON.stringify(value || '')\n\n if (value && currentValue !== lastValueRef.current) {\n try {\n const parsed = typeof value === 'string' ? JSON.parse(value) : value\n if (parsed && Array.isArray(parsed.fieldMappings)) {\n setFieldMappings(parsed.fieldMappings)\n }\n lastValueRef.current = currentValue\n } catch (error) {\n console.warn('Failed to parse field mappings:', error)\n }\n }\n }, [value])\n\n // Function to update parent value without causing loops\n const updateParentValue = (mappings: FieldMapping[]) => {\n const newValue = JSON.stringify({\n contentType: contentTypeValue,\n fieldMappings: mappings,\n targetCollection: targetCollectionValue,\n })\n\n // Prevent infinite loops\n if (newValue !== lastValueRef.current && !isUpdatingRef.current) {\n isUpdatingRef.current = true\n setValue(newValue)\n lastValueRef.current = newValue\n\n // Reset the flag after a short delay\n setTimeout(() => {\n isUpdatingRef.current = false\n }, 100)\n }\n }\n\n // Add new mapping row\n const handleAddMapping = () => {\n const newMappings = [...fieldMappings, { destinationField: '', sourceField: '' }]\n setFieldMappings(newMappings)\n updateParentValue(newMappings)\n }\n\n // Remove mapping row\n const handleRemoveMapping = (idx: number) => {\n if (fieldMappings.length <= 1) {\n return\n } // Keep at least one mapping\n\n const newMappings = fieldMappings.filter((_, i) => i !== idx)\n setFieldMappings(newMappings)\n updateParentValue(newMappings)\n }\n\n // Update mapping\n const handleChange = (idx: number, field: keyof FieldMapping, newValue: string) => {\n const newMappings = fieldMappings.map((mapping, i) =>\n i === idx ? { ...mapping, [field]: newValue } : mapping,\n )\n setFieldMappings(newMappings)\n updateParentValue(newMappings)\n }\n\n // Filter dropdown options to exclude already-mapped fields (except for current row)\n const getFilteredOptions = (options: Option[], field: keyof FieldMapping, idx: number) => {\n const used = fieldMappings\n .map((m, i) => (i !== idx ? m[field] : null))\n .filter(Boolean) as string[]\n return options.filter((opt) => !used.includes(opt.value))\n }\n\n if (!contentTypeValue || !targetCollectionValue) {\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>\n <label style={{ color: 'var(--theme-elevation-700)', fontSize: '14px', fontWeight: '600' }}>\n Field Mapping Configuration\n </label>\n <div\n style={{\n backgroundColor: 'var(--theme-elevation-50)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n color: 'var(--theme-elevation-600)',\n padding: '1rem',\n textAlign: 'center',\n }}\n >\n Please select both \"Content Type to Migrate\" and \"Target Payload Collection\" to configure\n field mappings.\n </div>\n </div>\n )\n }\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>\n <div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>\n <label style={{ color: 'var(--theme-elevation-700)', fontSize: '14px', fontWeight: '600' }}>\n Field Mapping Configuration\n </label>\n <button\n disabled={loading}\n onClick={handleAddMapping}\n style={{\n backgroundColor: loading ? 'var(--theme-elevation-200)' : 'var(--theme-success-600)',\n border: 'none',\n borderRadius: '4px',\n color: loading ? 'var(--theme-elevation-500)' : 'white',\n cursor: loading ? 'not-allowed' : 'pointer',\n fontSize: '12px',\n fontWeight: '500',\n padding: '0.5rem 1rem',\n }}\n type=\"button\"\n >\n Add Mapping\n </button>\n </div>\n\n {loading && (\n <div\n style={{\n backgroundColor: 'var(--theme-elevation-50)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n color: 'var(--theme-elevation-600)',\n padding: '1rem',\n textAlign: 'center',\n }}\n >\n Loading field schemas from WordPress and PayloadCMS...\n </div>\n )}\n\n {error && (\n <div\n style={{\n backgroundColor: 'var(--theme-error-50)',\n border: '1px solid var(--theme-error-200)',\n borderRadius: '4px',\n color: 'var(--theme-error-700)',\n padding: '1rem',\n }}\n >\n <strong>Error:</strong> {error}\n </div>\n )}\n\n {!loading && !error && sourceFields.length === 0 && destinationFields.length === 0 && (\n <div\n style={{\n backgroundColor: 'var(--theme-elevation-50)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n color: 'var(--theme-elevation-600)',\n padding: '1rem',\n textAlign: 'center',\n }}\n >\n No fields found. Please check your WordPress connection and selected content type.\n </div>\n )}\n\n {!loading && !error && (sourceFields.length > 0 || destinationFields.length > 0) && (\n <>\n {fieldMappings.length === 0 && (\n <div\n style={{\n backgroundColor: 'var(--theme-elevation-50)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n color: 'var(--theme-elevation-600)',\n padding: '1rem',\n textAlign: 'center',\n }}\n >\n No field mappings configured. Click \"Add Mapping\" to start.\n </div>\n )}\n\n {fieldMappings.map((mapping, idx) => (\n <div\n key={idx}\n style={{\n alignItems: 'center',\n backgroundColor: 'var(--theme-elevation-0)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n display: 'grid',\n gap: '0.75rem',\n gridTemplateColumns: '1fr auto 1fr auto',\n padding: '1rem',\n }}\n >\n <select\n onChange={(e) => handleChange(idx, 'sourceField', e.target.value)}\n style={{\n backgroundColor: 'var(--theme-elevation-0)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n fontSize: '14px',\n padding: '0.5rem',\n }}\n value={mapping.sourceField}\n >\n <option value=\"\">Select WordPress field...</option>\n {getFilteredOptions(sourceFields, 'sourceField', idx).map((opt, optIdx) => (\n <option key={`source-${idx}-${opt.value}-${optIdx}`} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n\n <span\n style={{\n color: 'var(--theme-elevation-400)',\n fontSize: '18px',\n fontWeight: 'bold',\n }}\n >\n →\n </span>\n\n <select\n onChange={(e) => handleChange(idx, 'destinationField', e.target.value)}\n style={{\n backgroundColor: 'var(--theme-elevation-0)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n fontSize: '14px',\n padding: '0.5rem',\n }}\n value={mapping.destinationField}\n >\n <option value=\"\">Select Payload field...</option>\n {getFilteredOptions(destinationFields, 'destinationField', idx).map(\n (opt, optIdx) => (\n <option key={`dest-${idx}-${opt.value}-${optIdx}`} value={opt.value}>\n {opt.label}\n </option>\n ),\n )}\n </select>\n\n <button\n disabled={fieldMappings.length <= 1}\n onClick={() => handleRemoveMapping(idx)}\n style={{\n backgroundColor:\n fieldMappings.length <= 1\n ? 'var(--theme-elevation-200)'\n : 'var(--theme-error-600)',\n border: 'none',\n borderRadius: '4px',\n color: fieldMappings.length <= 1 ? 'var(--theme-elevation-500)' : 'white',\n cursor: fieldMappings.length <= 1 ? 'not-allowed' : 'pointer',\n fontSize: '12px',\n fontWeight: '500',\n minWidth: '60px',\n padding: '0.5rem',\n }}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n </>\n )}\n\n <div\n style={{\n color: 'var(--theme-elevation-500)',\n fontSize: '12px',\n fontStyle: 'italic',\n marginTop: '0.5rem',\n }}\n >\n Map WordPress fields to their corresponding Payload fields. Each field can only be mapped\n once. Fields marked with * are custom fields (ACF/Meta).\n {sourceFields.length > 0 && (\n <>\n <br />\n <strong>Available WordPress fields:</strong> {sourceFields.length} |{' '}\n <strong>Available Payload fields:</strong> {destinationFields.length}\n </>\n )}\n </div>\n </div>\n )\n}\n\nexport default SimpleFieldMapping\n"],"names":["path","contentType","wpPassword","wpSiteUrl","wpUsername","type","label","value","setError","setSourceFields","setDestinationFields","contentTypeValue","targetCollectionValue","setFieldMappings","lastValueRef","isUpdatingRef","fieldMappings","backgroundColor","color","cursor","loading","error","opt","onClick","idx","sourceFields","destinationFields"],"mappings":";;;;;AAiBA;AACE;AAAuCA;AAAK;AAC5C;;AAAmE;AACnE;;AAA6E;;AAG7E;AACA;;AAGA;AACA;AACA;AACE;;;AAAwC;AACzC;AACD;AACA;;AAGA;;;;;AAMI;AACE;AACF;;AAIA;AACEC;AACAC;AACAC;AACAC;AACF;;;;;AAME;;AAEF;;AAGA;AACE;AACEC;AACAC;AACAN;AACAO;;;;;AAKF;AACF;AACF;;AAEEC;AACAC;AACF;AACF;;AAGA;;;;AAMI;AACE;AACEJ;;AAEAL;AACAO;;;;;AAKF;AACF;AACF;;AAEEG;AACF;AACF;;;AAIE;AACE;;;;;;AAMK;AACH;;;;AAIA;;AAEAD;AACAC;;AACkB;;;AAAwC;AAAE;;AAE9D;AACF;;;AAGEC;AAAkBC;AAAsB;;;;AAKxC;AACF;;;AAKA;;AAEI;AACA;AACEC;AACF;AACAC;AACF;;AAEA;AACF;;AACEP;AAAM;;AAGV;;;;;AAKE;;AAGA;AACEQ;;AAEAD;;;AAIEC;;AAEJ;AACF;;AAGA;AACE;AAAwBC;AAAe;;;AAAwC;AAAE;;;AAGnF;;AAGA;;AAEI;AACF;AAEA;;;AAGF;;;;AAKkB;AAAY;;;;AAI9B;;;AAIE;;AAIF;;AAGE;;;;;AACsE;;;;;;;AACuB;AAAG;;;;;;;;;;AAW1F;AACD;;;;AAMP;AAEA;;;;;AACsE;;;;;;;AACmB;;;;;;;AACM;AAAG;;;;;;AAOxFC;;;AAGAC;AACAC;;;;AAIF;;AAED;;;;AAKFC;;;;;;;;AASG;AACD;;AAKFC;;;;;;;AAQG;;;AAEQ;;AAAe;AAAEA;;;;;;;;;;;AAazB;AACD;;AAKF;;;;;;;;;;AAWO;AACD;;AAKFL;;;;;;;;;;AAYG;;;;;;;;;;AAUE;AACAT;;;;AAEiB;;;AAEsCA;AAClDe;;;;;;;;;AAUL;AACD;;;;;;;;;;AAYC;AACAf;;;;AAEiB;;;AAGsCA;AAChDe;;;;;;AAQPC;;AAEEN;;;AAMAC;AACAC;;;;;AAKF;;AAED;;;AAhFIK;;;;;;;;;AA8FX;;AACD;;;;;AAMa;;AAAoC;AAAEC;AAAoB;AAAG;;AAC7D;;AAAkC;AAAEC;;;;;;;AAMxD;;"}
1
+ {"version":3,"file":"SimpleFieldMapping.js","sources":["../../src/components/SimpleFieldMapping.tsx"],"sourcesContent":["'use client'\n\nimport { useField } from '@payloadcms/ui'\nimport React, { useEffect, useRef, useState } from 'react'\n\ntype FieldMapping = {\n destinationField: string\n sourceField: string\n}\n\ntype Option = {\n label: string\n path?: string\n type?: string\n value: string\n}\n\nconst SimpleFieldMapping: React.FC<{ path: string }> = ({ path }) => {\n const { setValue, value } = useField({ path })\n const { value: contentTypeValue } = useField({ path: 'contentType' })\n const { value: targetCollectionValue } = useField({ path: 'targetCollection' })\n\n // Use ref to prevent infinite loops - change to any type to handle different value types\n const isUpdatingRef = useRef(false)\n const lastValueRef = useRef<any>('')\n\n // Dynamic field options from API\n const [sourceFields, setSourceFields] = useState<Option[]>([])\n const [destinationFields, setDestinationFields] = useState<Option[]>([])\n const [fieldMappings, setFieldMappings] = useState<FieldMapping[]>([\n { destinationField: '', sourceField: '' },\n ])\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState('')\n\n // Fetch WordPress content fields (credentials resolved server-side from plugin config)\n const fetchWordPressFields = async (contentType: string) => {\n try {\n setLoading(true)\n setError('')\n\n const response = await fetch('/api/wordpress/content-fields', {\n body: JSON.stringify({ contentType }),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n const result = await response.json()\n\n if (response.ok && result.success) {\n const wpFieldOptions = (result.fields || []).map((field: any) => ({\n type: field.type,\n label: `${field.label} (${field.type})${field.custom ? ' *' : ''}`,\n path: field.path || field.name,\n value: field.path || field.name,\n }))\n setSourceFields(wpFieldOptions)\n } else {\n console.error('API returned error:', result)\n throw new Error(result.error || 'Failed to fetch WordPress fields')\n }\n } catch (error) {\n console.error('Error fetching WordPress fields:', error)\n setError(error instanceof Error ? error.message : 'Failed to fetch WordPress fields')\n setSourceFields([])\n }\n }\n\n // Fetch PayloadCMS collection fields\n const fetchPayloadFields = async (collectionSlug: string) => {\n try {\n const response = await fetch(`/api/collections/${collectionSlug}/fields`)\n\n const result = await response.json()\n\n if (response.ok && result.success) {\n const payloadFieldOptions = (result.fields || []).map((field: any) => ({\n type: field.type,\n label: `${field.label} (${field.type})`,\n path: field.path || field.name,\n value: field.path || field.name,\n }))\n setDestinationFields(payloadFieldOptions)\n } else {\n console.error('Payload API returned error:', result)\n throw new Error(result.error || 'Failed to fetch Payload collection fields')\n }\n } catch (error) {\n console.error('Error fetching Payload fields:', error)\n setDestinationFields([])\n }\n }\n\n // Fetch fields when both content type and target collection are selected\n useEffect(() => {\n const fetchFields = async () => {\n if (contentTypeValue && targetCollectionValue) {\n setLoading(true)\n try {\n await Promise.all([\n fetchWordPressFields(contentTypeValue as string),\n fetchPayloadFields(targetCollectionValue as string),\n ])\n } catch (error) {\n console.error('Error fetching fields:', error)\n } finally {\n setLoading(false)\n }\n } else {\n setSourceFields([])\n setDestinationFields([])\n setFieldMappings([{ destinationField: '', sourceField: '' }])\n setError('')\n }\n }\n\n void fetchFields()\n }, [contentTypeValue, targetCollectionValue])\n\n // Parse existing field mappings from JSON value - only when value actually changes\n useEffect(() => {\n if (isUpdatingRef.current) {\n return\n } // Prevent processing our own updates\n\n // Convert value to string for comparison\n const currentValue = typeof value === 'string' ? value : JSON.stringify(value || '')\n\n if (value && currentValue !== lastValueRef.current) {\n try {\n const parsed = typeof value === 'string' ? JSON.parse(value) : value\n if (parsed && Array.isArray(parsed.fieldMappings)) {\n setFieldMappings(parsed.fieldMappings)\n }\n lastValueRef.current = currentValue\n } catch (error) {\n console.warn('Failed to parse field mappings:', error)\n }\n }\n }, [value])\n\n // Function to update parent value without causing loops\n const updateParentValue = (mappings: FieldMapping[]) => {\n const newValue = JSON.stringify({\n contentType: contentTypeValue,\n fieldMappings: mappings,\n targetCollection: targetCollectionValue,\n })\n\n // Prevent infinite loops\n if (newValue !== lastValueRef.current && !isUpdatingRef.current) {\n isUpdatingRef.current = true\n setValue(newValue)\n lastValueRef.current = newValue\n\n // Reset the flag after a short delay\n setTimeout(() => {\n isUpdatingRef.current = false\n }, 100)\n }\n }\n\n // Add new mapping row\n const handleAddMapping = () => {\n const newMappings = [...fieldMappings, { destinationField: '', sourceField: '' }]\n setFieldMappings(newMappings)\n updateParentValue(newMappings)\n }\n\n // Remove mapping row\n const handleRemoveMapping = (idx: number) => {\n if (fieldMappings.length <= 1) {\n return\n } // Keep at least one mapping\n\n const newMappings = fieldMappings.filter((_, i) => i !== idx)\n setFieldMappings(newMappings)\n updateParentValue(newMappings)\n }\n\n // Update mapping\n const handleChange = (idx: number, field: keyof FieldMapping, newValue: string) => {\n const newMappings = fieldMappings.map((mapping, i) =>\n i === idx ? { ...mapping, [field]: newValue } : mapping,\n )\n setFieldMappings(newMappings)\n updateParentValue(newMappings)\n }\n\n // Filter dropdown options to exclude already-mapped fields (except for current row)\n const getFilteredOptions = (options: Option[], field: keyof FieldMapping, idx: number) => {\n const used = fieldMappings\n .map((m, i) => (i !== idx ? m[field] : null))\n .filter(Boolean) as string[]\n return options.filter((opt) => !used.includes(opt.value))\n }\n\n if (!contentTypeValue || !targetCollectionValue) {\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>\n <label style={{ color: 'var(--theme-elevation-700)', fontSize: '14px', fontWeight: '600' }}>\n Field Mapping Configuration\n </label>\n <div\n style={{\n backgroundColor: 'var(--theme-elevation-50)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n color: 'var(--theme-elevation-600)',\n padding: '1rem',\n textAlign: 'center',\n }}\n >\n Please select both \"Content Type to Migrate\" and \"Target Payload Collection\" to configure\n field mappings.\n </div>\n </div>\n )\n }\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>\n <div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>\n <label style={{ color: 'var(--theme-elevation-700)', fontSize: '14px', fontWeight: '600' }}>\n Field Mapping Configuration\n </label>\n <button\n disabled={loading}\n onClick={handleAddMapping}\n style={{\n backgroundColor: loading ? 'var(--theme-elevation-200)' : 'var(--theme-success-600)',\n border: 'none',\n borderRadius: '4px',\n color: loading ? 'var(--theme-elevation-500)' : 'white',\n cursor: loading ? 'not-allowed' : 'pointer',\n fontSize: '12px',\n fontWeight: '500',\n padding: '0.5rem 1rem',\n }}\n type=\"button\"\n >\n Add Mapping\n </button>\n </div>\n\n {loading && (\n <div\n style={{\n backgroundColor: 'var(--theme-elevation-50)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n color: 'var(--theme-elevation-600)',\n padding: '1rem',\n textAlign: 'center',\n }}\n >\n Loading field schemas from WordPress and PayloadCMS...\n </div>\n )}\n\n {error && (\n <div\n style={{\n backgroundColor: 'var(--theme-error-50)',\n border: '1px solid var(--theme-error-200)',\n borderRadius: '4px',\n color: 'var(--theme-error-700)',\n padding: '1rem',\n }}\n >\n <strong>Error:</strong> {error}\n </div>\n )}\n\n {!loading && !error && sourceFields.length === 0 && destinationFields.length === 0 && (\n <div\n style={{\n backgroundColor: 'var(--theme-elevation-50)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n color: 'var(--theme-elevation-600)',\n padding: '1rem',\n textAlign: 'center',\n }}\n >\n No fields found. Please check your WordPress connection and selected content type.\n </div>\n )}\n\n {!loading && !error && (sourceFields.length > 0 || destinationFields.length > 0) && (\n <>\n {fieldMappings.length === 0 && (\n <div\n style={{\n backgroundColor: 'var(--theme-elevation-50)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n color: 'var(--theme-elevation-600)',\n padding: '1rem',\n textAlign: 'center',\n }}\n >\n No field mappings configured. Click \"Add Mapping\" to start.\n </div>\n )}\n\n {fieldMappings.map((mapping, idx) => (\n <div\n key={idx}\n style={{\n alignItems: 'center',\n backgroundColor: 'var(--theme-elevation-0)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n display: 'grid',\n gap: '0.75rem',\n gridTemplateColumns: '1fr auto 1fr auto',\n padding: '1rem',\n }}\n >\n <select\n onChange={(e) => handleChange(idx, 'sourceField', e.target.value)}\n style={{\n backgroundColor: 'var(--theme-elevation-0)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n fontSize: '14px',\n padding: '0.5rem',\n }}\n value={mapping.sourceField}\n >\n <option value=\"\">Select WordPress field...</option>\n {getFilteredOptions(sourceFields, 'sourceField', idx).map((opt, optIdx) => (\n <option key={`source-${idx}-${opt.value}-${optIdx}`} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n\n <span\n style={{\n color: 'var(--theme-elevation-400)',\n fontSize: '18px',\n fontWeight: 'bold',\n }}\n >\n →\n </span>\n\n <select\n onChange={(e) => handleChange(idx, 'destinationField', e.target.value)}\n style={{\n backgroundColor: 'var(--theme-elevation-0)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '4px',\n fontSize: '14px',\n padding: '0.5rem',\n }}\n value={mapping.destinationField}\n >\n <option value=\"\">Select Payload field...</option>\n {getFilteredOptions(destinationFields, 'destinationField', idx).map(\n (opt, optIdx) => (\n <option key={`dest-${idx}-${opt.value}-${optIdx}`} value={opt.value}>\n {opt.label}\n </option>\n ),\n )}\n </select>\n\n <button\n disabled={fieldMappings.length <= 1}\n onClick={() => handleRemoveMapping(idx)}\n style={{\n backgroundColor:\n fieldMappings.length <= 1\n ? 'var(--theme-elevation-200)'\n : 'var(--theme-error-600)',\n border: 'none',\n borderRadius: '4px',\n color: fieldMappings.length <= 1 ? 'var(--theme-elevation-500)' : 'white',\n cursor: fieldMappings.length <= 1 ? 'not-allowed' : 'pointer',\n fontSize: '12px',\n fontWeight: '500',\n minWidth: '60px',\n padding: '0.5rem',\n }}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n </>\n )}\n\n <div\n style={{\n color: 'var(--theme-elevation-500)',\n fontSize: '12px',\n fontStyle: 'italic',\n marginTop: '0.5rem',\n }}\n >\n Map WordPress fields to their corresponding Payload fields. Each field can only be mapped\n once. Fields marked with * are custom fields (ACF/Meta).\n {sourceFields.length > 0 && (\n <>\n <br />\n <strong>Available WordPress fields:</strong> {sourceFields.length} |{' '}\n <strong>Available Payload fields:</strong> {destinationFields.length}\n </>\n )}\n </div>\n </div>\n )\n}\n\nexport default SimpleFieldMapping\n"],"names":["path","contentType","type","label","value","setError","setSourceFields","setDestinationFields","contentTypeValue","targetCollectionValue","setFieldMappings","lastValueRef","isUpdatingRef","fieldMappings","backgroundColor","color","cursor","loading","error","opt","onClick","idx","sourceFields","destinationFields"],"mappings":";;;;;AAiBA;AACE;AAAuCA;AAAK;AAC5C;;AAAmE;AACnE;;AAA6E;;AAG7E;AACA;;AAGA;AACA;AACA;AACE;;;AAAwC;AACzC;AACD;AACA;;AAGA;;;;;;AAM6BC;AAAY;;;AAGnC;;AAEF;;AAGA;AACE;AACEC;AACAC;AACAH;AACAI;;;;;AAKF;AACF;AACF;;AAEEC;AACAC;AACF;AACF;;AAGA;;;;AAMI;AACE;AACEJ;;AAEAF;AACAI;;;;;AAKF;AACF;AACF;;AAEEG;AACF;AACF;;;AAIE;AACE;;;;;;AAMK;AACH;;;;AAIA;;AAEAD;AACAC;;AACkB;;;AAAwC;AAAE;;AAE9D;AACF;;;AAGEC;AAAkBC;AAAsB;;;;AAKxC;AACF;;;AAKA;;AAEI;AACA;AACEC;AACF;AACAC;AACF;;AAEA;AACF;;AACEP;AAAM;;AAGV;;;;;AAKE;;AAGA;AACEQ;;AAEAD;;;AAIEC;;AAEJ;AACF;;AAGA;AACE;AAAwBC;AAAe;;;AAAwC;AAAE;;;AAGnF;;AAGA;;AAEI;AACF;AAEA;;;AAGF;;;;AAKkB;AAAY;;;;AAI9B;;;AAIE;;AAIF;;AAGE;;;;;AACsE;;;;;;;AACuB;AAAG;;;;;;;;;;AAW1F;AACD;;;;AAMP;AAEA;;;;;AACsE;;;;;;;AACmB;;;;;;;AACM;AAAG;;;;;;AAOxFC;;;AAGAC;AACAC;;;;AAIF;;AAED;;;;AAKFC;;;;;;;;AASG;AACD;;AAKFC;;;;;;;AAQG;;;AAEQ;;AAAe;AAAEA;;;;;;;;;;;AAazB;AACD;;AAKF;;;;;;;;;;AAWO;AACD;;AAKFL;;;;;;;;;;AAYG;;;;;;;;;;AAUE;AACAT;;;;AAEiB;;;AAEsCA;AAClDe;;;;;;;;;AAUL;AACD;;;;;;;;;;AAYC;AACAf;;;;AAEiB;;;AAGsCA;AAChDe;;;;;;AAQPC;;AAEEN;;;AAMAC;AACAC;;;;;AAKF;;AAED;;;AAhFIK;;;;;;;;;AA8FX;;AACD;;;;;AAMa;;AAAoC;AAAEC;AAAoB;AAAG;;AAC7D;;AAAkC;AAAEC;;;;;;;AAMxD;;"}
@@ -51,9 +51,16 @@ const fetchWordPressContentFields = async (req, pluginOptions)=>{
51
51
  if (authError) return authError;
52
52
  try {
53
53
  const body = await parseRequestBody(req);
54
- const { contentType, wpPassword, wpSiteUrl, wpUsername } = body;
55
- if (!wpSiteUrl || !wpUsername || !wpPassword || !contentType) {
56
- throw new Error('Missing required parameters: wpSiteUrl, wpUsername, wpPassword, contentType');
54
+ const { contentType } = body;
55
+ // Credentials from request body take priority, then fall back to plugin config (env vars)
56
+ const wpSiteUrl = body.wpSiteUrl || pluginOptions.wpSiteUrl;
57
+ const wpUsername = body.wpUsername || pluginOptions.wpUsername;
58
+ const wpPassword = body.wpPassword || pluginOptions.wpPassword;
59
+ if (!wpSiteUrl || !wpUsername || !wpPassword) {
60
+ throw new Error('WordPress credentials are required. Configure them in the plugin config or the dashboard Site Configuration panel.');
61
+ }
62
+ if (!contentType) {
63
+ throw new Error('contentType is required');
57
64
  }
58
65
  const credentials = {
59
66
  wpPassword,
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.js","sources":["../../../src/utils/endpoints/handlers.ts"],"sourcesContent":["/** API endpoint request handlers for WordPress migration operations. */\n\nimport type { Payload, PayloadRequest } from 'payload'\n\nimport type { PayloadWordPressMigratorConfig } from '../../index.js'\n\nimport { analyzePayloadFields } from '../fields/analyzer.js'\nimport {\n checkRateLimit,\n createErrorResponse,\n createSuccessResponse,\n getContentCache,\n isCacheValid,\n parseRequestBody,\n requireAuth,\n requireMigrationAccess,\n setContentCache,\n validateWordPressCredentials,\n} from '../helpers/index.js'\nimport { MIGRATION_COLLECTION } from '../types.js'\nimport { WordPressClient } from '../wordpress/client.js'\n\nexport const wordpressConnectionHandler = async (\n req: PayloadRequest,\n pluginOptions: PayloadWordPressMigratorConfig,\n): Promise<Response> => {\n const authError = await requireMigrationAccess(req, pluginOptions.access)\n if (authError) return authError\n\n // Rate limit connection attempts to prevent credential brute-forcing\n const clientIp = req.headers?.get?.('x-forwarded-for') || req.headers?.get?.('x-real-ip') || 'unknown'\n if (!checkRateLimit(`wp-connection:${clientIp}`)) {\n return createErrorResponse('Too many connection attempts. Try again in 1 minute.', 429)\n }\n\n try {\n const body = await parseRequestBody(req)\n const credentials = validateWordPressCredentials(body)\n\n const client = new WordPressClient(credentials)\n const result = await client.testConnection()\n\n return createSuccessResponse(result, result.success ? 200 : 400)\n } catch (error) {\n req.payload.logger.error(`WordPress connection handler error: ${error}`)\n return createErrorResponse(error instanceof Error ? error : new Error(String(error)), 500)\n }\n}\n\nexport const discoverWordPressContent = async (\n req: PayloadRequest,\n pluginOptions: PayloadWordPressMigratorConfig,\n): Promise<Response> => {\n const authError = await requireMigrationAccess(req, pluginOptions.access)\n if (authError) return authError\n\n try {\n const body = await parseRequestBody(req)\n const credentials = validateWordPressCredentials(body)\n\n const client = new WordPressClient(credentials)\n const result = await client.discoverContent()\n\n if (result.success) {\n return createSuccessResponse({\n ...result.data,\n success: true,\n })\n } else {\n return createErrorResponse(result.error || 'Failed to discover content', 400)\n }\n } catch (error) {\n req.payload.logger.error(`Content discovery error: ${error}`)\n return createErrorResponse(error instanceof Error ? error : new Error(String(error)), 500)\n }\n}\n\nexport const fetchWordPressContentFields = async (\n req: PayloadRequest,\n pluginOptions: PayloadWordPressMigratorConfig,\n): Promise<Response> => {\n const authError = await requireMigrationAccess(req, pluginOptions.access)\n if (authError) return authError\n\n try {\n const body = await parseRequestBody(req)\n const { contentType, wpPassword, wpSiteUrl, wpUsername } = body\n\n if (!wpSiteUrl || !wpUsername || !wpPassword || !contentType) {\n throw new Error('Missing required parameters: wpSiteUrl, wpUsername, wpPassword, contentType')\n }\n\n const credentials = { wpPassword, wpSiteUrl, wpUsername }\n const client = new WordPressClient(credentials)\n const result = await client.fetchContentFields(contentType)\n\n if (result.success) {\n return createSuccessResponse({\n ...result.data,\n success: true,\n })\n } else {\n return createErrorResponse(result.error || 'Failed to fetch fields', 500)\n }\n } catch (error) {\n return createErrorResponse(error instanceof Error ? error : new Error(String(error)), 500)\n }\n}\n\nexport const fetchPayloadCollectionFields = (req: PayloadRequest): Response => {\n const authError = requireAuth(req)\n if (authError) return authError\n\n try {\n // Extract slug from URL path parameter\n const url = new URL(req.url || '')\n const pathParts = url.pathname.split('/')\n const collectionsIndex = pathParts.indexOf('collections')\n const collectionSlug =\n collectionsIndex >= 0 && pathParts[collectionsIndex + 1]\n ? pathParts[collectionsIndex + 1]\n : null\n\n if (!collectionSlug) {\n return createErrorResponse('Collection slug is required')\n }\n\n // Access the collection configuration from the Payload config\n const collectionConfig = req.payload.config?.collections?.find(\n (collection: any) => collection.slug === collectionSlug,\n )\n\n if (!collectionConfig) {\n return createErrorResponse(`Collection '${collectionSlug}' not found in config`, 404)\n }\n\n // Extract and format field information from the collection config\n const fields = analyzePayloadFields(collectionConfig.fields || [])\n\n return createSuccessResponse({\n collectionSlug,\n fields,\n success: true,\n })\n } catch (error) {\n req.payload.logger.error(`Error in fetchPayloadCollectionFields: ${error}`)\n return createErrorResponse(error instanceof Error ? error : new Error(String(error)), 500)\n }\n}\n\n// Function to get migration summary data (for server components)\nexport const getMigrationSummaryData = async (forceRefresh = false, payload?: Payload) => {\n try {\n const now = Date.now()\n\n // Check for global cache invalidation from collection hooks\n let globalCacheInvalidated = false\n try {\n const { getLastCacheInvalidation } = await import('../../index.js')\n const lastInvalidation = getLastCacheInvalidation()\n const contentCache = getContentCache()\n globalCacheInvalidated = contentCache ? lastInvalidation > contentCache.timestamp : false\n } catch (error) {\n // Ignore import errors - fallback to local cache only\n }\n\n if (!forceRefresh && !globalCacheInvalidated && isCacheValid()) {\n const contentCache = getContentCache()\n return contentCache?.data\n }\n\n if (!payload) {\n throw new Error('Payload instance is required')\n }\n\n // Get migration jobs from database\n const jobs = await payload.find({\n collection: MIGRATION_COLLECTION,\n limit: 100,\n sort: '-createdAt',\n })\n\n // Collect recent logs from all jobs for centralized display\n const recentLogs: Array<{\n jobName: string\n level: string\n message: string\n timestamp: Date\n }> = []\n\n jobs.docs.forEach((job: any) => {\n if (job.logs && Array.isArray(job.logs)) {\n job.logs.forEach((log: any) => {\n recentLogs.push({\n jobName: job.jobName,\n level: log.level,\n message: log.message,\n timestamp: new Date(log.timestamp),\n })\n })\n }\n })\n\n // Sort logs by timestamp (newest first) and limit to recent 50\n recentLogs.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())\n const limitedLogs = recentLogs.slice(0, 50)\n\n // Calculate summary statistics\n const activeJobs = jobs.docs.filter((job: any) => job.status === 'running').length\n const completedJobs = jobs.docs.filter((job: any) => job.status === 'completed').length\n const recentJobs = jobs.docs.slice(0, 10).map((job: any) => ({\n id: job.id,\n createdAt: job.createdAt,\n jobName: job.jobName,\n progress: job.progress || {\n processedItems: 0,\n successfulItems: 0,\n totalItems: 0,\n },\n status: job.status,\n }))\n\n const totalItemsMigrated = jobs.docs.reduce((total: number, job: any) => {\n return total + (job.progress?.successfulItems || 0)\n }, 0)\n\n const summary = {\n activeJobs,\n completedJobs,\n recentJobs,\n recentLogs: limitedLogs,\n totalItemsMigrated,\n totalJobs: jobs.totalDocs,\n totalSites: 1, // Single site configuration handled in dashboard\n }\n\n // Update cache\n setContentCache(summary)\n\n return summary\n } catch (error) {\n const msg = `Error fetching migration summary: ${error}`\n payload ? payload.logger.error(msg) : console.error(msg)\n throw error\n }\n}\n\nexport const migrationSummaryHandler = async (\n req: PayloadRequest,\n pluginOptions: PayloadWordPressMigratorConfig,\n): Promise<Response> => {\n const authError = await requireMigrationAccess(req, pluginOptions.access)\n if (authError) return authError\n\n try {\n // Check for refresh parameter\n const url = req.url || ''\n const forceRefresh = url.includes('refresh=true')\n\n const summary = await getMigrationSummaryData(forceRefresh, req.payload)\n\n return createSuccessResponse(summary)\n } catch (error) {\n req.payload.logger.error(`Error fetching migration summary: ${error}`)\n return createErrorResponse('Failed to fetch migration summary', 500)\n }\n}\n"],"names":["wordpressConnectionHandler","req","pluginOptions","authError","requireMigrationAccess","access","clientIp","headers","get","checkRateLimit","createErrorResponse","body","parseRequestBody","credentials","validateWordPressCredentials","client","WordPressClient","result","testConnection","createSuccessResponse","success","error","payload","logger","Error","String","discoverWordPressContent","discoverContent","data","fetchWordPressContentFields","contentType","wpPassword","wpSiteUrl","wpUsername","fetchContentFields","fetchPayloadCollectionFields","requireAuth","url","URL","pathParts","pathname","split","collectionsIndex","indexOf","collectionSlug","collectionConfig","config","collections","find","collection","slug","fields","analyzePayloadFields","getMigrationSummaryData","forceRefresh","now","Date","globalCacheInvalidated","getLastCacheInvalidation","lastInvalidation","contentCache","getContentCache","timestamp","isCacheValid","jobs","MIGRATION_COLLECTION","limit","sort","recentLogs","docs","forEach","job","logs","Array","isArray","log","push","jobName","level","message","a","b","getTime","limitedLogs","slice","activeJobs","filter","status","length","completedJobs","recentJobs","map","id","createdAt","progress","processedItems","successfulItems","totalItems","totalItemsMigrated","reduce","total","summary","totalJobs","totalDocs","totalSites","setContentCache","msg","console","migrationSummaryHandler","includes"],"mappings":"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"}
1
+ {"version":3,"file":"handlers.js","sources":["../../../src/utils/endpoints/handlers.ts"],"sourcesContent":["/** API endpoint request handlers for WordPress migration operations. */\n\nimport type { Payload, PayloadRequest } from 'payload'\n\nimport type { PayloadWordPressMigratorConfig } from '../../index.js'\n\nimport { analyzePayloadFields } from '../fields/analyzer.js'\nimport {\n checkRateLimit,\n createErrorResponse,\n createSuccessResponse,\n getContentCache,\n isCacheValid,\n parseRequestBody,\n requireAuth,\n requireMigrationAccess,\n setContentCache,\n validateWordPressCredentials,\n} from '../helpers/index.js'\nimport { MIGRATION_COLLECTION } from '../types.js'\nimport { WordPressClient } from '../wordpress/client.js'\n\nexport const wordpressConnectionHandler = async (\n req: PayloadRequest,\n pluginOptions: PayloadWordPressMigratorConfig,\n): Promise<Response> => {\n const authError = await requireMigrationAccess(req, pluginOptions.access)\n if (authError) return authError\n\n // Rate limit connection attempts to prevent credential brute-forcing\n const clientIp = req.headers?.get?.('x-forwarded-for') || req.headers?.get?.('x-real-ip') || 'unknown'\n if (!checkRateLimit(`wp-connection:${clientIp}`)) {\n return createErrorResponse('Too many connection attempts. Try again in 1 minute.', 429)\n }\n\n try {\n const body = await parseRequestBody(req)\n const credentials = validateWordPressCredentials(body)\n\n const client = new WordPressClient(credentials)\n const result = await client.testConnection()\n\n return createSuccessResponse(result, result.success ? 200 : 400)\n } catch (error) {\n req.payload.logger.error(`WordPress connection handler error: ${error}`)\n return createErrorResponse(error instanceof Error ? error : new Error(String(error)), 500)\n }\n}\n\nexport const discoverWordPressContent = async (\n req: PayloadRequest,\n pluginOptions: PayloadWordPressMigratorConfig,\n): Promise<Response> => {\n const authError = await requireMigrationAccess(req, pluginOptions.access)\n if (authError) return authError\n\n try {\n const body = await parseRequestBody(req)\n const credentials = validateWordPressCredentials(body)\n\n const client = new WordPressClient(credentials)\n const result = await client.discoverContent()\n\n if (result.success) {\n return createSuccessResponse({\n ...result.data,\n success: true,\n })\n } else {\n return createErrorResponse(result.error || 'Failed to discover content', 400)\n }\n } catch (error) {\n req.payload.logger.error(`Content discovery error: ${error}`)\n return createErrorResponse(error instanceof Error ? error : new Error(String(error)), 500)\n }\n}\n\nexport const fetchWordPressContentFields = async (\n req: PayloadRequest,\n pluginOptions: PayloadWordPressMigratorConfig,\n): Promise<Response> => {\n const authError = await requireMigrationAccess(req, pluginOptions.access)\n if (authError) return authError\n\n try {\n const body = await parseRequestBody(req)\n const { contentType } = body\n\n // Credentials from request body take priority, then fall back to plugin config (env vars)\n const wpSiteUrl = body.wpSiteUrl || pluginOptions.wpSiteUrl\n const wpUsername = body.wpUsername || pluginOptions.wpUsername\n const wpPassword = body.wpPassword || pluginOptions.wpPassword\n\n if (!wpSiteUrl || !wpUsername || !wpPassword) {\n throw new Error('WordPress credentials are required. Configure them in the plugin config or the dashboard Site Configuration panel.')\n }\n\n if (!contentType) {\n throw new Error('contentType is required')\n }\n\n const credentials = { wpPassword, wpSiteUrl, wpUsername }\n const client = new WordPressClient(credentials)\n const result = await client.fetchContentFields(contentType)\n\n if (result.success) {\n return createSuccessResponse({\n ...result.data,\n success: true,\n })\n } else {\n return createErrorResponse(result.error || 'Failed to fetch fields', 500)\n }\n } catch (error) {\n return createErrorResponse(error instanceof Error ? error : new Error(String(error)), 500)\n }\n}\n\nexport const fetchPayloadCollectionFields = (req: PayloadRequest): Response => {\n const authError = requireAuth(req)\n if (authError) return authError\n\n try {\n // Extract slug from URL path parameter\n const url = new URL(req.url || '')\n const pathParts = url.pathname.split('/')\n const collectionsIndex = pathParts.indexOf('collections')\n const collectionSlug =\n collectionsIndex >= 0 && pathParts[collectionsIndex + 1]\n ? pathParts[collectionsIndex + 1]\n : null\n\n if (!collectionSlug) {\n return createErrorResponse('Collection slug is required')\n }\n\n // Access the collection configuration from the Payload config\n const collectionConfig = req.payload.config?.collections?.find(\n (collection: any) => collection.slug === collectionSlug,\n )\n\n if (!collectionConfig) {\n return createErrorResponse(`Collection '${collectionSlug}' not found in config`, 404)\n }\n\n // Extract and format field information from the collection config\n const fields = analyzePayloadFields(collectionConfig.fields || [])\n\n return createSuccessResponse({\n collectionSlug,\n fields,\n success: true,\n })\n } catch (error) {\n req.payload.logger.error(`Error in fetchPayloadCollectionFields: ${error}`)\n return createErrorResponse(error instanceof Error ? error : new Error(String(error)), 500)\n }\n}\n\n// Function to get migration summary data (for server components)\nexport const getMigrationSummaryData = async (forceRefresh = false, payload?: Payload) => {\n try {\n const now = Date.now()\n\n // Check for global cache invalidation from collection hooks\n let globalCacheInvalidated = false\n try {\n const { getLastCacheInvalidation } = await import('../../index.js')\n const lastInvalidation = getLastCacheInvalidation()\n const contentCache = getContentCache()\n globalCacheInvalidated = contentCache ? lastInvalidation > contentCache.timestamp : false\n } catch (error) {\n // Ignore import errors - fallback to local cache only\n }\n\n if (!forceRefresh && !globalCacheInvalidated && isCacheValid()) {\n const contentCache = getContentCache()\n return contentCache?.data\n }\n\n if (!payload) {\n throw new Error('Payload instance is required')\n }\n\n // Get migration jobs from database\n const jobs = await payload.find({\n collection: MIGRATION_COLLECTION,\n limit: 100,\n sort: '-createdAt',\n })\n\n // Collect recent logs from all jobs for centralized display\n const recentLogs: Array<{\n jobName: string\n level: string\n message: string\n timestamp: Date\n }> = []\n\n jobs.docs.forEach((job: any) => {\n if (job.logs && Array.isArray(job.logs)) {\n job.logs.forEach((log: any) => {\n recentLogs.push({\n jobName: job.jobName,\n level: log.level,\n message: log.message,\n timestamp: new Date(log.timestamp),\n })\n })\n }\n })\n\n // Sort logs by timestamp (newest first) and limit to recent 50\n recentLogs.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())\n const limitedLogs = recentLogs.slice(0, 50)\n\n // Calculate summary statistics\n const activeJobs = jobs.docs.filter((job: any) => job.status === 'running').length\n const completedJobs = jobs.docs.filter((job: any) => job.status === 'completed').length\n const recentJobs = jobs.docs.slice(0, 10).map((job: any) => ({\n id: job.id,\n createdAt: job.createdAt,\n jobName: job.jobName,\n progress: job.progress || {\n processedItems: 0,\n successfulItems: 0,\n totalItems: 0,\n },\n status: job.status,\n }))\n\n const totalItemsMigrated = jobs.docs.reduce((total: number, job: any) => {\n return total + (job.progress?.successfulItems || 0)\n }, 0)\n\n const summary = {\n activeJobs,\n completedJobs,\n recentJobs,\n recentLogs: limitedLogs,\n totalItemsMigrated,\n totalJobs: jobs.totalDocs,\n totalSites: 1, // Single site configuration handled in dashboard\n }\n\n // Update cache\n setContentCache(summary)\n\n return summary\n } catch (error) {\n const msg = `Error fetching migration summary: ${error}`\n payload ? payload.logger.error(msg) : console.error(msg)\n throw error\n }\n}\n\nexport const migrationSummaryHandler = async (\n req: PayloadRequest,\n pluginOptions: PayloadWordPressMigratorConfig,\n): Promise<Response> => {\n const authError = await requireMigrationAccess(req, pluginOptions.access)\n if (authError) return authError\n\n try {\n // Check for refresh parameter\n const url = req.url || ''\n const forceRefresh = url.includes('refresh=true')\n\n const summary = await getMigrationSummaryData(forceRefresh, req.payload)\n\n return createSuccessResponse(summary)\n } catch (error) {\n req.payload.logger.error(`Error fetching migration summary: ${error}`)\n return createErrorResponse('Failed to fetch migration summary', 500)\n }\n}\n"],"names":["wordpressConnectionHandler","req","pluginOptions","authError","requireMigrationAccess","access","clientIp","headers","get","checkRateLimit","createErrorResponse","body","parseRequestBody","credentials","validateWordPressCredentials","client","WordPressClient","result","testConnection","createSuccessResponse","success","error","payload","logger","Error","String","discoverWordPressContent","discoverContent","data","fetchWordPressContentFields","contentType","wpSiteUrl","wpUsername","wpPassword","fetchContentFields","fetchPayloadCollectionFields","requireAuth","url","URL","pathParts","pathname","split","collectionsIndex","indexOf","collectionSlug","collectionConfig","config","collections","find","collection","slug","fields","analyzePayloadFields","getMigrationSummaryData","forceRefresh","now","Date","globalCacheInvalidated","getLastCacheInvalidation","lastInvalidation","contentCache","getContentCache","timestamp","isCacheValid","jobs","MIGRATION_COLLECTION","limit","sort","recentLogs","docs","forEach","job","logs","Array","isArray","log","push","jobName","level","message","a","b","getTime","limitedLogs","slice","activeJobs","filter","status","length","completedJobs","recentJobs","map","id","createdAt","progress","processedItems","successfulItems","totalItems","totalItemsMigrated","reduce","total","summary","totalJobs","totalDocs","totalSites","setContentCache","msg","console","migrationSummaryHandler","includes"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-wordpress-migrator",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "A PayloadCMS plugin for WordPress migration - migrate and manage WordPress content directly in your Payload admin dashboard",
5
5
  "license": "MIT",
6
6
  "type": "module",