publish to wp

This commit is contained in:
alorig
2025-11-28 12:35:02 +05:00
parent 081f94ffdb
commit f76e791de7
8 changed files with 664 additions and 51 deletions

View File

@@ -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.

View File

@@ -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<BulkWordPressPublishProps> = ({
contentItems,
onPublishComplete
}) => {
const [open, setOpen] = useState(false);
const [publishing, setPublishing] = useState(false);
const [results, setResults] = useState<PublishResult[]>([]);
// 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 (
<>
<Button
variant="contained"
color="primary"
startIcon={<PublishIcon />}
onClick={() => setOpen(true)}
size="small"
>
Publish Ready ({readyToPublish.length})
</Button>
<Dialog
open={open}
onClose={handleClose}
maxWidth="md"
fullWidth
disableEscapeKeyDown={publishing}
>
<DialogTitle>
Bulk Publish to WordPress
</DialogTitle>
<DialogContent>
{!publishing && results.length === 0 && (
<>
<Typography variant="body1" gutterBottom>
Ready to publish <strong>{readyToPublish.length}</strong> content items to WordPress:
</Typography>
<Alert severity="info" sx={{ mt: 2, mb: 2 }}>
Only content with generated images and not yet published will be included.
</Alert>
<Box sx={{ maxHeight: 300, overflow: 'auto', border: 1, borderColor: 'divider', borderRadius: 1 }}>
<List dense>
{readyToPublish.map((item, index) => (
<div key={item.id}>
<ListItem>
<ListItemText
primary={item.title}
secondary={`ID: ${item.id}`}
/>
</ListItem>
{index < readyToPublish.length - 1 && <Divider />}
</div>
))}
</List>
</Box>
</>
)}
{publishing && (
<Box display="flex" alignItems="center" gap={2} py={4}>
<CircularProgress />
<Typography>
Publishing {readyToPublish.length} items to WordPress...
</Typography>
</Box>
)}
{!publishing && results.length > 0 && (
<>
<Box sx={{ mb: 2 }}>
{successCount > 0 && (
<Alert severity="success" sx={{ mb: 1 }}>
Successfully published {successCount} items
</Alert>
)}
{failedCount > 0 && (
<Alert severity="error">
Failed to publish {failedCount} items
</Alert>
)}
</Box>
<Typography variant="h6" gutterBottom>
Results:
</Typography>
<Box sx={{ maxHeight: 400, overflow: 'auto', border: 1, borderColor: 'divider', borderRadius: 1 }}>
<List dense>
{results.map((result, index) => (
<div key={result.id}>
<ListItem>
<Box display="flex" alignItems="center" width="100%">
{result.status === 'success' ? (
<SuccessIcon color="success" sx={{ mr: 1 }} />
) : (
<ErrorIcon color="error" sx={{ mr: 1 }} />
)}
<ListItemText
primary={result.title}
secondary={result.message || (result.status === 'success' ? 'Published successfully' : 'Publishing failed')}
/>
</Box>
</ListItem>
{index < results.length - 1 && <Divider />}
</div>
))}
</List>
</Box>
</>
)}
</DialogContent>
<DialogActions>
{!publishing && results.length === 0 && (
<>
<Button onClick={handleClose}>
Cancel
</Button>
<Button
onClick={handleBulkPublish}
variant="contained"
color="primary"
startIcon={<PublishIcon />}
>
Publish All ({readyToPublish.length})
</Button>
</>
)}
{publishing && (
<Button disabled>
Publishing...
</Button>
)}
{!publishing && results.length > 0 && (
<Button onClick={handleClose} variant="contained">
Close
</Button>
)}
</DialogActions>
</Dialog>
</>
);
};

View File

@@ -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<ContentActionsMenuProps> = ({
contentId,
contentTitle,
imageGenerationStatus,
wordpressStatus,
onEdit,
onGenerateImage,
onExport,
onDelete,
onWordPressStatusChange
}) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [showWordPressDialog, setShowWordPressDialog] = useState(false);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
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 (
<>
<IconButton
aria-label="more actions"
id="content-actions-button"
aria-controls={open ? 'content-actions-menu' : undefined}
aria-expanded={open ? 'true' : undefined}
aria-haspopup="true"
onClick={handleClick}
size="small"
>
<MoreVertIcon />
</IconButton>
<Menu
id="content-actions-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'content-actions-button',
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
{/* WordPress Publishing - Only show if images are ready */}
{canPublishToWordPress && (
<>
<MenuItem onClick={handlePublishClick}>
<ListItemIcon>
<PublishIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Publish to WordPress</ListItemText>
</MenuItem>
<Divider />
</>
)}
{/* Edit Action */}
{onEdit && (
<MenuItem onClick={() => handleMenuAction(onEdit)}>
<ListItemIcon>
<EditIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Edit</ListItemText>
</MenuItem>
)}
{/* Generate Image Action */}
{onGenerateImage && (
<MenuItem onClick={() => handleMenuAction(onGenerateImage)}>
<ListItemIcon>
<ImageIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Generate Image Prompts</ListItemText>
</MenuItem>
)}
{/* Export Action */}
{onExport && (
<MenuItem onClick={() => handleMenuAction(onExport)}>
<ListItemIcon>
<ExportIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Export</ListItemText>
</MenuItem>
)}
{/* Delete Action */}
{onDelete && (
<>
<Divider />
<MenuItem onClick={() => handleMenuAction(onDelete)} sx={{ color: 'error.main' }}>
<ListItemIcon>
<DeleteIcon fontSize="small" color="error" />
</ListItemIcon>
<ListItemText>Delete</ListItemText>
</MenuItem>
</>
)}
</Menu>
{/* WordPress Publish Dialog */}
{showWordPressDialog && (
<WordPressPublish
contentId={contentId}
contentTitle={contentTitle}
currentStatus={wordpressStatus}
imageGenerationStatus={imageGenerationStatus}
onStatusChange={onWordPressStatusChange}
showOnlyIfImagesReady={true}
size="medium"
/>
)}
</>
);
};

View File

@@ -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<WordPressPublishProps> = ({
contentId,
contentTitle,
currentStatus = 'draft',
imageGenerationStatus = 'pending',
onStatusChange,
size = 'medium'
size = 'medium',
showOnlyIfImagesReady = false
}) => {
const [wpStatus, setWpStatus] = useState<WordPressStatus | null>(null);
const [loading, setLoading] = useState(false);
@@ -193,6 +197,35 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
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 (
<Tooltip title={`Images must be generated before publishing to WordPress`}>
<Box display="flex" alignItems="center" gap={1}>
<Button
variant="outlined"
disabled
size={size}
startIcon={<PendingIcon />}
>
Awaiting Images
</Button>
{size !== 'small' && (
<Chip
icon={<PendingIcon />}
label="Images Pending"
color="warning"
size="small"
variant="outlined"
/>
)}
</Box>
</Tooltip>
);
}
const renderButton = () => {
if (size === 'small') {
return (
@@ -301,6 +334,18 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
images, categories, and SEO metadata.
</Typography>
{imageGenerationStatus === 'complete' && (
<Alert severity="success" sx={{ mt: 2 }}>
Images are generated and ready for publishing
</Alert>
)}
{imageGenerationStatus !== 'complete' && showOnlyIfImagesReady && (
<Alert severity="warning" sx={{ mt: 2 }}>
Images are still being generated. Please wait before publishing.
</Alert>
)}
{wpStatus?.wordpress_sync_status === 'success' && (
<Alert severity="info" sx={{ mt: 2 }}>
This content is already published to WordPress.

View File

@@ -0,0 +1,4 @@
export { WordPressPublish } from './WordPressPublish';
export { BulkWordPressPublish } from './BulkWordPressPublish';
export { ContentActionsMenu } from './ContentActionsMenu';
export type { WordPressPublishProps } from './WordPressPublish';

View File

@@ -258,13 +258,7 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
icon: EditIcon,
variant: 'primary',
},
{
key: 'publish',
label: 'Publish to WordPress',
icon: <CheckCircleIcon className="w-5 h-5 text-success-500" />,
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<string, TableActionsConfig> = {
variant: 'secondary',
shouldShow: (row: any) => !!row.external_id, // Only show if published
},
{
key: 'unpublish',
label: 'Unpublish',
icon: <TrashBinIcon className="w-5 h-5" />,
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<string, TableActionsConfig> = {
},
'/writer/images': {
rowActions: [
{
key: 'publish_wordpress',
label: 'Publish to WordPress',
icon: <ArrowRightIcon className="w-5 h-5" />,
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<string, TableActionsConfig> = {
variant: 'primary',
},
],
bulkActions: [],
bulkActions: [
{
key: 'bulk_publish_wordpress',
label: 'Publish Ready to WordPress',
icon: <ArrowRightIcon className="w-5 h-5" />,
variant: 'success',
},
],
},
// Default config (fallback)
default: {

View File

@@ -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]);

View File

@@ -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[]) => {
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`);
}, [toast]);
}
}, [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) => {