Section 3-8 - #MIgration Runs -
Multiple Migfeat: Update publishing terminology and add publishing settings - Changed references from "WordPress" to "Site" across multiple components for consistency. - Introduced a new "Publishing" tab in Site Settings to manage automatic content approval and publishing behavior. - Added publishing settings model to the backend with fields for auto-approval, auto-publish, and publishing limits. - Implemented Celery tasks for scheduling and processing automated content publishing. - Enhanced Writer Dashboard to include metrics for content published to the site and scheduled for publishing.
This commit is contained in:
@@ -200,7 +200,7 @@ export const BulkWordPressPublish: React.FC<BulkWordPressPublishProps> = ({
|
||||
onClick={handleOpen}
|
||||
disabled={selectedContentIds.length === 0}
|
||||
>
|
||||
Bulk Publish to WordPress ({selectedContentIds.length})
|
||||
Bulk Publish to Site ({selectedContentIds.length})
|
||||
</Button>
|
||||
|
||||
<Dialog
|
||||
@@ -210,14 +210,14 @@ export const BulkWordPressPublish: React.FC<BulkWordPressPublishProps> = ({
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
Bulk Publish to WordPress
|
||||
Bulk Publish to Site
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{!publishing && !result && (
|
||||
<>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
You are about to publish {selectedContentIds.length} content items to WordPress:
|
||||
You are about to publish {selectedContentIds.length} content items to your site:
|
||||
</Typography>
|
||||
|
||||
<List dense sx={{ maxHeight: 300, overflow: 'auto', mt: 2 }}>
|
||||
|
||||
@@ -138,14 +138,14 @@ export const BulkWordPressPublish: React.FC<BulkWordPressPublishProps> = ({
|
||||
disableEscapeKeyDown={publishing}
|
||||
>
|
||||
<DialogTitle>
|
||||
Bulk Publish to WordPress
|
||||
Bulk Publish to Site
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{!publishing && results.length === 0 && (
|
||||
<>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Ready to publish <strong>{readyToPublish.length}</strong> content items to WordPress:
|
||||
Ready to publish <strong>{readyToPublish.length}</strong> content items to your site:
|
||||
</Typography>
|
||||
|
||||
<Alert severity="info" sx={{ mt: 2, mb: 2 }}>
|
||||
|
||||
@@ -99,7 +99,7 @@ export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
|
||||
<ListItemIcon>
|
||||
<PublishIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Publish to WordPress</ListItemText>
|
||||
<ListItemText>Publish to Site</ListItemText>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
</>
|
||||
|
||||
@@ -202,7 +202,7 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
|
||||
|
||||
if (!shouldShowPublishButton) {
|
||||
return (
|
||||
<Tooltip title={`Images must be generated before publishing to WordPress`}>
|
||||
<Tooltip title={`Images must be generated before publishing to site`}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -229,7 +229,7 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
|
||||
const renderButton = () => {
|
||||
if (size === 'small') {
|
||||
return (
|
||||
<Tooltip title={`WordPress: ${statusInfo.label}`}>
|
||||
<Tooltip title={`Site: ${statusInfo.label}`}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
@@ -267,9 +267,9 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
|
||||
disabled={loading || statusInfo.action === 'wait'}
|
||||
size={size}
|
||||
>
|
||||
{statusInfo.action === 'publish' && 'Publish to WordPress'}
|
||||
{statusInfo.action === 'publish' && 'Publish to Site'}
|
||||
{statusInfo.action === 'retry' && 'Retry'}
|
||||
{statusInfo.action === 'view' && 'View on WordPress'}
|
||||
{statusInfo.action === 'view' && 'View on Site'}
|
||||
{statusInfo.action === 'wait' && statusInfo.label}
|
||||
</Button>
|
||||
);
|
||||
@@ -323,14 +323,14 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Publish to WordPress</DialogTitle>
|
||||
<DialogTitle>Publish to Site</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Are you sure you want to publish "<strong>{contentTitle}</strong>" to WordPress?
|
||||
Are you sure you want to publish "<strong>{contentTitle}</strong>" to your site?
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 2 }}>
|
||||
This will create a new post on your connected WordPress site with all content,
|
||||
This will create a new post on your connected site with all content,
|
||||
images, categories, and SEO metadata.
|
||||
</Typography>
|
||||
|
||||
@@ -348,7 +348,7 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
|
||||
|
||||
{wpStatus?.wordpress_sync_status === 'success' && (
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
This content is already published to WordPress.
|
||||
This content is already published to your site.
|
||||
You can force republish to update the existing post.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -202,7 +202,7 @@ export function createReviewPageConfig(params: {
|
||||
<span className="text-[11px] font-normal">{label}</span>
|
||||
</Badge>
|
||||
{row.external_id && (
|
||||
<CheckCircleIcon className="w-3.5 h-3.5 text-success-500" title="Published to WordPress" />
|
||||
<CheckCircleIcon className="w-3.5 h-3.5 text-success-500" title="Published to Site" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -251,7 +251,7 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
|
||||
|
||||
{
|
||||
key: 'view_on_wordpress',
|
||||
label: 'View on WordPress',
|
||||
label: 'View on Site',
|
||||
icon: <CheckCircleIcon className="w-5 h-5 text-brand-500" />,
|
||||
variant: 'secondary',
|
||||
shouldShow: (row: any) => !!row.external_id, // Only show if published
|
||||
@@ -334,14 +334,14 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
|
||||
},
|
||||
{
|
||||
key: 'publish_wordpress',
|
||||
label: 'Publish to WordPress',
|
||||
label: 'Publish to Site',
|
||||
icon: <ArrowRightIcon className="w-5 h-5" />,
|
||||
variant: 'success',
|
||||
shouldShow: (row: any) => !row.external_id, // Only show if not published
|
||||
},
|
||||
{
|
||||
key: 'view_on_wordpress',
|
||||
label: 'View on WordPress',
|
||||
label: 'View on Site',
|
||||
icon: <CheckCircleIcon className="w-5 h-5 text-brand-500" />,
|
||||
variant: 'secondary',
|
||||
shouldShow: (row: any) => !!row.external_id, // Only show if published
|
||||
@@ -350,7 +350,7 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
|
||||
bulkActions: [
|
||||
{
|
||||
key: 'bulk_publish_wordpress',
|
||||
label: 'Publish to WordPress',
|
||||
label: 'Publish to Site',
|
||||
icon: <ArrowRightIcon className="w-4 h-4" />,
|
||||
variant: 'success',
|
||||
},
|
||||
@@ -389,7 +389,7 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
|
||||
},
|
||||
{
|
||||
key: 'publish_wordpress',
|
||||
label: 'Publish to WordPress',
|
||||
label: 'Publish to Site',
|
||||
icon: <ArrowRightIcon className="w-5 h-5" />,
|
||||
variant: 'primary',
|
||||
},
|
||||
@@ -403,7 +403,7 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
|
||||
},
|
||||
{
|
||||
key: 'bulk_publish_wordpress',
|
||||
label: 'Publish to WordPress',
|
||||
label: 'Publish to Site',
|
||||
icon: <ArrowRightIcon className="w-5 h-5" />,
|
||||
variant: 'primary',
|
||||
},
|
||||
|
||||
@@ -46,11 +46,16 @@ export default function SiteSettings() {
|
||||
const siteSelectorRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Check for tab parameter in URL
|
||||
const initialTab = (searchParams.get('tab') as 'general' | 'integrations' | 'content-types') || 'general';
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'integrations' | 'content-types'>(initialTab);
|
||||
const initialTab = (searchParams.get('tab') as 'general' | 'integrations' | 'publishing' | 'content-types') || 'general';
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'integrations' | 'publishing' | 'content-types'>(initialTab);
|
||||
const [contentTypes, setContentTypes] = useState<any>(null);
|
||||
const [contentTypesLoading, setContentTypesLoading] = useState(false);
|
||||
|
||||
// Publishing settings state
|
||||
const [publishingSettings, setPublishingSettings] = useState<any>(null);
|
||||
const [publishingSettingsLoading, setPublishingSettingsLoading] = useState(false);
|
||||
const [publishingSettingsSaving, setPublishingSettingsSaving] = useState(false);
|
||||
|
||||
// Sectors selection state
|
||||
const [industries, setIndustries] = useState<Industry[]>([]);
|
||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||
@@ -102,7 +107,7 @@ export default function SiteSettings() {
|
||||
useEffect(() => {
|
||||
// Update tab if URL parameter changes
|
||||
const tab = searchParams.get('tab');
|
||||
if (tab && ['general', 'integrations', 'content-types'].includes(tab)) {
|
||||
if (tab && ['general', 'integrations', 'publishing', 'content-types'].includes(tab)) {
|
||||
setActiveTab(tab as typeof activeTab);
|
||||
}
|
||||
}, [searchParams]);
|
||||
@@ -113,6 +118,12 @@ export default function SiteSettings() {
|
||||
}
|
||||
}, [activeTab, wordPressIntegration]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'publishing' && siteId && !publishingSettings) {
|
||||
loadPublishingSettings();
|
||||
}
|
||||
}, [activeTab, siteId]);
|
||||
|
||||
// Load sites for selector
|
||||
useEffect(() => {
|
||||
loadSites();
|
||||
@@ -197,6 +208,47 @@ export default function SiteSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadPublishingSettings = async () => {
|
||||
if (!siteId) return;
|
||||
try {
|
||||
setPublishingSettingsLoading(true);
|
||||
const response = await fetchAPI(`/v1/integration/sites/${siteId}/publishing-settings/`);
|
||||
setPublishingSettings(response.data || response);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load publishing settings:', error);
|
||||
// Set defaults if endpoint fails
|
||||
setPublishingSettings({
|
||||
auto_approval_enabled: true,
|
||||
auto_publish_enabled: true,
|
||||
daily_publish_limit: 3,
|
||||
weekly_publish_limit: 15,
|
||||
monthly_publish_limit: 50,
|
||||
publish_days: ['mon', 'tue', 'wed', 'thu', 'fri'],
|
||||
publish_time_slots: ['09:00', '14:00', '18:00'],
|
||||
});
|
||||
} finally {
|
||||
setPublishingSettingsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const savePublishingSettings = async (newSettings: any) => {
|
||||
if (!siteId) return;
|
||||
try {
|
||||
setPublishingSettingsSaving(true);
|
||||
const response = await fetchAPI(`/v1/integration/sites/${siteId}/publishing-settings/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(newSettings),
|
||||
});
|
||||
setPublishingSettings(response.data || response);
|
||||
toast.success('Publishing settings saved successfully');
|
||||
} catch (error: any) {
|
||||
console.error('Failed to save publishing settings:', error);
|
||||
toast.error('Failed to save publishing settings');
|
||||
} finally {
|
||||
setPublishingSettingsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadIndustries = async () => {
|
||||
try {
|
||||
const response = await fetchIndustries();
|
||||
@@ -583,6 +635,21 @@ export default function SiteSettings() {
|
||||
<PlugInIcon className="w-4 h-4 inline mr-2" />
|
||||
Integrations
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveTab('publishing');
|
||||
navigate(`/sites/${siteId}/settings?tab=publishing`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'publishing'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<PaperPlaneIcon className="w-4 h-4 inline mr-2" />
|
||||
Publishing
|
||||
</button>
|
||||
{(wordPressIntegration || site?.wp_url || site?.wp_api_key || site?.hosting_type === 'wordpress') && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -603,6 +670,257 @@ export default function SiteSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Publishing Tab */}
|
||||
{activeTab === 'publishing' && (
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Publishing Settings</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Configure automatic content approval and publishing behavior</p>
|
||||
</div>
|
||||
|
||||
{publishingSettingsLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-brand-600 mb-3"></div>
|
||||
<p>Loading publishing settings...</p>
|
||||
</div>
|
||||
) : publishingSettings ? (
|
||||
<div className="space-y-8">
|
||||
{/* Auto-Approval Section */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-base font-medium text-gray-900 dark:text-white">Auto-Approval</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Automatically approve content after review (moves from 'review' to 'approved' status)
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={publishingSettings.auto_approval_enabled}
|
||||
onChange={(e) => {
|
||||
const newSettings = { ...publishingSettings, auto_approval_enabled: e.target.checked };
|
||||
setPublishingSettings(newSettings);
|
||||
savePublishingSettings({ auto_approval_enabled: e.target.checked });
|
||||
}}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-300 dark:peer-focus:ring-brand-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto-Publish Section */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-base font-medium text-gray-900 dark:text-white">Auto-Publish to Site</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Automatically publish approved content to WordPress based on the schedule below
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={publishingSettings.auto_publish_enabled}
|
||||
onChange={(e) => {
|
||||
const newSettings = { ...publishingSettings, auto_publish_enabled: e.target.checked };
|
||||
setPublishingSettings(newSettings);
|
||||
savePublishingSettings({ auto_publish_enabled: e.target.checked });
|
||||
}}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-300 dark:peer-focus:ring-brand-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Publishing Limits Section */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-6">
|
||||
<h3 className="text-base font-medium text-gray-900 dark:text-white mb-4">Publishing Limits</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Set limits for automatic publishing to control content flow
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label>Daily Limit</Label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
value={publishingSettings.daily_publish_limit}
|
||||
onChange={(e) => {
|
||||
const value = Math.max(1, Math.min(50, parseInt(e.target.value) || 1));
|
||||
setPublishingSettings({ ...publishingSettings, daily_publish_limit: value });
|
||||
}}
|
||||
onBlur={() => savePublishingSettings({ daily_publish_limit: publishingSettings.daily_publish_limit })}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Articles per day</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Weekly Limit</Label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="200"
|
||||
value={publishingSettings.weekly_publish_limit}
|
||||
onChange={(e) => {
|
||||
const value = Math.max(1, Math.min(200, parseInt(e.target.value) || 1));
|
||||
setPublishingSettings({ ...publishingSettings, weekly_publish_limit: value });
|
||||
}}
|
||||
onBlur={() => savePublishingSettings({ weekly_publish_limit: publishingSettings.weekly_publish_limit })}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Articles per week</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Monthly Limit</Label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="500"
|
||||
value={publishingSettings.monthly_publish_limit}
|
||||
onChange={(e) => {
|
||||
const value = Math.max(1, Math.min(500, parseInt(e.target.value) || 1));
|
||||
setPublishingSettings({ ...publishingSettings, monthly_publish_limit: value });
|
||||
}}
|
||||
onBlur={() => savePublishingSettings({ monthly_publish_limit: publishingSettings.monthly_publish_limit })}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Articles per month</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Publishing Days Section */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-6">
|
||||
<h3 className="text-base font-medium text-gray-900 dark:text-white mb-4">Publishing Days</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Select which days of the week to automatically publish content
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ value: 'mon', label: 'Mon' },
|
||||
{ value: 'tue', label: 'Tue' },
|
||||
{ value: 'wed', label: 'Wed' },
|
||||
{ value: 'thu', label: 'Thu' },
|
||||
{ value: 'fri', label: 'Fri' },
|
||||
{ value: 'sat', label: 'Sat' },
|
||||
{ value: 'sun', label: 'Sun' },
|
||||
].map((day) => (
|
||||
<button
|
||||
key={day.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const currentDays = publishingSettings.publish_days || [];
|
||||
const newDays = currentDays.includes(day.value)
|
||||
? currentDays.filter((d: string) => d !== day.value)
|
||||
: [...currentDays, day.value];
|
||||
setPublishingSettings({ ...publishingSettings, publish_days: newDays });
|
||||
savePublishingSettings({ publish_days: newDays });
|
||||
}}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
(publishingSettings.publish_days || []).includes(day.value)
|
||||
? 'bg-brand-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Publishing Time Slots Section */}
|
||||
<div>
|
||||
<h3 className="text-base font-medium text-gray-900 dark:text-white mb-4">Publishing Time Slots</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Set preferred times for publishing content (in your local timezone)
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{(publishingSettings.publish_time_slots || ['09:00', '14:00', '18:00']).map((time: string, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<input
|
||||
type="time"
|
||||
value={time}
|
||||
onChange={(e) => {
|
||||
const newSlots = [...(publishingSettings.publish_time_slots || [])];
|
||||
newSlots[index] = e.target.value;
|
||||
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
||||
}}
|
||||
onBlur={() => savePublishingSettings({ publish_time_slots: publishingSettings.publish_time_slots })}
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
|
||||
/>
|
||||
{(publishingSettings.publish_time_slots || []).length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newSlots = (publishingSettings.publish_time_slots || []).filter((_: string, i: number) => i !== index);
|
||||
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
||||
savePublishingSettings({ publish_time_slots: newSlots });
|
||||
}}
|
||||
className="p-2 text-gray-400 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newSlots = [...(publishingSettings.publish_time_slots || []), '12:00'];
|
||||
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
||||
savePublishingSettings({ publish_time_slots: newSlots });
|
||||
}}
|
||||
className="text-sm text-brand-600 hover:text-brand-700 font-medium flex items-center gap-1"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add Time Slot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<p className="font-medium mb-1">How Publishing Works</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-blue-700 dark:text-blue-300">
|
||||
<li>Content moves from <span className="font-medium">Draft</span> → <span className="font-medium">Review</span> → <span className="font-medium">Approved</span> → <span className="font-medium">Published</span></li>
|
||||
<li>Auto-approval moves content from Review to Approved automatically</li>
|
||||
<li>Auto-publish sends Approved content to your WordPress site</li>
|
||||
<li>You can always manually publish content using the "Publish to Site" button</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>Failed to load publishing settings. Please try again.</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-4"
|
||||
onClick={loadPublishingSettings}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Content Types Tab */}
|
||||
{activeTab === 'content-types' && (
|
||||
<Card>
|
||||
|
||||
@@ -43,6 +43,8 @@ interface WriterStats {
|
||||
total: number;
|
||||
drafts: number;
|
||||
published: number;
|
||||
publishedToSite: number;
|
||||
scheduledForPublish: number;
|
||||
totalWordCount: number;
|
||||
avgWordCount: number;
|
||||
byContentType: Record<string, number>;
|
||||
@@ -109,12 +111,17 @@ export default function WriterDashboard() {
|
||||
const content = contentRes.results || [];
|
||||
let drafts = 0;
|
||||
let published = 0;
|
||||
let publishedToSite = 0;
|
||||
let scheduledForPublish = 0;
|
||||
let contentTotalWordCount = 0;
|
||||
const contentByType: Record<string, number> = {};
|
||||
|
||||
content.forEach(c => {
|
||||
if (c.status === 'draft') drafts++;
|
||||
else if (c.status === 'published') published++;
|
||||
// Count site_status for external publishing metrics
|
||||
if (c.site_status === 'published') publishedToSite++;
|
||||
else if (c.site_status === 'scheduled') scheduledForPublish++;
|
||||
if (c.word_count) contentTotalWordCount += c.word_count;
|
||||
});
|
||||
|
||||
@@ -162,6 +169,8 @@ export default function WriterDashboard() {
|
||||
total: content.length,
|
||||
drafts,
|
||||
published,
|
||||
publishedToSite,
|
||||
scheduledForPublish,
|
||||
totalWordCount: contentTotalWordCount,
|
||||
avgWordCount: contentAvgWordCount,
|
||||
byContentType: contentByType
|
||||
@@ -237,13 +246,13 @@ export default function WriterDashboard() {
|
||||
metric: `${stats?.images.pending || 0} pending`,
|
||||
},
|
||||
{
|
||||
title: "Published",
|
||||
description: "Published content and posts",
|
||||
title: "Published to Site",
|
||||
description: "Content published to external site",
|
||||
icon: PaperPlaneIcon,
|
||||
color: "from-[var(--color-purple)] to-[var(--color-purple-dark)]",
|
||||
path: "/writer/published",
|
||||
count: stats?.content.published || 0,
|
||||
metric: "View all published",
|
||||
count: stats?.content.publishedToSite || 0,
|
||||
metric: stats?.content.scheduledForPublish ? `${stats.content.scheduledForPublish} scheduled` : "None scheduled",
|
||||
},
|
||||
{
|
||||
title: "Taxonomies",
|
||||
@@ -269,7 +278,7 @@ export default function WriterDashboard() {
|
||||
{
|
||||
id: 1,
|
||||
type: "Content Published",
|
||||
description: `${stats?.content.published || 0} pieces published to WordPress`,
|
||||
description: `${stats?.content.published || 0} pieces published to site`,
|
||||
timestamp: new Date(Date.now() - 30 * 60 * 1000),
|
||||
icon: PaperPlaneIcon,
|
||||
color: "text-success-600",
|
||||
@@ -723,7 +732,7 @@ export default function WriterDashboard() {
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Publish Content</h4>
|
||||
<p className="text-sm text-gray-600">Publish to WordPress</p>
|
||||
<p className="text-sm text-gray-600">Publish to Site</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-purple-500 transition" />
|
||||
</Link>
|
||||
|
||||
@@ -2240,6 +2240,10 @@ export interface Content {
|
||||
external_id?: string | null;
|
||||
external_url?: string | null;
|
||||
wordpress_status?: 'publish' | 'draft' | 'pending' | 'future' | 'private' | 'trash' | null;
|
||||
// Site publishing status
|
||||
site_status?: 'not_published' | 'scheduled' | 'publishing' | 'published' | 'failed';
|
||||
scheduled_publish_at?: string | null;
|
||||
site_status_updated_at?: string | null;
|
||||
// Timestamps
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
Reference in New Issue
Block a user