igny8-wp
This commit is contained in:
@@ -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;
|
||||
338
frontend/src/components/WordPressPublish/WordPressPublish.tsx
Normal file
338
frontend/src/components/WordPressPublish/WordPressPublish.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user