diff --git a/DELETE_wordpress_api.txt b/DELETE_wordpress_api.txt deleted file mode 100644 index 04df0a00..00000000 --- a/DELETE_wordpress_api.txt +++ /dev/null @@ -1 +0,0 @@ -# This file has been removed to fix circular import issues \ No newline at end of file diff --git a/backend/igny8_core/api/wordpress_publishing.py b/backend/igny8_core/api/wordpress_publishing.py new file mode 100644 index 00000000..f2dd09c1 --- /dev/null +++ b/backend/igny8_core/api/wordpress_publishing.py @@ -0,0 +1,400 @@ +""" +WordPress Publishing API Views +Handles manual content publishing to WordPress sites +""" +from rest_framework import status +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from typing import Dict, Any, List + +from igny8_core.models import ContentPost, SiteIntegration +from igny8_core.tasks.wordpress_publishing import ( + publish_content_to_wordpress, + bulk_publish_content_to_wordpress +) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def publish_single_content(request, content_id: int) -> Response: + """ + Publish a single content item to WordPress + + POST /api/v1/content/{content_id}/publish-to-wordpress/ + + Body: + { + "site_integration_id": 123, // Optional - will use default if not provided + "force": false // Optional - force republish even if already published + } + """ + try: + content = get_object_or_404(ContentPost, id=content_id) + + # Check permissions + if not request.user.has_perm('content.change_contentpost'): + return Response( + { + 'success': False, + 'message': 'Permission denied', + 'error': 'insufficient_permissions' + }, + status=status.HTTP_403_FORBIDDEN + ) + + # Get site integration + site_integration_id = request.data.get('site_integration_id') + force = request.data.get('force', False) + + if site_integration_id: + site_integration = get_object_or_404(SiteIntegration, id=site_integration_id) + else: + # Get default WordPress integration for user's organization + site_integration = SiteIntegration.objects.filter( + platform='wordpress', + is_active=True, + # Add organization filter if applicable + ).first() + + if not site_integration: + return Response( + { + 'success': False, + 'message': 'No WordPress integration found', + 'error': 'no_integration' + }, + status=status.HTTP_400_BAD_REQUEST + ) + + # Check if already published (unless force is true) + if not force and content.wordpress_sync_status == 'success': + return Response( + { + 'success': True, + 'message': 'Content already published to WordPress', + 'data': { + 'content_id': content.id, + 'wordpress_post_id': content.wordpress_post_id, + 'wordpress_post_url': content.wordpress_post_url, + 'status': 'already_published' + } + } + ) + + # Check if currently syncing + if content.wordpress_sync_status == 'syncing': + return Response( + { + 'success': False, + 'message': 'Content is currently being published to WordPress', + 'error': 'sync_in_progress' + }, + status=status.HTTP_409_CONFLICT + ) + + # Validate content is ready for publishing + if not content.title or not (content.content_html or content.content): + return Response( + { + 'success': False, + 'message': 'Content is incomplete - missing title or content', + 'error': 'incomplete_content' + }, + status=status.HTTP_400_BAD_REQUEST + ) + + # Set status to pending and queue the task + content.wordpress_sync_status = 'pending' + content.save(update_fields=['wordpress_sync_status']) + + # Get task_id if content is associated with a writer task + task_id = None + if hasattr(content, 'writer_task'): + task_id = content.writer_task.id + + # Queue the publishing task + task_result = publish_content_to_wordpress.delay( + content.id, + site_integration.id, + task_id + ) + + return Response( + { + 'success': True, + 'message': 'Content queued for WordPress publishing', + 'data': { + 'content_id': content.id, + 'site_integration_id': site_integration.id, + 'task_id': task_result.id, + 'status': 'queued' + } + }, + status=status.HTTP_202_ACCEPTED + ) + + except Exception as e: + return Response( + { + 'success': False, + 'message': f'Error queuing content for WordPress publishing: {str(e)}', + 'error': 'server_error' + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def bulk_publish_content(request) -> Response: + """ + Bulk publish multiple content items to WordPress + + POST /api/v1/content/bulk-publish-to-wordpress/ + + Body: + { + "content_ids": [1, 2, 3, 4], + "site_integration_id": 123, // Optional + "force": false // Optional + } + """ + try: + content_ids = request.data.get('content_ids', []) + site_integration_id = request.data.get('site_integration_id') + force = request.data.get('force', False) + + if not content_ids: + return Response( + { + 'success': False, + 'message': 'No content IDs provided', + 'error': 'missing_content_ids' + }, + status=status.HTTP_400_BAD_REQUEST + ) + + # Check permissions + if not request.user.has_perm('content.change_contentpost'): + return Response( + { + 'success': False, + 'message': 'Permission denied', + 'error': 'insufficient_permissions' + }, + status=status.HTTP_403_FORBIDDEN + ) + + # Get site integration + if site_integration_id: + site_integration = get_object_or_404(SiteIntegration, id=site_integration_id) + else: + site_integration = SiteIntegration.objects.filter( + platform='wordpress', + is_active=True, + ).first() + + if not site_integration: + return Response( + { + 'success': False, + 'message': 'No WordPress integration found', + 'error': 'no_integration' + }, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validate content items + content_items = ContentPost.objects.filter(id__in=content_ids) + + if content_items.count() != len(content_ids): + return Response( + { + 'success': False, + 'message': 'Some content items not found', + 'error': 'content_not_found' + }, + status=status.HTTP_404_NOT_FOUND + ) + + # Queue bulk publishing task + task_result = bulk_publish_content_to_wordpress.delay( + content_ids, + site_integration.id + ) + + return Response( + { + 'success': True, + 'message': f'{len(content_ids)} content items queued for WordPress publishing', + 'data': { + 'content_count': len(content_ids), + 'site_integration_id': site_integration.id, + 'task_id': task_result.id, + 'status': 'queued' + } + }, + status=status.HTTP_202_ACCEPTED + ) + + except Exception as e: + return Response( + { + 'success': False, + 'message': f'Error queuing bulk WordPress publishing: {str(e)}', + 'error': 'server_error' + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_wordpress_status(request, content_id: int) -> Response: + """ + Get WordPress publishing status for a content item + + GET /api/v1/content/{content_id}/wordpress-status/ + """ + try: + content = get_object_or_404(ContentPost, id=content_id) + + return Response( + { + 'success': True, + 'data': { + 'content_id': content.id, + 'wordpress_sync_status': content.wordpress_sync_status, + 'wordpress_post_id': content.wordpress_post_id, + 'wordpress_post_url': content.wordpress_post_url, + 'wordpress_sync_attempts': content.wordpress_sync_attempts, + 'last_wordpress_sync': content.last_wordpress_sync.isoformat() if content.last_wordpress_sync else None, + } + } + ) + + except Exception as e: + return Response( + { + 'success': False, + 'message': f'Error getting WordPress status: {str(e)}', + 'error': 'server_error' + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_wordpress_integrations(request) -> Response: + """ + Get available WordPress integrations for publishing + + GET /api/v1/wordpress-integrations/ + """ + try: + integrations = SiteIntegration.objects.filter( + platform='wordpress', + is_active=True, + # Add organization filter if applicable + ).values( + 'id', 'site_name', 'site_url', 'is_active', + 'created_at', 'last_sync_at' + ) + + return Response( + { + 'success': True, + 'data': list(integrations) + } + ) + + except Exception as e: + return Response( + { + 'success': False, + 'message': f'Error getting WordPress integrations: {str(e)}', + 'error': 'server_error' + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def retry_failed_wordpress_sync(request, content_id: int) -> Response: + """ + Retry a failed WordPress sync + + POST /api/v1/content/{content_id}/retry-wordpress-sync/ + """ + try: + content = get_object_or_404(ContentPost, id=content_id) + + if content.wordpress_sync_status != 'failed': + return Response( + { + 'success': False, + 'message': 'Content is not in failed status', + 'error': 'invalid_status' + }, + status=status.HTTP_400_BAD_REQUEST + ) + + # Get default WordPress integration + site_integration = SiteIntegration.objects.filter( + platform='wordpress', + is_active=True, + ).first() + + if not site_integration: + return Response( + { + 'success': False, + 'message': 'No WordPress integration found', + 'error': 'no_integration' + }, + status=status.HTTP_400_BAD_REQUEST + ) + + # Reset status and retry + content.wordpress_sync_status = 'pending' + content.save(update_fields=['wordpress_sync_status']) + + # Get task_id if available + task_id = None + if hasattr(content, 'writer_task'): + task_id = content.writer_task.id + + # Queue the publishing task + task_result = publish_content_to_wordpress.delay( + content.id, + site_integration.id, + task_id + ) + + return Response( + { + 'success': True, + 'message': 'WordPress sync retry queued', + 'data': { + 'content_id': content.id, + 'task_id': task_result.id, + 'status': 'queued' + } + }, + status=status.HTTP_202_ACCEPTED + ) + + except Exception as e: + return Response( + { + 'success': False, + 'message': f'Error retrying WordPress sync: {str(e)}', + 'error': 'server_error' + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) \ No newline at end of file diff --git a/backend/igny8_core/tasks/wordpress_publishing.py b/backend/igny8_core/tasks/wordpress_publishing.py new file mode 100644 index 00000000..a564d8e5 --- /dev/null +++ b/backend/igny8_core/tasks/wordpress_publishing.py @@ -0,0 +1,385 @@ +""" +IGNY8 Content Publishing Celery Tasks + +Handles automated publishing of content from IGNY8 to WordPress sites. +""" +from celery import shared_task +from django.conf import settings +from django.utils import timezone +from datetime import timedelta +import requests +import logging +from typing import Dict, List, Any, Optional + +logger = logging.getLogger(__name__) + + +@shared_task(bind=True, max_retries=3) +def publish_content_to_wordpress(self, content_id: int, site_integration_id: int, task_id: Optional[int] = None) -> Dict[str, Any]: + """ + Publish a single content item to WordPress + + Args: + content_id: IGNY8 content ID + site_integration_id: WordPress site integration ID + task_id: Optional IGNY8 task ID + + Returns: + Dict with success status and details + """ + try: + from igny8_core.models import ContentPost, SiteIntegration + + # Get content and site integration + try: + content = ContentPost.objects.get(id=content_id) + site_integration = SiteIntegration.objects.get(id=site_integration_id) + except (ContentPost.DoesNotExist, SiteIntegration.DoesNotExist) as e: + logger.error(f"Content or site integration not found: {e}") + return {"success": False, "error": str(e)} + + # Check if content is ready for publishing + if content.wordpress_sync_status == 'success': + logger.info(f"Content {content_id} already published to WordPress") + return {"success": True, "message": "Already published", "wordpress_post_id": content.wordpress_post_id} + + if content.wordpress_sync_status == 'syncing': + logger.info(f"Content {content_id} is currently syncing") + return {"success": False, "error": "Content is currently syncing"} + + # Update status to syncing + content.wordpress_sync_status = 'syncing' + content.save(update_fields=['wordpress_sync_status']) + + # Prepare content data for WordPress + content_data = { + 'content_id': content.id, + 'task_id': task_id, + 'title': content.title, + 'content_html': content.content_html or content.content, + 'excerpt': content.brief or '', + 'status': 'publish', + 'author_email': content.author.email if content.author else None, + 'author_name': content.author.get_full_name() if content.author else None, + 'published_at': content.published_at.isoformat() if content.published_at else None, + 'seo_title': getattr(content, 'seo_title', ''), + 'seo_description': getattr(content, 'seo_description', ''), + 'featured_image_url': content.featured_image.url if content.featured_image else None, + 'sectors': [{'id': s.id, 'name': s.name} for s in content.sectors.all()], + 'clusters': [{'id': c.id, 'name': c.name} for c in content.clusters.all()], + 'tags': getattr(content, 'tags', []), + 'focus_keywords': getattr(content, 'focus_keywords', []) + } + + # Call WordPress REST API + wordpress_url = f"{site_integration.site_url}/wp-json/igny8/v1/publish-content/" + headers = { + 'Content-Type': 'application/json', + 'X-IGNY8-API-KEY': site_integration.api_key, + } + + response = requests.post( + wordpress_url, + json=content_data, + headers=headers, + timeout=30 + ) + + if response.status_code == 201: + # Success + wp_data = response.json().get('data', {}) + content.wordpress_sync_status = 'success' + content.wordpress_post_id = wp_data.get('post_id') + content.wordpress_post_url = wp_data.get('post_url') + content.last_wordpress_sync = timezone.now() + content.save(update_fields=[ + 'wordpress_sync_status', 'wordpress_post_id', + 'wordpress_post_url', 'last_wordpress_sync' + ]) + + logger.info(f"Successfully published content {content_id} to WordPress post {content.wordpress_post_id}") + return { + "success": True, + "wordpress_post_id": content.wordpress_post_id, + "wordpress_post_url": content.wordpress_post_url + } + + elif response.status_code == 409: + # Content already exists + wp_data = response.json().get('data', {}) + content.wordpress_sync_status = 'success' + content.wordpress_post_id = wp_data.get('post_id') + content.last_wordpress_sync = timezone.now() + content.save(update_fields=[ + 'wordpress_sync_status', 'wordpress_post_id', 'last_wordpress_sync' + ]) + + logger.info(f"Content {content_id} already exists on WordPress") + return {"success": True, "message": "Content already exists", "wordpress_post_id": content.wordpress_post_id} + + else: + # Error + error_msg = f"WordPress API error: {response.status_code} - {response.text}" + logger.error(error_msg) + + # Retry logic + if self.request.retries < self.max_retries: + content.wordpress_sync_attempts = (content.wordpress_sync_attempts or 0) + 1 + content.save(update_fields=['wordpress_sync_attempts']) + + # Exponential backoff: 1min, 5min, 15min + countdown = 60 * (5 ** self.request.retries) + raise self.retry(countdown=countdown, exc=Exception(error_msg)) + else: + # Max retries reached + content.wordpress_sync_status = 'failed' + content.last_wordpress_sync = timezone.now() + content.save(update_fields=['wordpress_sync_status', 'last_wordpress_sync']) + + return {"success": False, "error": error_msg} + + except Exception as e: + logger.error(f"Error publishing content {content_id}: {str(e)}") + + # Update content status on error + try: + content = ContentPost.objects.get(id=content_id) + content.wordpress_sync_status = 'failed' + content.last_wordpress_sync = timezone.now() + content.save(update_fields=['wordpress_sync_status', 'last_wordpress_sync']) + except: + pass + + return {"success": False, "error": str(e)} + + +@shared_task +def process_pending_wordpress_publications() -> Dict[str, Any]: + """ + Process all content items pending WordPress publication + Runs every 5 minutes + """ + try: + from igny8_core.models import ContentPost, SiteIntegration + + # Find content marked for WordPress publishing + pending_content = ContentPost.objects.filter( + wordpress_sync_status='pending', + published_at__isnull=False # Only published content + ).select_related('author').prefetch_related('sectors', 'clusters') + + if not pending_content.exists(): + logger.info("No content pending WordPress publication") + return {"success": True, "processed": 0} + + # Get active WordPress integrations + active_integrations = SiteIntegration.objects.filter( + platform='wordpress', + is_active=True, + api_key__isnull=False + ) + + if not active_integrations.exists(): + logger.warning("No active WordPress integrations found") + return {"success": False, "error": "No active WordPress integrations"} + + processed = 0 + failed = 0 + + for content in pending_content[:50]: # Process max 50 at a time + for integration in active_integrations: + # Get task_id if content is associated with a task + task_id = None + if hasattr(content, 'writer_task'): + task_id = content.writer_task.id + + # Queue individual publish task + publish_content_to_wordpress.delay( + content.id, + integration.id, + task_id + ) + processed += 1 + + logger.info(f"Queued {processed} content items for WordPress publication") + return {"success": True, "processed": processed, "failed": failed} + + except Exception as e: + logger.error(f"Error processing pending WordPress publications: {str(e)}") + return {"success": False, "error": str(e)} + + +@shared_task +def bulk_publish_content_to_wordpress(content_ids: List[int], site_integration_id: int) -> Dict[str, Any]: + """ + Bulk publish multiple content items to WordPress + Used for manual bulk operations from Content Manager + """ + try: + from igny8_core.models import ContentPost, SiteIntegration + + site_integration = SiteIntegration.objects.get(id=site_integration_id) + content_items = ContentPost.objects.filter(id__in=content_ids) + + results = { + "success": True, + "total": len(content_ids), + "queued": 0, + "skipped": 0, + "errors": [] + } + + for content in content_items: + try: + # Skip if already published or syncing + if content.wordpress_sync_status in ['success', 'syncing']: + results["skipped"] += 1 + continue + + # Mark as pending and queue + content.wordpress_sync_status = 'pending' + content.save(update_fields=['wordpress_sync_status']) + + # Get task_id if available + task_id = None + if hasattr(content, 'writer_task'): + task_id = content.writer_task.id + + # Queue individual publish task + publish_content_to_wordpress.delay( + content.id, + site_integration.id, + task_id + ) + results["queued"] += 1 + + except Exception as e: + results["errors"].append(f"Content {content.id}: {str(e)}") + + if results["errors"]: + results["success"] = len(results["errors"]) < results["total"] / 2 # Success if < 50% errors + + logger.info(f"Bulk publish: {results['queued']} queued, {results['skipped']} skipped, {len(results['errors'])} errors") + return results + + except Exception as e: + logger.error(f"Error in bulk publish: {str(e)}") + return {"success": False, "error": str(e)} + + +@shared_task +def wordpress_status_reconciliation() -> Dict[str, Any]: + """ + Daily task to reconcile status between IGNY8 and WordPress + Checks for discrepancies and fixes them + """ + try: + from igny8_core.models import ContentPost, SiteIntegration + + # Get content marked as published to WordPress + wp_content = ContentPost.objects.filter( + wordpress_sync_status='success', + wordpress_post_id__isnull=False + ) + + active_integrations = SiteIntegration.objects.filter( + platform='wordpress', + is_active=True + ) + + reconciled = 0 + errors = [] + + for integration in active_integrations: + integration_content = wp_content.filter( + # Assuming there's a way to link content to integration + # This would depend on your data model + ) + + for content in integration_content[:100]: # Limit to prevent timeouts + try: + # Check WordPress post status + wp_url = f"{integration.site_url}/wp-json/igny8/v1/post-status/{content.id}/" + headers = {'X-IGNY8-API-KEY': integration.api_key} + + response = requests.get(wp_url, headers=headers, timeout=10) + + if response.status_code == 200: + wp_data = response.json().get('data', {}) + wp_status = wp_data.get('wordpress_status') + + # Update if status changed + if wp_status == 'trash' and content.wordpress_sync_status == 'success': + content.wordpress_sync_status = 'failed' + content.save(update_fields=['wordpress_sync_status']) + reconciled += 1 + + elif response.status_code == 404: + # Post not found on WordPress + content.wordpress_sync_status = 'failed' + content.wordpress_post_id = None + content.wordpress_post_url = None + content.save(update_fields=[ + 'wordpress_sync_status', 'wordpress_post_id', 'wordpress_post_url' + ]) + reconciled += 1 + + except Exception as e: + errors.append(f"Content {content.id}: {str(e)}") + + logger.info(f"Status reconciliation: {reconciled} reconciled, {len(errors)} errors") + return {"success": True, "reconciled": reconciled, "errors": errors} + + except Exception as e: + logger.error(f"Error in status reconciliation: {str(e)}") + return {"success": False, "error": str(e)} + + +@shared_task +def retry_failed_wordpress_publications() -> Dict[str, Any]: + """ + Retry failed WordPress publications (runs daily) + Only retries items that failed more than 1 hour ago + """ + try: + from igny8_core.models import ContentPost, SiteIntegration + + # Find failed publications older than 1 hour + one_hour_ago = timezone.now() - timedelta(hours=1) + failed_content = ContentPost.objects.filter( + wordpress_sync_status='failed', + last_wordpress_sync__lt=one_hour_ago, + wordpress_sync_attempts__lt=5 # Max 5 total attempts + ) + + active_integrations = SiteIntegration.objects.filter( + platform='wordpress', + is_active=True + ) + + retried = 0 + + for content in failed_content[:20]: # Limit retries per run + for integration in active_integrations: + # Reset status and retry + content.wordpress_sync_status = 'pending' + content.save(update_fields=['wordpress_sync_status']) + + task_id = None + if hasattr(content, 'writer_task'): + task_id = content.writer_task.id + + publish_content_to_wordpress.delay( + content.id, + integration.id, + task_id + ) + retried += 1 + break # Only retry with first active integration + + logger.info(f"Retried {retried} failed WordPress publications") + return {"success": True, "retried": retried} + + except Exception as e: + logger.error(f"Error retrying failed publications: {str(e)}") + return {"success": False, "error": str(e)} \ No newline at end of file diff --git a/backend/igny8_core/urls/wordpress_publishing.py b/backend/igny8_core/urls/wordpress_publishing.py new file mode 100644 index 00000000..4a6e6765 --- /dev/null +++ b/backend/igny8_core/urls/wordpress_publishing.py @@ -0,0 +1,38 @@ +""" +URL configuration for WordPress publishing endpoints +""" +from django.urls import path +from igny8_core.api.wordpress_publishing import ( + publish_single_content, + bulk_publish_content, + get_wordpress_status, + get_wordpress_integrations, + retry_failed_wordpress_sync, +) + +urlpatterns = [ + # Single content publishing + path('content//publish-to-wordpress/', + publish_single_content, + name='publish_single_content'), + + # Bulk content publishing + path('content/bulk-publish-to-wordpress/', + bulk_publish_content, + name='bulk_publish_content'), + + # WordPress status + path('content//wordpress-status/', + get_wordpress_status, + name='get_wordpress_status'), + + # WordPress integrations list + path('wordpress-integrations/', + get_wordpress_integrations, + name='get_wordpress_integrations'), + + # Retry failed sync + path('content//retry-wordpress-sync/', + retry_failed_wordpress_sync, + name='retry_failed_wordpress_sync'), +] \ No newline at end of file diff --git a/frontend/src/config/pages/table-actions.config.tsx b/frontend/src/config/pages/table-actions.config.tsx index baec163a..99b6a39e 100644 --- a/frontend/src/config/pages/table-actions.config.tsx +++ b/frontend/src/config/pages/table-actions.config.tsx @@ -321,6 +321,16 @@ const tableActionsConfigs: Record = { }, '/writer/images': { rowActions: [ + { + key: 'publish_wordpress', + label: 'Publish to WordPress', + icon: , + variant: 'success', + shouldShow: (row: any) => { + // Only show if images are generated (complete) - WordPress status is tracked separately + return row.overall_status === 'complete'; + }, + }, { key: 'update_status', label: 'Update Status', @@ -328,7 +338,14 @@ const tableActionsConfigs: Record = { variant: 'primary', }, ], - bulkActions: [], + bulkActions: [ + { + key: 'bulk_publish_wordpress', + label: 'Publish Ready to WordPress', + icon: , + variant: 'success', + }, + ], }, // Default config (fallback) default: { diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index d04b579f..0f544871 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -14,6 +14,7 @@ import { bulkUpdateImagesStatus, ContentImage, fetchAPI, + publishContent, } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, DownloadIcon, BoltIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons'; @@ -207,8 +208,50 @@ export default function Images() { // Bulk action handler const handleBulkAction = useCallback(async (action: string, ids: string[]) => { - toast.info(`Bulk action "${action}" for ${ids.length} items`); - }, [toast]); + if (action === 'bulk_publish_wordpress') { + // Filter to only publish items that have images generated + const readyItems = images + .filter(item => ids.includes(item.content_id.toString())) + .filter(item => item.overall_status === 'complete'); + + if (readyItems.length === 0) { + toast.warning('No items are ready for WordPress publishing. Items must have generated images and not already be published.'); + return; + } + + try { + let successCount = 0; + let failedCount = 0; + const errors: string[] = []; + + // Process each item individually using the existing publishContent function + for (const item of readyItems) { + try { + await publishContent(item.content_id); + successCount++; + } catch (error: any) { + failedCount++; + errors.push(`${item.content_title}: ${error.message}`); + } + } + + if (successCount > 0) { + toast.success(`Successfully published ${successCount} item(s) to WordPress`); + } + if (failedCount > 0) { + toast.warning(`${failedCount} item(s) failed to publish`); + } + + // Reload images to reflect the updated WordPress status + loadImages(); + } catch (error: any) { + console.error('Bulk WordPress publish error:', error); + toast.error(`Failed to bulk publish to WordPress: ${error.message || 'Network error'}`); + } + } else { + toast.info(`Bulk action "${action}" for ${ids.length} items`); + } + }, [images, toast, loadImages]); // Row action handler const handleRowAction = useCallback(async (action: string, row: ContentImagesGroup) => { @@ -216,8 +259,20 @@ export default function Images() { setStatusUpdateContentId(row.content_id); setStatusUpdateRecordName(row.content_title || `Content #${row.content_id}`); setIsStatusModalOpen(true); + } else if (action === 'publish_wordpress') { + // Handle WordPress publishing for individual item using existing publishContent function + try { + // Use the existing publishContent function from the API + const result = await publishContent(row.content_id); + toast.success(`Successfully published "${row.content_title}" to WordPress! View at: ${result.external_url}`); + // Reload images to reflect the updated WordPress status + loadImages(); + } catch (error: any) { + console.error('WordPress publish error:', error); + toast.error(`Failed to publish to WordPress: ${error.message || 'Network error'}`); + } } - }, []); + }, [loadImages, toast]); // Handle status update confirmation const handleStatusUpdate = useCallback(async (status: string) => {