diff --git a/frontend/src/components/WordPressPublish/BulkWordPressPublish.tsx b/frontend/src/components/WordPressPublish/BulkWordPressPublish.tsx new file mode 100644 index 00000000..f6e790d9 --- /dev/null +++ b/frontend/src/components/WordPressPublish/BulkWordPressPublish.tsx @@ -0,0 +1,260 @@ +import React, { useState } from 'react'; +import { + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Typography, + Alert, + Box, + CircularProgress, + List, + ListItem, + ListItemText, + Divider +} from '@mui/material'; +import { + Publish as PublishIcon, + CheckCircle as SuccessIcon, + Error as ErrorIcon +} from '@mui/icons-material'; +import { api } from '../../services/api'; + +interface BulkWordPressPublishProps { + contentItems: Array<{ + id: string; + title: string; + imageGenerationStatus: 'pending' | 'generating' | 'complete' | 'failed'; + wordpressStatus: 'draft' | 'publishing' | 'published' | 'failed'; + }>; + onPublishComplete?: (results: { success: string[], failed: string[] }) => void; +} + +interface PublishResult { + id: string; + title: string; + status: 'success' | 'failed' | 'pending'; + message?: string; +} + +export const BulkWordPressPublish: React.FC = ({ + contentItems, + onPublishComplete +}) => { + const [open, setOpen] = useState(false); + const [publishing, setPublishing] = useState(false); + const [results, setResults] = useState([]); + + // Filter items that are ready to publish + const readyToPublish = contentItems.filter(item => + item.imageGenerationStatus === 'complete' && + item.wordpressStatus !== 'published' && + item.wordpressStatus !== 'publishing' + ); + + const handleBulkPublish = async () => { + if (readyToPublish.length === 0) return; + + setPublishing(true); + setResults([]); + + try { + const response = await api.post('/api/wordpress/bulk-publish/', { + content_ids: readyToPublish.map(item => item.id) + }); + + if (response.data.success) { + const publishResults: PublishResult[] = response.data.data.results.map((result: any) => ({ + id: result.content_id, + title: readyToPublish.find(item => item.id === result.content_id)?.title || 'Unknown', + status: result.success ? 'success' : 'failed', + message: result.message + })); + + setResults(publishResults); + + // Notify parent component + if (onPublishComplete) { + const success = publishResults.filter(r => r.status === 'success').map(r => r.id); + const failed = publishResults.filter(r => r.status === 'failed').map(r => r.id); + onPublishComplete({ success, failed }); + } + } else { + // Handle API error + const failedResults: PublishResult[] = readyToPublish.map(item => ({ + id: item.id, + title: item.title, + status: 'failed', + message: response.data.message || 'Failed to publish' + })); + setResults(failedResults); + } + } catch (error) { + console.error('Bulk publish error:', error); + const failedResults: PublishResult[] = readyToPublish.map(item => ({ + id: item.id, + title: item.title, + status: 'failed', + message: 'Network error or server unavailable' + })); + setResults(failedResults); + } finally { + setPublishing(false); + } + }; + + const handleClose = () => { + if (!publishing) { + setOpen(false); + setResults([]); + } + }; + + const successCount = results.filter(r => r.status === 'success').length; + const failedCount = results.filter(r => r.status === 'failed').length; + + if (readyToPublish.length === 0) { + return null; // Don't show button if nothing to publish + } + + return ( + <> + + + + + Bulk Publish to WordPress + + + + {!publishing && results.length === 0 && ( + <> + + Ready to publish {readyToPublish.length} content items to WordPress: + + + + Only content with generated images and not yet published will be included. + + + + + {readyToPublish.map((item, index) => ( +
+ + + + {index < readyToPublish.length - 1 && } +
+ ))} +
+
+ + )} + + {publishing && ( + + + + Publishing {readyToPublish.length} items to WordPress... + + + )} + + {!publishing && results.length > 0 && ( + <> + + {successCount > 0 && ( + + ✓ Successfully published {successCount} items + + )} + + {failedCount > 0 && ( + + ✗ Failed to publish {failedCount} items + + )} + + + + Results: + + + + + {results.map((result, index) => ( +
+ + + {result.status === 'success' ? ( + + ) : ( + + )} + + + + {index < results.length - 1 && } +
+ ))} +
+
+ + )} +
+ + + {!publishing && results.length === 0 && ( + <> + + + + )} + + {publishing && ( + + )} + + {!publishing && results.length > 0 && ( + + )} + +
+ + ); +}; \ No newline at end of file diff --git a/frontend/src/components/WordPressPublish/ContentActionsMenu.tsx b/frontend/src/components/WordPressPublish/ContentActionsMenu.tsx new file mode 100644 index 00000000..3a87de06 --- /dev/null +++ b/frontend/src/components/WordPressPublish/ContentActionsMenu.tsx @@ -0,0 +1,166 @@ +import React, { useState } from 'react'; +import { + IconButton, + Menu, + MenuItem, + ListItemIcon, + ListItemText, + Divider +} from '@mui/material'; +import { + MoreVert as MoreVertIcon, + Publish as PublishIcon, + Edit as EditIcon, + Image as ImageIcon, + GetApp as ExportIcon, + Delete as DeleteIcon +} from '@mui/icons-material'; +import { WordPressPublish } from './WordPressPublish'; + +interface ContentActionsMenuProps { + contentId: string; + contentTitle: string; + imageGenerationStatus: 'pending' | 'generating' | 'complete' | 'failed'; + wordpressStatus: 'draft' | 'publishing' | 'published' | 'failed'; + onEdit?: () => void; + onGenerateImage?: () => void; + onExport?: () => void; + onDelete?: () => void; + onWordPressStatusChange?: (status: string) => void; +} + +export const ContentActionsMenu: React.FC = ({ + contentId, + contentTitle, + imageGenerationStatus, + wordpressStatus, + onEdit, + onGenerateImage, + onExport, + onDelete, + onWordPressStatusChange +}) => { + const [anchorEl, setAnchorEl] = useState(null); + const [showWordPressDialog, setShowWordPressDialog] = useState(false); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handlePublishClick = () => { + setShowWordPressDialog(true); + handleClose(); + }; + + const handleMenuAction = (action: () => void) => { + action(); + handleClose(); + }; + + // Check if WordPress publishing is available + const canPublishToWordPress = imageGenerationStatus === 'complete' && + wordpressStatus !== 'published' && + wordpressStatus !== 'publishing'; + + return ( + <> + + + + + + {/* WordPress Publishing - Only show if images are ready */} + {canPublishToWordPress && ( + <> + + + + + Publish to WordPress + + + + )} + + {/* Edit Action */} + {onEdit && ( + handleMenuAction(onEdit)}> + + + + Edit + + )} + + {/* Generate Image Action */} + {onGenerateImage && ( + handleMenuAction(onGenerateImage)}> + + + + Generate Image Prompts + + )} + + {/* Export Action */} + {onExport && ( + handleMenuAction(onExport)}> + + + + Export + + )} + + {/* Delete Action */} + {onDelete && ( + <> + + handleMenuAction(onDelete)} sx={{ color: 'error.main' }}> + + + + Delete + + + )} + + + {/* WordPress Publish Dialog */} + {showWordPressDialog && ( + + )} + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/WordPressPublish/WordPressPublish.tsx b/frontend/src/components/WordPressPublish/WordPressPublish.tsx index e5a97f4a..d020db0f 100644 --- a/frontend/src/components/WordPressPublish/WordPressPublish.tsx +++ b/frontend/src/components/WordPressPublish/WordPressPublish.tsx @@ -27,8 +27,10 @@ export interface WordPressPublishProps { contentId: string; contentTitle: string; currentStatus?: 'draft' | 'publishing' | 'published' | 'failed'; + imageGenerationStatus?: 'pending' | 'generating' | 'complete' | 'failed'; onStatusChange?: (status: string) => void; size?: 'small' | 'medium' | 'large'; + showOnlyIfImagesReady?: boolean; } interface WordPressStatus { @@ -43,8 +45,10 @@ export const WordPressPublish: React.FC = ({ contentId, contentTitle, currentStatus = 'draft', + imageGenerationStatus = 'pending', onStatusChange, - size = 'medium' + size = 'medium', + showOnlyIfImagesReady = false }) => { const [wpStatus, setWpStatus] = useState(null); const [loading, setLoading] = useState(false); @@ -193,7 +197,34 @@ export const WordPressPublish: React.FC = ({ const statusInfo = getStatusInfo(); - + // Don't show publish button if images aren't ready and showOnlyIfImagesReady is true + const shouldShowPublishButton = !showOnlyIfImagesReady || imageGenerationStatus === 'complete'; + + if (!shouldShowPublishButton) { + return ( + + + + {size !== 'small' && ( + } + label="Images Pending" + color="warning" + size="small" + variant="outlined" + /> + )} + + + ); + } const renderButton = () => { if (size === 'small') { @@ -302,7 +333,18 @@ export const WordPressPublish: React.FC = ({ This will create a new post on your connected WordPress site with all content, images, categories, and SEO metadata. - + + {imageGenerationStatus === 'complete' && ( + + ✓ Images are generated and ready for publishing + + )} + + {imageGenerationStatus !== 'complete' && showOnlyIfImagesReady && ( + + Images are still being generated. Please wait before publishing. + + )} {wpStatus?.wordpress_sync_status === 'success' && ( diff --git a/frontend/src/components/WordPressPublish/index.ts b/frontend/src/components/WordPressPublish/index.ts index a28104ec..5fa86535 100644 --- a/frontend/src/components/WordPressPublish/index.ts +++ b/frontend/src/components/WordPressPublish/index.ts @@ -1,2 +1,4 @@ export { WordPressPublish } from './WordPressPublish'; +export { BulkWordPressPublish } from './BulkWordPressPublish'; +export { ContentActionsMenu } from './ContentActionsMenu'; export type { WordPressPublishProps } from './WordPressPublish'; \ 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 99b6a39e..f7018c45 100644 --- a/frontend/src/config/pages/table-actions.config.tsx +++ b/frontend/src/config/pages/table-actions.config.tsx @@ -327,8 +327,10 @@ const tableActionsConfigs: Record = { icon: , variant: 'success', shouldShow: (row: any) => { - // Only show if images are generated (complete) - WordPress status is tracked separately - return row.overall_status === 'complete'; + // Only show if images are generated and not already published/publishing + return row.status === 'complete' && + (!row.wordpress_status || + (row.wordpress_status !== 'published' && row.wordpress_status !== 'publishing')); }, }, { diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index 0f544871..3b58cd51 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -13,8 +13,7 @@ import { generateImages, bulkUpdateImagesStatus, ContentImage, - fetchAPI, - publishContent, + api, } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, DownloadIcon, BoltIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons'; @@ -209,10 +208,12 @@ export default function Images() { // Bulk action handler const handleBulkAction = useCallback(async (action: string, ids: string[]) => { if (action === 'bulk_publish_wordpress') { - // Filter to only publish items that have images generated + // Filter to only publish items that have images generated and are not already published const readyItems = images .filter(item => ids.includes(item.content_id.toString())) - .filter(item => item.overall_status === 'complete'); + .filter(item => item.status === 'complete' && + (!item.wordpress_status || + (item.wordpress_status !== 'published' && item.wordpress_status !== 'publishing'))); if (readyItems.length === 0) { toast.warning('No items are ready for WordPress publishing. Items must have generated images and not already be published.'); @@ -220,30 +221,27 @@ export default function Images() { } try { - let successCount = 0; - let failedCount = 0; - const errors: string[] = []; + const response = await api.post('/api/wordpress/bulk-publish/', { + content_ids: readyItems.map(item => item.content_id.toString()) + }); - // 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 (response.data.success) { + const results = response.data.data.results; + const successCount = results.filter((r: any) => r.success).length; + const failedCount = results.filter((r: any) => !r.success).length; + + 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(); + } else { + toast.error(`Bulk publish failed: ${response.data.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'}`); @@ -260,13 +258,19 @@ export default function Images() { 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 + // Handle WordPress publishing for individual item 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(); + const response = await api.post('/api/wordpress/publish/', { + content_id: row.content_id.toString() + }); + + if (response.data.success) { + toast.success(`Successfully published "${row.content_title}" to WordPress`); + // Reload images to reflect the updated WordPress status + loadImages(); + } else { + toast.error(`Failed to publish: ${response.data.message}`); + } } catch (error: any) { console.error('WordPress publish error:', error); toast.error(`Failed to publish to WordPress: ${error.message || 'Network error'}`);