This commit is contained in:
alorig
2025-11-28 12:40:34 +05:00
parent f76e791de7
commit 636b7ddca9
6 changed files with 36 additions and 512 deletions

View File

@@ -1,260 +0,0 @@
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

@@ -1,166 +0,0 @@
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,10 +27,8 @@ 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 {
@@ -45,10 +43,8 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
contentId,
contentTitle,
currentStatus = 'draft',
imageGenerationStatus = 'pending',
onStatusChange,
size = 'medium',
showOnlyIfImagesReady = false
size = 'medium'
}) => {
const [wpStatus, setWpStatus] = useState<WordPressStatus | null>(null);
const [loading, setLoading] = useState(false);
@@ -197,34 +193,7 @@ 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') {
@@ -334,17 +303,6 @@ 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 }}>

View File

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

View File

@@ -327,10 +327,8 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
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'));
// Only show if images are generated (complete) - WordPress status is tracked separately
return row.overall_status === 'complete';
},
},
{

View File

@@ -13,7 +13,8 @@ import {
generateImages,
bulkUpdateImagesStatus,
ContentImage,
api,
fetchAPI,
publishContent,
} from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon, DownloadIcon, BoltIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons';
@@ -208,12 +209,10 @@ 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
// Filter to only publish items that have images generated
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')));
.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.');
@@ -221,27 +220,30 @@ export default function Images() {
}
try {
const response = await api.post('/api/wordpress/bulk-publish/', {
content_ids: readyItems.map(item => item.content_id.toString())
});
let successCount = 0;
let failedCount = 0;
const errors: string[] = [];
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`);
// 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 (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'}`);
@@ -258,19 +260,13 @@ 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
// Handle WordPress publishing for individual item using existing publishContent function
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}`);
}
// 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'}`);