diff --git a/WORDPRESS_PUBLISH_UI_CHANGES.md b/WORDPRESS_PUBLISH_UI_CHANGES.md new file mode 100644 index 00000000..8db37392 --- /dev/null +++ b/WORDPRESS_PUBLISH_UI_CHANGES.md @@ -0,0 +1,101 @@ +# WordPress Publishing UI Update Summary + +## Changes Made + +### 🚀 **MOVED** WordPress Publishing from Content Page to Images Page + +**Reasoning**: Content page only contains text content without generated images, making it premature to publish. Images page contains complete content with generated images, making it the optimal place for publishing. + +### 📍 **WordPress Publishing Now Available On Images Page** + +#### **1. Individual Content Publishing** +- **Location**: 3-dot dropdown menu on each row in `/writer/images` +- **Visibility**: Only shows "Publish to WordPress" when: + - ✅ Images are generated (status = 'complete') + - ✅ Not already published/publishing to WordPress +- **Action**: Single click publishes individual content with all images, SEO metadata, categories, and content + +#### **2. Bulk Publishing** +- **Location**: Top toolbar next to "Columns" selector in `/writer/images` +- **Button Text**: "Publish Ready ({count})" - dynamically shows count of ready-to-publish items +- **Visibility**: Only appears when there are items ready to publish +- **Conditions**: + - ✅ Images must be generated + - ✅ Not already published + - ✅ Not currently publishing +- **Action**: Opens dialog showing all ready items, allows bulk publish with progress tracking + +#### **3. Smart Status Checks** +- Uses existing image generation status badges/logic +- Automatically filters eligible content +- Real-time status updates after publishing +- Error handling with detailed feedback + +### 🔄 **Updated Components** + +#### **Enhanced WordPress Publishing Components** +``` +frontend/src/components/WordPressPublish/ +├── WordPressPublish.tsx # Enhanced with image status checks +├── BulkWordPressPublish.tsx # NEW: Bulk publishing with progress +├── ContentActionsMenu.tsx # NEW: Smart dropdown with conditional visibility +└── index.ts # Export all components +``` + +#### **Updated Page Configuration** +``` +frontend/src/config/pages/table-actions.config.tsx +├── /writer/images # Added WordPress actions +│ ├── rowActions[] # "Publish to WordPress" (conditional) +│ └── bulkActions[] # "Publish Ready to WordPress" +└── /writer/content # Removed WordPress actions + └── rowActions[] # Removed publish/unpublish +``` + +#### **Updated Pages** +``` +frontend/src/pages/Writer/ +├── Images.tsx # Added WordPress publish handling +│ ├── handleRowAction() # WordPress single publish +│ ├── handleBulkAction() # WordPress bulk publish +│ └── import { api } # API for WordPress calls +└── Content.tsx # Removed WordPress functionality + ├── handleRowAction() # Removed publish/unpublish logic + └── imports # Removed publishContent, unpublishContent +``` + +### 🎯 **User Experience Improvements** + +#### **Before** (Content Page) +- ❌ WordPress publish available even when images not ready +- ❌ Users would publish incomplete content +- ❌ Required manual coordination between content creation and image generation + +#### **After** (Images Page) +- ✅ Publish only when content is complete with images +- ✅ Smart button visibility based on actual readiness +- ✅ Clear labeling: "Publish Ready (X)" shows exactly what's eligible +- ✅ Bulk operations for efficiency +- ✅ Real-time status tracking and feedback + +### 📋 **Status Explanations** + +The UI now uses short but explanatory labels: +- **"Publish Ready (X)"** - X items have generated images and are ready for WordPress +- **"Awaiting Images"** - Individual items waiting for image generation +- **"Images Pending"** - Status chip when images aren't complete +- **"Images Generated ✓"** - Confirmation in publish dialog + +### 🔧 **Technical Implementation** + +1. **Conditional Rendering**: Uses `shouldShow` functions to intelligently display actions +2. **Status Integration**: Leverages existing image generation status tracking +3. **API Integration**: Seamless connection to WordPress publishing endpoints +4. **Error Handling**: Comprehensive error messages and retry logic +5. **State Management**: Automatic reload of data after publishing actions + +### 🎊 **Result** + +Users now have a **streamlined, intelligent WordPress publishing workflow** that prevents premature publishing and ensures complete content (text + images) is always published together. + +The system automatically guides users to publish only when content is truly ready, improving content quality and user experience. \ No newline at end of file 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 b1d369fc..d020db0f 100644 --- a/frontend/src/components/WordPressPublish/WordPressPublish.tsx +++ b/frontend/src/components/WordPressPublish/WordPressPublish.tsx @@ -23,12 +23,14 @@ import { } from '@mui/icons-material'; import { api } from '../../services/api'; -interface WordPressPublishProps { +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,6 +197,35 @@ 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') { return ( @@ -300,6 +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 new file mode 100644 index 00000000..5fa86535 --- /dev/null +++ b/frontend/src/components/WordPressPublish/index.ts @@ -0,0 +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 3ce5a744..f7018c45 100644 --- a/frontend/src/config/pages/table-actions.config.tsx +++ b/frontend/src/config/pages/table-actions.config.tsx @@ -258,13 +258,7 @@ const tableActionsConfigs: Record = { icon: EditIcon, variant: 'primary', }, - { - key: 'publish', - label: 'Publish to WordPress', - icon: , - variant: 'success', - shouldShow: (row: any) => !row.external_id, // Only show if not published - }, + { key: 'view_on_wordpress', label: 'View on WordPress', @@ -272,13 +266,7 @@ const tableActionsConfigs: Record = { variant: 'secondary', shouldShow: (row: any) => !!row.external_id, // Only show if published }, - { - key: 'unpublish', - label: 'Unpublish', - icon: , - variant: 'secondary', - shouldShow: (row: any) => !!row.external_id, // Only show if published - }, + { key: 'generate_image_prompts', label: 'Generate Image Prompts', @@ -333,6 +321,18 @@ 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 and not already published/publishing + return row.status === 'complete' && + (!row.wordpress_status || + (row.wordpress_status !== 'published' && row.wordpress_status !== 'publishing')); + }, + }, { key: 'update_status', label: 'Update Status', @@ -340,7 +340,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/Content.tsx b/frontend/src/pages/Writer/Content.tsx index fc14884a..a6dfbafb 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -10,8 +10,6 @@ import { Content as ContentType, ContentFilters, generateImagePrompts, - publishContent, - unpublishContent, } from '../../services/api'; import { optimizerApi } from '../../api/optimizer.api'; import { useNavigate } from 'react-router'; @@ -164,40 +162,12 @@ export default function Content() { const navigate = useNavigate(); const handleRowAction = useCallback(async (action: string, row: ContentType) => { - if (action === 'publish') { - try { - // Check if already published - if (row.external_id) { - toast.warning('Content is already published to WordPress'); - return; - } - - const result = await publishContent(row.id); - toast.success(`Content published successfully! View at: ${result.external_url}`); - loadContent(); // Reload to show updated external_id - } catch (error: any) { - toast.error(`Failed to publish content: ${error.message}`); - } - } else if (action === 'view_on_wordpress') { + if (action === 'view_on_wordpress') { if (row.external_url) { window.open(row.external_url, '_blank'); } else { toast.warning('WordPress URL not available'); } - } else if (action === 'unpublish') { - try { - // Check if not published - if (!row.external_id) { - toast.warning('Content is not currently published'); - return; - } - - await unpublishContent(row.id); - toast.success('Content unpublished successfully'); - loadContent(); // Reload to show cleared external_id - } catch (error: any) { - toast.error(`Failed to unpublish content: ${error.message}`); - } } else if (action === 'generate_image_prompts') { try { const result = await generateImagePrompts([row.id]); diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index 8c9f4815..3b58cd51 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -13,6 +13,7 @@ import { generateImages, bulkUpdateImagesStatus, ContentImage, + api, } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, DownloadIcon, BoltIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons'; @@ -206,8 +207,49 @@ 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 and are not already published + const readyItems = images + .filter(item => ids.includes(item.content_id.toString())) + .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.'); + return; + } + + try { + const response = await api.post('/api/wordpress/bulk-publish/', { + content_ids: readyItems.map(item => item.content_id.toString()) + }); + + 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}`); + } + } 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) => { @@ -215,8 +257,26 @@ 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 + try { + 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'}`); + } } - }, []); + }, [loadImages, toast]); // Handle status update confirmation const handleStatusUpdate = useCallback(async (status: string) => {