This commit is contained in:
alorig
2025-11-28 12:08:21 +05:00
parent 719e477a2f
commit 081f94ffdb
7 changed files with 1521 additions and 0 deletions

View File

@@ -0,0 +1,284 @@
import React, { useState } from 'react';
import {
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Typography,
Box,
LinearProgress,
Alert,
List,
ListItem,
ListItemText,
ListItemIcon,
CircularProgress,
Chip
} from '@mui/material';
import {
Publish as PublishIcon,
CheckCircle as SuccessIcon,
Error as ErrorIcon,
Schedule as PendingIcon
} from '@mui/icons-material';
import { api } from '../../services/api';
interface BulkWordPressPublishProps {
selectedContentIds: string[];
contentItems: Array<{
id: string;
title: string;
status: string;
}>;
onPublishComplete: () => void;
onClose: () => void;
}
interface BulkPublishResult {
total: number;
queued: number;
skipped: number;
errors: string[];
}
export const BulkWordPressPublish: React.FC<BulkWordPressPublishProps> = ({
selectedContentIds,
contentItems,
onPublishComplete,
onClose
}) => {
const [open, setOpen] = useState(false);
const [publishing, setPublishing] = useState(false);
const [result, setResult] = useState<BulkPublishResult | null>(null);
const [error, setError] = useState<string | null>(null);
const selectedItems = contentItems.filter(item =>
selectedContentIds.includes(item.id)
);
const handleBulkPublish = async () => {
setPublishing(true);
setError(null);
setResult(null);
try {
const response = await api.post('/api/v1/content/bulk-publish-to-wordpress/', {
content_ids: selectedContentIds.map(id => parseInt(id))
});
if (response.data.success) {
setResult({
total: selectedContentIds.length,
queued: response.data.data.content_count,
skipped: 0,
errors: []
});
// Start polling for individual status updates
startStatusPolling();
} else {
setError(response.data.message || 'Failed to start bulk publishing');
}
} catch (error: any) {
setError(error.response?.data?.message || 'Error starting bulk publish');
} finally {
setPublishing(false);
}
};
const startStatusPolling = () => {
// Poll for 2 minutes to check status
const pollInterval = setInterval(async () => {
try {
// Check status of all items (this could be optimized with a dedicated endpoint)
const statusPromises = selectedContentIds.map(id =>
api.get(`/api/v1/content/${id}/wordpress-status/`)
);
const responses = await Promise.allSettled(statusPromises);
let completedCount = 0;
let successCount = 0;
let failedCount = 0;
responses.forEach((response) => {
if (response.status === 'fulfilled' && response.value.data.success) {
const status = response.value.data.data.wordpress_sync_status;
if (status === 'success' || status === 'failed') {
completedCount++;
if (status === 'success') successCount++;
if (status === 'failed') failedCount++;
}
}
});
// If all items are complete, stop polling
if (completedCount === selectedContentIds.length) {
clearInterval(pollInterval);
setResult(prev => prev ? {
...prev,
queued: successCount,
errors: Array(failedCount).fill('Publishing failed')
} : null);
onPublishComplete();
}
} catch (error) {
console.error('Error polling status:', error);
}
}, 5000);
// Stop polling after 2 minutes
setTimeout(() => {
clearInterval(pollInterval);
}, 120000);
};
const handleOpen = () => {
setOpen(true);
setResult(null);
setError(null);
};
const handleClose = () => {
setOpen(false);
onClose();
};
const getResultSummary = () => {
if (!result) return null;
const { total, queued, skipped, errors } = result;
const failed = errors.length;
return (
<Box sx={{ mt: 2 }}>
<Typography variant="h6" gutterBottom>
Bulk Publish Results
</Typography>
<Box display="flex" gap={1} flexWrap="wrap" mb={2}>
<Chip
icon={<SuccessIcon />}
label={`${queued} Queued`}
color="success"
size="small"
/>
{skipped > 0 && (
<Chip
icon={<PendingIcon />}
label={`${skipped} Skipped`}
color="warning"
size="small"
/>
)}
{failed > 0 && (
<Chip
icon={<ErrorIcon />}
label={`${failed} Failed`}
color="error"
size="small"
/>
)}
</Box>
{failed > 0 && (
<Alert severity="warning" sx={{ mt: 1 }}>
Some items failed to publish. Check individual item status for details.
</Alert>
)}
</Box>
);
};
return (
<>
<Button
variant="contained"
color="primary"
startIcon={<PublishIcon />}
onClick={handleOpen}
disabled={selectedContentIds.length === 0}
>
Bulk Publish to WordPress ({selectedContentIds.length})
</Button>
<Dialog
open={open}
onClose={handleClose}
maxWidth="md"
fullWidth
>
<DialogTitle>
Bulk Publish to WordPress
</DialogTitle>
<DialogContent>
{!publishing && !result && (
<>
<Typography variant="body1" gutterBottom>
You are about to publish {selectedContentIds.length} content items to WordPress:
</Typography>
<List dense sx={{ maxHeight: 300, overflow: 'auto', mt: 2 }}>
{selectedItems.map((item) => (
<ListItem key={item.id}>
<ListItemIcon>
<PublishIcon />
</ListItemIcon>
<ListItemText
primary={item.title}
secondary={`Status: ${item.status}`}
/>
</ListItem>
))}
</List>
<Alert severity="info" sx={{ mt: 2 }}>
This will create new posts on your WordPress site with all content,
images, categories, and SEO metadata. Items already published will be skipped.
</Alert>
</>
)}
{publishing && (
<Box sx={{ py: 3 }}>
<Box display="flex" alignItems="center" gap={2} mb={2}>
<CircularProgress size={24} />
<Typography>Queuing content for WordPress publishing...</Typography>
</Box>
<LinearProgress />
</Box>
)}
{result && getResultSummary()}
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>
{result ? 'Close' : 'Cancel'}
</Button>
{!publishing && !result && (
<Button
onClick={handleBulkPublish}
color="primary"
variant="contained"
disabled={selectedContentIds.length === 0}
>
Publish All to WordPress
</Button>
)}
</DialogActions>
</Dialog>
</>
);
};
export default BulkWordPressPublish;

View File

@@ -0,0 +1,338 @@
import React, { useState, useEffect } from 'react';
import {
Button,
Alert,
Chip,
IconButton,
Tooltip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
CircularProgress,
Box,
Typography
} from '@mui/material';
import {
Publish as PublishIcon,
Refresh as RefreshIcon,
CheckCircle as SuccessIcon,
Error as ErrorIcon,
Schedule as PendingIcon,
Sync as SyncingIcon
} from '@mui/icons-material';
import { api } from '../../services/api';
interface WordPressPublishProps {
contentId: string;
contentTitle: string;
currentStatus?: 'draft' | 'publishing' | 'published' | 'failed';
onStatusChange?: (status: string) => void;
size?: 'small' | 'medium' | 'large';
}
interface WordPressStatus {
wordpress_sync_status: 'pending' | 'syncing' | 'success' | 'failed';
wordpress_post_id?: number;
wordpress_post_url?: string;
wordpress_sync_attempts: number;
last_wordpress_sync?: string;
}
export const WordPressPublish: React.FC<WordPressPublishProps> = ({
contentId,
contentTitle,
currentStatus = 'draft',
onStatusChange,
size = 'medium'
}) => {
const [wpStatus, setWpStatus] = useState<WordPressStatus | null>(null);
const [loading, setLoading] = useState(false);
const [publishDialogOpen, setPublishDialogOpen] = useState(false);
const [error, setError] = useState<string | null>(null);
// Fetch current WordPress status
const fetchWordPressStatus = async () => {
try {
const response = await api.get(`/api/v1/content/${contentId}/wordpress-status/`);
if (response.data.success) {
setWpStatus(response.data.data);
}
} catch (error) {
console.error('Failed to fetch WordPress status:', error);
}
};
useEffect(() => {
fetchWordPressStatus();
}, [contentId]);
// Handle publish to WordPress
const handlePublishToWordPress = async (force: boolean = false) => {
setLoading(true);
setError(null);
try {
const response = await api.post(`/api/v1/content/${contentId}/publish-to-wordpress/`, {
force: force
});
if (response.data.success) {
setWpStatus(prev => prev ? { ...prev, wordpress_sync_status: 'pending' } : null);
onStatusChange?.('publishing');
// Poll for status updates
pollForStatusUpdate();
} else {
setError(response.data.message || 'Failed to publish to WordPress');
}
} catch (error: any) {
setError(error.response?.data?.message || 'Error publishing to WordPress');
} finally {
setLoading(false);
setPublishDialogOpen(false);
}
};
// Poll for status updates after publishing
const pollForStatusUpdate = () => {
const pollInterval = setInterval(async () => {
try {
const response = await api.get(`/api/v1/content/${contentId}/wordpress-status/`);
if (response.data.success) {
const status = response.data.data;
setWpStatus(status);
// Stop polling if sync is complete (success or failed)
if (status.wordpress_sync_status === 'success' || status.wordpress_sync_status === 'failed') {
clearInterval(pollInterval);
onStatusChange?.(status.wordpress_sync_status === 'success' ? 'published' : 'failed');
}
}
} catch (error) {
clearInterval(pollInterval);
}
}, 3000); // Poll every 3 seconds
// Stop polling after 2 minutes
setTimeout(() => {
clearInterval(pollInterval);
}, 120000);
};
// Handle retry
const handleRetry = async () => {
setLoading(true);
setError(null);
try {
const response = await api.post(`/api/v1/content/${contentId}/retry-wordpress-sync/`);
if (response.data.success) {
setWpStatus(prev => prev ? { ...prev, wordpress_sync_status: 'pending' } : null);
onStatusChange?.('publishing');
pollForStatusUpdate();
} else {
setError(response.data.message || 'Failed to retry WordPress sync');
}
} catch (error: any) {
setError(error.response?.data?.message || 'Error retrying WordPress sync');
} finally {
setLoading(false);
}
};
// Get status display info
const getStatusInfo = () => {
if (!wpStatus) {
return {
color: 'default' as const,
icon: <PublishIcon />,
label: 'Not Published',
action: 'publish'
};
}
switch (wpStatus.wordpress_sync_status) {
case 'pending':
return {
color: 'warning' as const,
icon: <PendingIcon />,
label: 'Queued',
action: 'wait'
};
case 'syncing':
return {
color: 'info' as const,
icon: <SyncingIcon className="animate-spin" />,
label: 'Publishing...',
action: 'wait'
};
case 'success':
return {
color: 'success' as const,
icon: <SuccessIcon />,
label: 'Published',
action: 'view'
};
case 'failed':
return {
color: 'error' as const,
icon: <ErrorIcon />,
label: 'Failed',
action: 'retry'
};
default:
return {
color: 'default' as const,
icon: <PublishIcon />,
label: 'Not Published',
action: 'publish'
};
}
};
const statusInfo = getStatusInfo();
const renderButton = () => {
if (size === 'small') {
return (
<Tooltip title={`WordPress: ${statusInfo.label}`}>
<IconButton
size="small"
onClick={() => {
if (statusInfo.action === 'publish') {
setPublishDialogOpen(true);
} else if (statusInfo.action === 'retry') {
handleRetry();
} else if (statusInfo.action === 'view' && wpStatus?.wordpress_post_url) {
window.open(wpStatus.wordpress_post_url, '_blank');
}
}}
disabled={loading || statusInfo.action === 'wait'}
color={statusInfo.color}
>
{loading ? <CircularProgress size={16} /> : statusInfo.icon}
</IconButton>
</Tooltip>
);
}
return (
<Button
variant={statusInfo.action === 'publish' ? 'contained' : 'outlined'}
color={statusInfo.color}
startIcon={loading ? <CircularProgress size={20} /> : statusInfo.icon}
onClick={() => {
if (statusInfo.action === 'publish') {
setPublishDialogOpen(true);
} else if (statusInfo.action === 'retry') {
handleRetry();
} else if (statusInfo.action === 'view' && wpStatus?.wordpress_post_url) {
window.open(wpStatus.wordpress_post_url, '_blank');
}
}}
disabled={loading || statusInfo.action === 'wait'}
size={size}
>
{statusInfo.action === 'publish' && 'Publish to WordPress'}
{statusInfo.action === 'retry' && 'Retry'}
{statusInfo.action === 'view' && 'View on WordPress'}
{statusInfo.action === 'wait' && statusInfo.label}
</Button>
);
};
const renderStatusChip = () => {
if (size === 'small') return null;
return (
<Chip
icon={statusInfo.icon}
label={statusInfo.label}
color={statusInfo.color}
size="small"
variant="outlined"
onClick={() => {
if (wpStatus?.wordpress_post_url) {
window.open(wpStatus.wordpress_post_url, '_blank');
}
}}
style={{
marginLeft: 8,
cursor: wpStatus?.wordpress_post_url ? 'pointer' : 'default'
}}
/>
);
};
return (
<Box display="flex" alignItems="center" gap={1}>
{renderButton()}
{renderStatusChip()}
{error && (
<Alert severity="error" sx={{ mt: 1 }}>
{error}
<IconButton
size="small"
onClick={() => setError(null)}
sx={{ ml: 1 }}
>
×
</IconButton>
</Alert>
)}
{/* Publish Confirmation Dialog */}
<Dialog
open={publishDialogOpen}
onClose={() => setPublishDialogOpen(false)}
maxWidth="sm"
fullWidth
>
<DialogTitle>Publish to WordPress</DialogTitle>
<DialogContent>
<Typography variant="body1" gutterBottom>
Are you sure you want to publish "<strong>{contentTitle}</strong>" to WordPress?
</Typography>
<Typography variant="body2" color="textSecondary" sx={{ mt: 2 }}>
This will create a new post on your connected WordPress site with all content,
images, categories, and SEO metadata.
</Typography>
{wpStatus?.wordpress_sync_status === 'success' && (
<Alert severity="info" sx={{ mt: 2 }}>
This content is already published to WordPress.
You can force republish to update the existing post.
</Alert>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setPublishDialogOpen(false)}>
Cancel
</Button>
{wpStatus?.wordpress_sync_status === 'success' && (
<Button
onClick={() => handlePublishToWordPress(true)}
color="warning"
disabled={loading}
>
Force Republish
</Button>
)}
<Button
onClick={() => handlePublishToWordPress(false)}
color="primary"
variant="contained"
disabled={loading}
>
{loading ? 'Publishing...' : 'Publish'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default WordPressPublish;