Phase 1-3 Implemented - PUBLISHING-PROGRESS-AND-SCHEDULING-UX-PLAN

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-16 14:14:17 +00:00
parent e704ed8bcf
commit 1f0a31fe79
14 changed files with 1809 additions and 224 deletions

View File

@@ -26,6 +26,12 @@ import { useSiteStore } from '../../store/siteStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import PageHeader from '../../components/common/PageHeader';
import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter';
import PublishingProgressModal, { PublishingProgressState } from '../../components/common/PublishingProgressModal';
import BulkPublishingModal, { PublishQueueItem } from '../../components/common/BulkPublishingModal';
import PublishLimitModal from '../../components/common/PublishLimitModal';
import ScheduleContentModal from '../../components/common/ScheduleContentModal';
import BulkScheduleModal from '../../components/common/BulkScheduleModal';
import BulkSchedulePreviewModal from '../../components/common/BulkSchedulePreviewModal';
export default function Approved() {
const toast = useToast();
@@ -70,6 +76,21 @@ export default function Approved() {
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [showContent, setShowContent] = useState(false);
// Publishing modals state
const [showPublishLimitModal, setShowPublishLimitModal] = useState(false);
const [showSinglePublishModal, setShowSinglePublishModal] = useState(false);
const [showBulkPublishModal, setShowBulkPublishModal] = useState(false);
const [singlePublishState, setSinglePublishState] = useState<PublishingProgressState | null>(null);
const [bulkPublishQueue, setBulkPublishQueue] = useState<PublishQueueItem[]>([]);
// Scheduling modals state
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [showBulkScheduleModal, setShowBulkScheduleModal] = useState(false);
const [showBulkSchedulePreviewModal, setShowBulkSchedulePreviewModal] = useState(false);
const [scheduleContent, setScheduleContent] = useState<Content | null>(null);
const [bulkScheduleItems, setBulkScheduleItems] = useState<Content[]>([]);
const [bulkSchedulePreview, setBulkSchedulePreview] = useState<any>(null);
// Load dynamic filter options based on current site's data and applied filters
// This implements cascading filters - each filter's options reflect what's available
// given the other currently applied filters
@@ -226,33 +247,240 @@ export default function Approved() {
setCurrentPage(1);
};
// Handle single content publish with progress modal
const handleSinglePublish = useCallback(async (row: Content) => {
if (!activeSite) {
toast.error('No active site selected');
return;
}
// Initialize publishing state
const initialState: PublishingProgressState = {
contentId: row.id,
contentTitle: row.title,
destination: activeSite.platform_type || 'wordpress',
siteName: activeSite.name,
status: 'preparing',
progress: 0,
statusMessage: 'Preparing content...',
error: null,
externalUrl: null,
externalId: null,
};
setSinglePublishState(initialState);
setShowSinglePublishModal(true);
try {
// Phase 1: Preparing (0-25%)
await new Promise(resolve => setTimeout(resolve, 500));
setSinglePublishState(prev => prev ? { ...prev, progress: 25, statusMessage: 'Uploading to site...' } : null);
// Phase 2: Uploading (25-50%) - Make API call
const response = await fetchAPI('/v1/publisher/publish/', {
method: 'POST',
body: JSON.stringify({
content_id: row.id,
destinations: [activeSite.platform_type || 'wordpress']
})
});
if (response.success && response.data?.results?.[0]?.success) {
const result = response.data.results[0];
// Phase 3: Processing (50-75%)
setSinglePublishState(prev => prev ? { ...prev, progress: 50, statusMessage: 'Processing response...' } : null);
await new Promise(resolve => setTimeout(resolve, 300));
// Phase 4: Finalizing (75-100%)
setSinglePublishState(prev => prev ? { ...prev, progress: 75, statusMessage: 'Finalizing...' } : null);
await new Promise(resolve => setTimeout(resolve, 300));
// Complete
setSinglePublishState(prev => prev ? {
...prev,
status: 'completed',
progress: 100,
statusMessage: 'Published successfully!',
externalUrl: result.url,
externalId: result.external_id,
} : null);
loadContent();
} else {
throw new Error(response.error || 'Failed to publish');
}
} catch (error: any) {
console.error('Publish error:', error);
setSinglePublishState(prev => prev ? {
...prev,
status: 'failed',
progress: 0,
statusMessage: 'Failed to publish',
error: error.message || 'Network error',
} : null);
}
}, [activeSite, toast, loadContent]);
// Schedule single content
const handleScheduleContent = useCallback(async (contentId: number, scheduledDate: string) => {
try {
await fetchAPI(`/v1/writer/content/${contentId}/schedule/`, {
method: 'POST',
body: JSON.stringify({ scheduled_publish_at: scheduledDate }),
});
toast.success('Content scheduled successfully');
loadContent();
} catch (error: any) {
toast.error(`Failed to schedule: ${error.message}`);
throw error;
}
}, [toast, loadContent]);
// Reschedule content (same API endpoint)
const handleRescheduleContent = useCallback(async (contentId: number, scheduledDate: string) => {
try {
await fetchAPI(`/v1/writer/content/${contentId}/reschedule/`, {
method: 'POST',
body: JSON.stringify({ scheduled_at: scheduledDate }),
});
toast.success('Content rescheduled successfully');
loadContent();
} catch (error: any) {
toast.error(`Failed to reschedule: ${error.message}`);
throw error;
}
}, [toast, loadContent]);
// Unschedule content
const handleUnscheduleContent = useCallback(async (contentId: number) => {
try {
await fetchAPI(`/v1/writer/content/${contentId}/unschedule/`, {
method: 'POST',
});
toast.success('Content unscheduled successfully');
loadContent();
} catch (error: any) {
toast.error(`Failed to unschedule: ${error.message}`);
}
}, [toast, loadContent]);
// Bulk schedule with manual date/time
const handleBulkScheduleManual = useCallback(async (contentIds: number[], scheduledDate: string) => {
try {
let successCount = 0;
let failedCount = 0;
for (const contentId of contentIds) {
try {
await fetchAPI(`/v1/writer/content/${contentId}/schedule/`, {
method: 'POST',
body: JSON.stringify({ scheduled_publish_at: scheduledDate }),
});
successCount++;
} catch (error) {
console.error(`Failed to schedule content ${contentId}:`, error);
failedCount++;
}
}
if (successCount > 0) {
toast.success(`Scheduled ${successCount} item(s)`);
}
if (failedCount > 0) {
toast.warning(`${failedCount} item(s) failed to schedule`);
}
loadContent();
} catch (error: any) {
toast.error(`Failed to schedule: ${error.message}`);
throw error;
}
}, [toast, loadContent]);
// Bulk schedule with site defaults - show preview first
const handleBulkScheduleWithDefaults = useCallback(async () => {
if (!activeSite || selectedIds.length === 0) return;
try {
const contentIds = selectedIds.map(id => parseInt(id));
const response = await fetchAPI('/v1/writer/content/bulk_schedule_preview/', {
method: 'POST',
body: JSON.stringify({
content_ids: contentIds,
site_id: activeSite.id
})
});
if (response.success) {
setBulkSchedulePreview(response.data);
setShowBulkSchedulePreviewModal(true);
} else {
throw new Error(response.error || 'Failed to generate preview');
}
} catch (error: any) {
toast.error(`Failed to generate schedule preview: ${error.message}`);
}
}, [activeSite, selectedIds, toast]);
// Confirm bulk schedule with site defaults
const handleConfirmBulkSchedule = useCallback(async () => {
if (!activeSite || selectedIds.length === 0) return;
try {
const contentIds = selectedIds.map(id => parseInt(id));
const response = await fetchAPI('/v1/writer/content/bulk_schedule/', {
method: 'POST',
body: JSON.stringify({
content_ids: contentIds,
use_site_defaults: true,
site_id: activeSite.id
})
});
if (response.success) {
toast.success(`Scheduled ${response.data.scheduled_count} item(s)`);
setShowBulkSchedulePreviewModal(false);
setBulkSchedulePreview(null);
setSelectedIds([]);
loadContent();
} else {
throw new Error(response.error || 'Failed to schedule');
}
} catch (error: any) {
toast.error(`Failed to schedule: ${error.message}`);
}
}, [activeSite, selectedIds, toast, loadContent]);
// Open site settings in new tab
const handleOpenSiteSettings = useCallback(() => {
if (activeSite) {
window.open(`/sites/${activeSite.id}/settings?tab=publishing`, '_blank');
}
}, [activeSite]);
// Row action handler
const handleRowAction = useCallback(async (action: string, row: Content) => {
if (action === 'publish_wordpress') {
try {
const response = await fetchAPI('/v1/publisher/publish/', {
method: 'POST',
body: JSON.stringify({
content_id: row.id,
destinations: ['wordpress']
})
});
if (response.success) {
toast.success(`Published "${row.title}" to WordPress`);
loadContent();
} else {
toast.error(response.error || 'Failed to publish');
}
} catch (error: any) {
console.error('WordPress publish error:', error);
toast.error(`Failed to publish: ${error.message || 'Network error'}`);
}
} else if (action === 'view_on_wordpress') {
if (action === 'publish_site') {
await handleSinglePublish(row);
} else if (action === 'view_on_site') {
if (row.external_url) {
window.open(row.external_url, '_blank');
} else {
toast.warning('WordPress URL not available');
toast.warning('Site URL not available');
}
} else if (action === 'schedule') {
setScheduleContent(row);
setShowScheduleModal(true);
} else if (action === 'reschedule') {
setScheduleContent(row);
setShowScheduleModal(true);
} else if (action === 'unschedule') {
if (window.confirm(`Are you sure you want to unschedule "${row.title}"?`)) {
await handleUnscheduleContent(row.id);
}
} else if (action === 'edit') {
// Navigate to content editor
@@ -263,7 +491,7 @@ export default function Approved() {
toast.warning('Unable to edit: Site information not available');
}
}
}, [toast, loadContent, navigate]);
}, [toast, navigate, handleSinglePublish, handleUnscheduleContent]);
const handleDelete = useCallback(async (id: number) => {
await deleteContent(id);
@@ -276,56 +504,113 @@ export default function Approved() {
return result;
}, [loadContent]);
// Bulk WordPress publish
const handleBulkPublishWordPress = useCallback(async (ids: string[]) => {
// Handle bulk publish with progress modal and limit validation
const handleBulkPublishToSite = useCallback(async (ids: string[]) => {
if (!activeSite) {
toast.error('No active site selected');
return;
}
// Validate: Max 5 items for direct bulk publish
if (ids.length > 5) {
setShowPublishLimitModal(true);
return;
}
try {
const contentIds = ids.map(id => parseInt(id));
let successCount = 0;
let failedCount = 0;
// Publish each item individually
for (const contentId of contentIds) {
// Initialize queue
const queue: PublishQueueItem[] = ids.map((id, index) => {
const contentItem = content.find(c => c.id === parseInt(id));
return {
contentId: parseInt(id),
contentTitle: contentItem?.title || `Content #${id}`,
index: index + 1,
status: 'pending' as const,
progress: 0,
statusMessage: 'Pending',
error: null,
externalUrl: null,
externalId: null,
};
});
setBulkPublishQueue(queue);
setShowBulkPublishModal(true);
// Process sequentially
for (let i = 0; i < queue.length; i++) {
// Update status to processing
setBulkPublishQueue(prev => prev.map((item, idx) =>
idx === i ? { ...item, status: 'processing', progress: 0, statusMessage: 'Preparing...' } : item
));
try {
// Simulate progress animation
await new Promise(resolve => setTimeout(resolve, 300));
setBulkPublishQueue(prev => prev.map((item, idx) =>
idx === i ? { ...item, progress: 25, statusMessage: 'Uploading to site...' } : item
));
// Call API
const response = await fetchAPI('/v1/publisher/publish/', {
method: 'POST',
body: JSON.stringify({
content_id: contentId,
destinations: ['wordpress']
content_id: queue[i].contentId,
destinations: [activeSite.platform_type || 'wordpress']
})
});
if (response.success) {
successCount++;
// Handle response
if (response.success && response.data?.results?.[0]?.success) {
const result = response.data.results[0];
// Animate to completion
setBulkPublishQueue(prev => prev.map((item, idx) =>
idx === i ? { ...item, progress: 75, statusMessage: 'Finalizing...' } : item
));
await new Promise(resolve => setTimeout(resolve, 200));
setBulkPublishQueue(prev => prev.map((item, idx) =>
idx === i ? {
...item,
status: 'completed',
progress: 100,
statusMessage: 'Published',
externalUrl: result.url,
externalId: result.external_id,
} : item
));
} else {
failedCount++;
console.warn(`Failed to publish content ${contentId}:`, response.error);
throw new Error(response.error || 'Unknown error');
}
} catch (error) {
failedCount++;
console.error(`Error publishing content ${contentId}:`, error);
} catch (error: any) {
console.error(`Error publishing content ${queue[i].contentId}:`, error);
setBulkPublishQueue(prev => prev.map((item, idx) =>
idx === i ? {
...item,
status: 'failed',
progress: 0,
statusMessage: 'Failed',
error: error.message || 'Network error',
} : item
));
}
}
if (successCount > 0) {
toast.success(`Published ${successCount} item(s) to WordPress`);
}
if (failedCount > 0) {
toast.warning(`${failedCount} item(s) failed to publish`);
}
// Refresh content after all done
loadContent();
} catch (error: any) {
toast.error(`Failed to bulk publish: ${error.message}`);
throw error;
}
}, [toast, loadContent]);
}, [activeSite, content, toast, loadContent]);
// Bulk action handler
const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
if (action === 'bulk_publish_wordpress') {
await handleBulkPublishWordPress(ids);
if (action === 'bulk_publish_site') {
await handleBulkPublishToSite(ids);
}
}, [handleBulkPublishWordPress]);
}, [handleBulkPublishToSite]);
// Bulk status update handler
const handleBulkUpdateStatus = useCallback(async (ids: string[], status: string) => {
@@ -430,10 +715,16 @@ export default function Approved() {
content_structure: contentStructureFilter,
}}
primaryAction={{
label: 'Publish to Site',
label: selectedIds.length > 5 ? 'Publish (Limit Exceeded)' : 'Publish to Site',
icon: <BoltIcon className="w-4 h-4" />,
onClick: () => handleBulkAction('bulk_publish_wordpress', selectedIds),
onClick: () => handleBulkAction('bulk_publish_site', selectedIds),
variant: 'success',
disabled: selectedIds.length === 0,
tooltip: selectedIds.length > 5
? 'You can only publish 5 items at once. Use scheduling for more.'
: selectedIds.length > 0
? `Publish ${selectedIds.length} item${selectedIds.length !== 1 ? 's' : ''} to ${activeSite?.name || 'site'}`
: 'Select items to publish',
}}
onFilterChange={(key: string, value: any) => {
if (key === 'search') {
@@ -504,6 +795,97 @@ export default function Approved() {
}}
module="writer"
/>
{/* Publishing Modals */}
{singlePublishState && (
<PublishingProgressModal
isOpen={showSinglePublishModal}
onClose={() => setShowSinglePublishModal(false)}
publishingState={singlePublishState}
onRetry={() => {
setShowSinglePublishModal(false);
const contentItem = content.find(c => c.id === singlePublishState.contentId);
if (contentItem) {
handleSinglePublish(contentItem);
}
}}
/>
)}
<BulkPublishingModal
isOpen={showBulkPublishModal}
onClose={() => {
setShowBulkPublishModal(false);
setBulkPublishQueue([]);
setSelectedIds([]);
}}
queue={bulkPublishQueue}
siteName={activeSite?.name || 'Site'}
destination={activeSite?.platform_type || 'wordpress'}
onUpdateQueue={setBulkPublishQueue}
onRetryItem={(contentId) => {
const contentItem = content.find(c => c.id === contentId);
if (contentItem) {
handleSinglePublish(contentItem);
}
}}
/>
<PublishLimitModal
isOpen={showPublishLimitModal}
onClose={() => setShowPublishLimitModal(false)}
selectedCount={selectedIds.length}
onScheduleInstead={() => {
setShowPublishLimitModal(false);
handleBulkScheduleWithDefaults();
}}
/>
<ScheduleContentModal
isOpen={showScheduleModal}
onClose={() => {
setShowScheduleModal(false);
setScheduleContent(null);
}}
content={scheduleContent}
onSchedule={async (contentId, scheduledDate) => {
if (scheduleContent?.site_status === 'scheduled' || scheduleContent?.site_status === 'failed') {
await handleRescheduleContent(contentId, scheduledDate);
} else {
await handleScheduleContent(contentId, scheduledDate);
}
setShowScheduleModal(false);
setScheduleContent(null);
}}
mode={scheduleContent?.site_status === 'scheduled' || scheduleContent?.site_status === 'failed' ? 'reschedule' : 'schedule'}
/>
<BulkScheduleModal
isOpen={showBulkScheduleModal}
onClose={() => {
setShowBulkScheduleModal(false);
setBulkScheduleItems([]);
}}
contentItems={bulkScheduleItems}
onSchedule={async (contentIds, scheduledDate) => {
await handleBulkScheduleManual(contentIds, scheduledDate);
setShowBulkScheduleModal(false);
setBulkScheduleItems([]);
setSelectedIds([]);
}}
/>
<BulkSchedulePreviewModal
isOpen={showBulkSchedulePreviewModal}
onClose={() => {
setShowBulkSchedulePreviewModal(false);
setBulkSchedulePreview(null);
}}
previewData={bulkSchedulePreview}
onConfirm={handleConfirmBulkSchedule}
onChangeSettings={handleOpenSiteSettings}
siteId={activeSite?.id || 0}
/>
</>
);
}