Revert "12"

This reverts commit 636b7ddca9.
This commit is contained in:
alorig
2025-11-28 13:33:27 +05:00
parent 3fcba76d0b
commit e360c5fede
6 changed files with 512 additions and 36 deletions

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

@@ -27,8 +27,10 @@ export interface WordPressPublishProps {
contentId: string; contentId: string;
contentTitle: string; contentTitle: string;
currentStatus?: 'draft' | 'publishing' | 'published' | 'failed'; currentStatus?: 'draft' | 'publishing' | 'published' | 'failed';
imageGenerationStatus?: 'pending' | 'generating' | 'complete' | 'failed';
onStatusChange?: (status: string) => void; onStatusChange?: (status: string) => void;
size?: 'small' | 'medium' | 'large'; size?: 'small' | 'medium' | 'large';
showOnlyIfImagesReady?: boolean;
} }
interface WordPressStatus { interface WordPressStatus {
@@ -43,8 +45,10 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
contentId, contentId,
contentTitle, contentTitle,
currentStatus = 'draft', currentStatus = 'draft',
imageGenerationStatus = 'pending',
onStatusChange, onStatusChange,
size = 'medium' size = 'medium',
showOnlyIfImagesReady = false
}) => { }) => {
const [wpStatus, setWpStatus] = useState<WordPressStatus | null>(null); const [wpStatus, setWpStatus] = useState<WordPressStatus | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -193,7 +197,34 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
const statusInfo = getStatusInfo(); 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 = () => { const renderButton = () => {
if (size === 'small') { if (size === 'small') {
@@ -302,7 +333,18 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
This will create a new post on your connected WordPress site with all content, This will create a new post on your connected WordPress site with all content,
images, categories, and SEO metadata. images, categories, and SEO metadata.
</Typography> </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' && ( {wpStatus?.wordpress_sync_status === 'success' && (
<Alert severity="info" sx={{ mt: 2 }}> <Alert severity="info" sx={{ mt: 2 }}>

View File

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

View File

@@ -327,8 +327,10 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
icon: <ArrowRightIcon className="w-5 h-5" />, icon: <ArrowRightIcon className="w-5 h-5" />,
variant: 'success', variant: 'success',
shouldShow: (row: any) => { shouldShow: (row: any) => {
// Only show if images are generated (complete) - WordPress status is tracked separately // Only show if images are generated and not already published/publishing
return row.overall_status === 'complete'; return row.status === 'complete' &&
(!row.wordpress_status ||
(row.wordpress_status !== 'published' && row.wordpress_status !== 'publishing'));
}, },
}, },
{ {

View File

@@ -13,8 +13,7 @@ import {
generateImages, generateImages,
bulkUpdateImagesStatus, bulkUpdateImagesStatus,
ContentImage, ContentImage,
fetchAPI, api,
publishContent,
} from '../../services/api'; } from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer'; import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon, DownloadIcon, BoltIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons'; import { FileIcon, DownloadIcon, BoltIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons';
@@ -209,10 +208,12 @@ export default function Images() {
// Bulk action handler // Bulk action handler
const handleBulkAction = useCallback(async (action: string, ids: string[]) => { const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
if (action === 'bulk_publish_wordpress') { 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 const readyItems = images
.filter(item => ids.includes(item.content_id.toString())) .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) { if (readyItems.length === 0) {
toast.warning('No items are ready for WordPress publishing. Items must have generated images and not already be published.'); 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 { try {
let successCount = 0; const response = await api.post('/api/wordpress/bulk-publish/', {
let failedCount = 0; content_ids: readyItems.map(item => item.content_id.toString())
const errors: string[] = []; });
// Process each item individually using the existing publishContent function if (response.data.success) {
for (const item of readyItems) { const results = response.data.data.results;
try { const successCount = results.filter((r: any) => r.success).length;
await publishContent(item.content_id); const failedCount = results.filter((r: any) => !r.success).length;
successCount++;
} catch (error: any) { if (successCount > 0) {
failedCount++; toast.success(`Successfully published ${successCount} item(s) to WordPress`);
errors.push(`${item.content_title}: ${error.message}`);
} }
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) { } catch (error: any) {
console.error('Bulk WordPress publish error:', error); console.error('Bulk WordPress publish error:', error);
toast.error(`Failed to bulk publish to WordPress: ${error.message || 'Network 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}`); setStatusUpdateRecordName(row.content_title || `Content #${row.content_id}`);
setIsStatusModalOpen(true); setIsStatusModalOpen(true);
} else if (action === 'publish_wordpress') { } else if (action === 'publish_wordpress') {
// Handle WordPress publishing for individual item using existing publishContent function // Handle WordPress publishing for individual item
try { try {
// Use the existing publishContent function from the API const response = await api.post('/api/wordpress/publish/', {
const result = await publishContent(row.content_id); content_id: row.content_id.toString()
toast.success(`Successfully published "${row.content_title}" to WordPress! View at: ${result.external_url}`); });
// Reload images to reflect the updated WordPress status
loadImages(); 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) { } catch (error: any) {
console.error('WordPress publish error:', error); console.error('WordPress publish error:', error);
toast.error(`Failed to publish to WordPress: ${error.message || 'Network error'}`); toast.error(`Failed to publish to WordPress: ${error.message || 'Network error'}`);