fine tuning
This commit is contained in:
@@ -221,6 +221,7 @@ export default function Content() {
|
||||
{ label: 'Tasks', path: '/writer/tasks', icon: <TaskIcon /> },
|
||||
{ label: 'Content', path: '/writer/content', icon: <FileIcon /> },
|
||||
{ label: 'Images', path: '/writer/images', icon: <ImageIcon /> },
|
||||
{ label: 'Review', path: '/writer/review', icon: <CheckCircleIcon /> },
|
||||
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
|
||||
];
|
||||
|
||||
|
||||
@@ -240,63 +240,8 @@ 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 are ready and not already published
|
||||
const readyItems = images
|
||||
.filter(item => ids.includes(item.content_id.toString()))
|
||||
.filter(item => item.status === 'published' &&
|
||||
(!item.external_id || !item.external_url) &&
|
||||
(!item.sync_status || item.sync_status !== 'published'));
|
||||
|
||||
if (readyItems.length === 0) {
|
||||
toast.warning('No items are ready for WordPress publishing. Items must be published and not already synced to WordPress.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
// Publish each item individually using the unified publisher API
|
||||
for (const item of readyItems) {
|
||||
try {
|
||||
const response = await fetchAPI('/v1/publisher/publish/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content_id: item.content_id,
|
||||
destinations: ['wordpress']
|
||||
})
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
failedCount++;
|
||||
console.warn(`Failed to publish content ${item.content_id}:`, response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
failedCount++;
|
||||
console.error(`Error publishing content ${item.content_id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
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'}`);
|
||||
}
|
||||
} else {
|
||||
toast.info(`Bulk action "${action}" for ${ids.length} items`);
|
||||
}
|
||||
}, [images, toast, loadImages]);
|
||||
toast.info(`Bulk action "${action}" for ${ids.length} items`);
|
||||
}, [toast]);
|
||||
|
||||
// Row action handler
|
||||
const handleRowAction = useCallback(async (action: string, row: ContentImagesGroup) => {
|
||||
@@ -304,30 +249,8 @@ export default function Images() {
|
||||
setStatusUpdateContentId(row.content_id);
|
||||
setStatusUpdateRecordName(row.content_title || `Content #${row.content_id}`);
|
||||
setIsStatusModalOpen(true);
|
||||
} else if (action === 'publish_wordpress') {
|
||||
// Handle WordPress publishing for individual item
|
||||
try {
|
||||
const response = await fetchAPI('/v1/publisher/publish/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content_id: row.content_id,
|
||||
destinations: ['wordpress']
|
||||
})
|
||||
});
|
||||
|
||||
if (response.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.error || response.message}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('WordPress publish error:', error);
|
||||
toast.error(`Failed to publish to WordPress: ${error.message || 'Network error'}`);
|
||||
}
|
||||
}
|
||||
}, [loadImages, toast]);
|
||||
}, []);
|
||||
|
||||
// Handle status update confirmation
|
||||
const handleStatusUpdate = useCallback(async (status: string) => {
|
||||
@@ -577,6 +500,7 @@ export default function Images() {
|
||||
{ label: 'Tasks', path: '/writer/tasks', icon: <TaskIcon /> },
|
||||
{ label: 'Content', path: '/writer/content', icon: <FileIcon /> },
|
||||
{ label: 'Images', path: '/writer/images', icon: <ImageIcon /> },
|
||||
{ label: 'Review', path: '/writer/review', icon: <CheckCircleIcon /> },
|
||||
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
|
||||
];
|
||||
|
||||
|
||||
@@ -274,6 +274,7 @@ export default function Published() {
|
||||
{ label: 'Tasks', path: '/writer/tasks', icon: <TaskIcon /> },
|
||||
{ label: 'Content', path: '/writer/content', icon: <FileIcon /> },
|
||||
{ label: 'Images', path: '/writer/images', icon: <ImageIcon /> },
|
||||
{ label: 'Review', path: '/writer/review', icon: <CheckCircleIcon /> },
|
||||
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
|
||||
];
|
||||
|
||||
|
||||
337
frontend/src/pages/Writer/Review.tsx
Normal file
337
frontend/src/pages/Writer/Review.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* Review Page - Built with TablePageTemplate
|
||||
* Shows content with status='review' ready for publishing
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchContent,
|
||||
Content,
|
||||
ContentListResponse,
|
||||
ContentFilters,
|
||||
fetchAPI,
|
||||
} from '../../services/api';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { FileIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons';
|
||||
import { createReviewPageConfig } from '../../config/pages/review.config';
|
||||
import { useSectorStore } from '../../store/sectorStore';
|
||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
||||
import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter';
|
||||
|
||||
export default function Review() {
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { activeSector } = useSectorStore();
|
||||
const { pageSize } = usePageSizeStore();
|
||||
|
||||
// Data state
|
||||
const [content, setContent] = useState<Content[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Filter state - default to review status
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('review'); // Default to review
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
|
||||
// Pagination state
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
// Sorting state
|
||||
const [sortBy, setSortBy] = useState<string>('created_at');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
|
||||
// Load content - filtered for review status
|
||||
const loadContent = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setShowContent(false);
|
||||
try {
|
||||
const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-created_at';
|
||||
|
||||
const filters: ContentFilters = {
|
||||
...(searchTerm && { search: searchTerm }),
|
||||
status: 'review', // Always filter for review status
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
ordering,
|
||||
};
|
||||
|
||||
const data: ContentListResponse = await fetchContent(filters);
|
||||
setContent(data.results || []);
|
||||
setTotalCount(data.count || 0);
|
||||
setTotalPages(Math.ceil((data.count || 0) / pageSize));
|
||||
|
||||
setTimeout(() => {
|
||||
setShowContent(true);
|
||||
setLoading(false);
|
||||
}, 100);
|
||||
} catch (error: any) {
|
||||
console.error('Error loading content:', error);
|
||||
toast.error(`Failed to load content: ${error.message}`);
|
||||
setShowContent(true);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentPage, sortBy, sortDirection, searchTerm, pageSize, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
loadContent();
|
||||
}, [loadContent]);
|
||||
|
||||
// Listen for site and sector changes and refresh data
|
||||
useEffect(() => {
|
||||
const handleSiteChange = () => {
|
||||
loadContent();
|
||||
};
|
||||
|
||||
window.addEventListener('site-changed', handleSiteChange);
|
||||
window.addEventListener('sector-changed', handleSiteChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('site-changed', handleSiteChange);
|
||||
window.removeEventListener('sector-changed', handleSiteChange);
|
||||
};
|
||||
}, [loadContent]);
|
||||
|
||||
// Sorting handler
|
||||
const handleSort = useCallback((column: string) => {
|
||||
if (column === sortBy) {
|
||||
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortBy(column);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
setCurrentPage(1);
|
||||
}, [sortBy]);
|
||||
|
||||
// Build page config
|
||||
const pageConfig = useMemo(() =>
|
||||
createReviewPageConfig({
|
||||
activeSector,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
statusFilter,
|
||||
setStatusFilter,
|
||||
setCurrentPage,
|
||||
}),
|
||||
[activeSector, searchTerm, statusFilter]
|
||||
);
|
||||
|
||||
// Header metrics (calculated from loaded data)
|
||||
const headerMetrics = useMemo(() =>
|
||||
pageConfig.headerMetrics.map(metric => ({
|
||||
...metric,
|
||||
value: metric.calculate({ content, totalCount }),
|
||||
})),
|
||||
[pageConfig.headerMetrics, content, totalCount]
|
||||
);
|
||||
|
||||
// Export handler
|
||||
const handleBulkExport = useCallback(async (ids: string[]) => {
|
||||
toast.info(`Exporting ${ids.length} item(s)...`);
|
||||
return { success: true };
|
||||
}, [toast]);
|
||||
|
||||
// Publish to WordPress - single item
|
||||
const handlePublishSingle = useCallback(async (row: Content) => {
|
||||
try {
|
||||
const response = await fetchAPI('/v1/publisher/publish/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content_id: row.id,
|
||||
destinations: ['wordpress']
|
||||
})
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
toast.success(`Successfully published "${row.title}" to WordPress`);
|
||||
loadContent(); // Reload to reflect changes
|
||||
} else {
|
||||
toast.error(`Failed to publish: ${response.error || response.message}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('WordPress publish error:', error);
|
||||
toast.error(`Failed to publish to WordPress: ${error.message || 'Network error'}`);
|
||||
}
|
||||
}, [loadContent, toast]);
|
||||
|
||||
// Publish to WordPress - bulk
|
||||
const handlePublishBulk = useCallback(async (ids: string[]) => {
|
||||
try {
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
// Publish each item individually
|
||||
for (const id of ids) {
|
||||
try {
|
||||
const response = await fetchAPI('/v1/publisher/publish/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content_id: parseInt(id),
|
||||
destinations: ['wordpress']
|
||||
})
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
failedCount++;
|
||||
console.warn(`Failed to publish content ${id}:`, response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
failedCount++;
|
||||
console.error(`Error publishing content ${id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
toast.success(`Successfully published ${successCount} item(s) to WordPress`);
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
toast.warning(`${failedCount} item(s) failed to publish`);
|
||||
}
|
||||
|
||||
loadContent(); // Reload to reflect changes
|
||||
} catch (error: any) {
|
||||
console.error('Bulk WordPress publish error:', error);
|
||||
toast.error(`Failed to bulk publish to WordPress: ${error.message || 'Network error'}`);
|
||||
}
|
||||
}, [loadContent, toast]);
|
||||
|
||||
// Bulk action handler
|
||||
const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
|
||||
if (action === 'bulk_publish_wordpress') {
|
||||
await handlePublishBulk(ids);
|
||||
} else {
|
||||
toast.info(`Bulk action "${action}" for ${ids.length} items`);
|
||||
}
|
||||
}, [handlePublishBulk, toast]);
|
||||
|
||||
// Row action handler
|
||||
const handleRowAction = useCallback(async (action: string, row: Content) => {
|
||||
if (action === 'publish_wordpress') {
|
||||
await handlePublishSingle(row);
|
||||
} else if (action === 'view') {
|
||||
navigate(`/writer/content/${row.id}`);
|
||||
}
|
||||
}, [handlePublishSingle, navigate]);
|
||||
|
||||
// Delete handler (single)
|
||||
const handleDelete = useCallback(async (id: string) => {
|
||||
try {
|
||||
await fetchAPI(`/v1/writer/content/${id}/`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
toast.success('Content deleted successfully');
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to delete content: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}, [loadContent, toast]);
|
||||
|
||||
// Delete handler (bulk)
|
||||
const handleBulkDelete = useCallback(async (ids: string[]) => {
|
||||
try {
|
||||
// Delete each item individually
|
||||
let successCount = 0;
|
||||
for (const id of ids) {
|
||||
try {
|
||||
await fetchAPI(`/v1/writer/content/${id}/`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete content ${id}:`, error);
|
||||
}
|
||||
}
|
||||
toast.success(`Deleted ${successCount} content item(s)`);
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to bulk delete: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}, [loadContent, toast]);
|
||||
|
||||
// Writer navigation tabs
|
||||
const writerTabs = [
|
||||
{ label: 'Tasks', path: '/writer/tasks', icon: <TaskIcon /> },
|
||||
{ label: 'Content', path: '/writer/content', icon: <FileIcon /> },
|
||||
{ label: 'Images', path: '/writer/images', icon: <ImageIcon /> },
|
||||
{ label: 'Review', path: '/writer/review', icon: <CheckCircleIcon /> },
|
||||
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Content Review"
|
||||
badge={{ icon: <CheckCircleIcon />, color: 'blue' }}
|
||||
navigation={<ModuleNavigationTabs tabs={writerTabs} />}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
data={content}
|
||||
loading={loading}
|
||||
showContent={showContent}
|
||||
filters={pageConfig.filters}
|
||||
filterValues={{
|
||||
search: searchTerm,
|
||||
}}
|
||||
onFilterChange={(key, value) => {
|
||||
const stringValue = value === null || value === undefined ? '' : String(value);
|
||||
if (key === 'search') {
|
||||
setSearchTerm(stringValue);
|
||||
}
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
onBulkExport={handleBulkExport}
|
||||
onBulkAction={handleBulkAction}
|
||||
onDelete={handleDelete}
|
||||
onBulkDelete={handleBulkDelete}
|
||||
getItemDisplayName={(row: Content) => row.title || `Content #${row.id}`}
|
||||
onExport={async () => {
|
||||
toast.info('Export functionality coming soon');
|
||||
}}
|
||||
selectionLabel="content"
|
||||
pagination={{
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalCount,
|
||||
onPageChange: setCurrentPage,
|
||||
}}
|
||||
selection={{
|
||||
selectedIds,
|
||||
onSelectionChange: setSelectedIds,
|
||||
}}
|
||||
sorting={{
|
||||
sortBy,
|
||||
sortDirection,
|
||||
onSort: handleSort,
|
||||
}}
|
||||
headerMetrics={headerMetrics}
|
||||
onFilterReset={() => {
|
||||
setSearchTerm('');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
onRowAction={handleRowAction}
|
||||
/>
|
||||
<ModuleMetricsFooter
|
||||
metrics={[
|
||||
{
|
||||
title: 'Ready to Publish',
|
||||
value: content.length,
|
||||
subtitle: 'Total review items',
|
||||
icon: <CheckCircleIcon className="w-5 h-5" />,
|
||||
accentColor: 'blue',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user