ui improvements
This commit is contained in:
@@ -405,15 +405,13 @@ export default function Clusters() {
|
||||
volumeMin: volumeMin,
|
||||
volumeMax: volumeMax,
|
||||
}}
|
||||
nextAction={selectedIds.length > 0 ? {
|
||||
primaryAction={{
|
||||
label: 'Generate Ideas',
|
||||
message: `${selectedIds.length} selected`,
|
||||
onClick: () => handleBulkAction('generate_ideas', selectedIds),
|
||||
} : clusters.length > 0 ? {
|
||||
label: 'Generate Ideas',
|
||||
href: '/planner/ideas',
|
||||
message: `${clusters.length} clusters`,
|
||||
} : undefined}
|
||||
icon: <BoltIcon className="w-4 h-4" />,
|
||||
onClick: () => handleBulkAction('auto_generate_ideas', selectedIds),
|
||||
variant: 'success',
|
||||
}}
|
||||
getRowClassName={(row) => (row.ideas_count || 0) > 0 ? 'bg-success-50 dark:bg-success-500/10' : ''}
|
||||
onFilterChange={(key, value) => {
|
||||
const stringValue = value === null || value === undefined ? '' : String(value);
|
||||
if (key === 'search') {
|
||||
|
||||
@@ -23,7 +23,7 @@ import FormModal from '../../components/common/FormModal';
|
||||
import ProgressModal from '../../components/common/ProgressModal';
|
||||
import { useProgressModal } from '../../hooks/useProgressModal';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { BoltIcon, PlusIcon, DownloadIcon, ListIcon, GroupIcon } from '../../icons';
|
||||
import { BoltIcon, PlusIcon, DownloadIcon, ListIcon, GroupIcon, ArrowRightIcon } from '../../icons';
|
||||
import { LightBulbIcon } from '@heroicons/react/24/outline';
|
||||
import { createIdeasPageConfig } from '../../config/pages/ideas.config';
|
||||
import { useSectorStore } from '../../store/sectorStore';
|
||||
@@ -316,15 +316,13 @@ export default function Ideas() {
|
||||
content_structure: structureFilter,
|
||||
content_type: typeFilter,
|
||||
}}
|
||||
nextAction={selectedIds.length > 0 ? {
|
||||
primaryAction={{
|
||||
label: 'Queue to Writer',
|
||||
message: `${selectedIds.length} selected`,
|
||||
icon: <ArrowRightIcon className="w-4 h-4" />,
|
||||
onClick: () => handleBulkAction('queue_to_writer', selectedIds),
|
||||
} : ideas.filter(i => i.status === 'approved').length > 0 ? {
|
||||
label: 'Start Writing',
|
||||
href: '/writer/queue',
|
||||
message: `${ideas.filter(i => i.status === 'approved').length} approved`,
|
||||
} : undefined}
|
||||
variant: 'success',
|
||||
}}
|
||||
getRowClassName={(row) => row.status === 'queued' || row.status === 'completed' ? 'bg-success-50 dark:bg-success-500/10' : ''}
|
||||
onFilterChange={(key, value) => {
|
||||
const stringValue = value === null || value === undefined ? '' : String(value);
|
||||
if (key === 'search') {
|
||||
|
||||
@@ -597,19 +597,13 @@ export default function Keywords() {
|
||||
volumeMin: volumeMin,
|
||||
volumeMax: volumeMax,
|
||||
}}
|
||||
nextAction={selectedIds.length > 0 ? {
|
||||
label: 'Auto-Cluster Selected',
|
||||
message: `${selectedIds.length} selected`,
|
||||
onClick: handleAutoCluster,
|
||||
} : workflowStats.unclustered >= 5 ? {
|
||||
label: 'Auto-Cluster All',
|
||||
message: `${workflowStats.unclustered} unclustered`,
|
||||
onClick: handleAutoCluster,
|
||||
} : workflowStats.clustered > 0 ? {
|
||||
label: 'Generate Ideas',
|
||||
href: '/planner/ideas',
|
||||
message: `${workflowStats.clustered} clustered`,
|
||||
} : undefined}
|
||||
primaryAction={{
|
||||
label: 'Auto-Cluster',
|
||||
icon: <BoltIcon className="w-4 h-4" />,
|
||||
onClick: () => handleBulkAction('auto_cluster', selectedIds),
|
||||
variant: 'success',
|
||||
}}
|
||||
getRowClassName={(row) => row.cluster_id ? 'bg-success-50 dark:bg-success-500/10' : ''}
|
||||
onFilterChange={(key, value) => {
|
||||
// Normalize value to string, preserving empty strings
|
||||
const stringValue = value === null || value === undefined ? '' : String(value);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Published Page - Built with TablePageTemplate
|
||||
* Shows published/review content with WordPress publishing capabilities
|
||||
* Approved Page - Built with TablePageTemplate
|
||||
* Shows approved content ready for publishing to WordPress/external sites
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
@@ -17,15 +17,15 @@ import {
|
||||
bulkDeleteContent,
|
||||
} from '../../services/api';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { FileIcon, CheckCircleIcon } from '../../icons';
|
||||
import { FileIcon, CheckCircleIcon, BoltIcon } from '../../icons';
|
||||
import { RocketLaunchIcon } from '@heroicons/react/24/outline';
|
||||
import { createPublishedPageConfig } from '../../config/pages/published.config';
|
||||
import { createApprovedPageConfig } from '../../config/pages/approved.config';
|
||||
import { useSectorStore } from '../../store/sectorStore';
|
||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter';
|
||||
|
||||
export default function Published() {
|
||||
export default function Approved() {
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { activeSector } = useSectorStore();
|
||||
@@ -35,9 +35,9 @@ export default function Published() {
|
||||
const [content, setContent] = useState<Content[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Filter state - default to published/review status
|
||||
// Filter state - default to approved status
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('published'); // Default to published
|
||||
const [statusFilter, setStatusFilter] = useState('approved'); // Default to approved
|
||||
const [publishStatusFilter, setPublishStatusFilter] = useState('');
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function Published() {
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
|
||||
// Load content - filtered for published/review
|
||||
// Load content - filtered for approved status
|
||||
const loadContent = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setShowContent(false);
|
||||
@@ -60,7 +60,7 @@ export default function Published() {
|
||||
|
||||
const filters: ContentFilters = {
|
||||
...(searchTerm && { search: searchTerm }),
|
||||
...(statusFilter && { status: statusFilter }),
|
||||
status: 'approved', // Always filter for approved status
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
ordering,
|
||||
@@ -280,11 +280,9 @@ export default function Published() {
|
||||
|
||||
// Create page config
|
||||
const pageConfig = useMemo(() => {
|
||||
return createPublishedPageConfig({
|
||||
return createApprovedPageConfig({
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
statusFilter,
|
||||
setStatusFilter,
|
||||
publishStatusFilter,
|
||||
setPublishStatusFilter,
|
||||
setCurrentPage,
|
||||
@@ -293,7 +291,7 @@ export default function Published() {
|
||||
navigate(`/writer/content/${row.id}`);
|
||||
},
|
||||
});
|
||||
}, [searchTerm, statusFilter, publishStatusFilter, activeSector, navigate]);
|
||||
}, [searchTerm, publishStatusFilter, activeSector, navigate]);
|
||||
|
||||
// Calculate header metrics
|
||||
const headerMetrics = useMemo(() => {
|
||||
@@ -308,8 +306,8 @@ export default function Published() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Published"
|
||||
badge={{ icon: <RocketLaunchIcon />, color: 'green' }}
|
||||
title="Approved"
|
||||
badge={{ icon: <CheckCircleIcon />, color: 'green' }}
|
||||
parent="Writer"
|
||||
/>
|
||||
<TablePageTemplate
|
||||
@@ -320,24 +318,18 @@ export default function Published() {
|
||||
filters={pageConfig.filters}
|
||||
filterValues={{
|
||||
search: searchTerm,
|
||||
status: statusFilter,
|
||||
publishStatus: publishStatusFilter,
|
||||
}}
|
||||
nextAction={selectedIds.length > 0 ? {
|
||||
label: 'Sync to WordPress',
|
||||
message: `${selectedIds.length} selected`,
|
||||
onClick: () => handleBulkAction('publish_to_wordpress', selectedIds),
|
||||
} : {
|
||||
label: 'Create More Content',
|
||||
href: '/planner/keywords',
|
||||
message: `${content.length} published`,
|
||||
primaryAction={{
|
||||
label: 'Publish to Site',
|
||||
icon: <BoltIcon className="w-4 h-4" />,
|
||||
onClick: () => handleBulkAction('bulk_publish_wordpress', selectedIds),
|
||||
variant: 'success',
|
||||
}}
|
||||
getRowClassName={(row) => row.external_id ? 'bg-success-50 dark:bg-success-500/10' : ''}
|
||||
onFilterChange={(key: string, value: any) => {
|
||||
if (key === 'search') {
|
||||
setSearchTerm(value);
|
||||
} else if (key === 'status') {
|
||||
setStatusFilter(value);
|
||||
setCurrentPage(1);
|
||||
} else if (key === 'publishStatus') {
|
||||
setPublishStatusFilter(value);
|
||||
setCurrentPage(1);
|
||||
@@ -372,23 +364,23 @@ export default function Published() {
|
||||
<ModuleMetricsFooter
|
||||
metrics={[
|
||||
{
|
||||
title: 'Published Content',
|
||||
value: content.filter(c => c.status === 'published').length.toLocaleString(),
|
||||
subtitle: `${content.filter(c => c.external_id).length} on WordPress`,
|
||||
title: 'Approved Content',
|
||||
value: content.length.toLocaleString(),
|
||||
subtitle: 'ready for publishing',
|
||||
icon: <CheckCircleIcon className="w-5 h-5" />,
|
||||
accentColor: 'green',
|
||||
href: '/writer/published',
|
||||
},
|
||||
{
|
||||
title: 'Draft Content',
|
||||
value: content.filter(c => c.status === 'draft').length.toLocaleString(),
|
||||
subtitle: 'Not yet published',
|
||||
icon: <FileIcon className="w-5 h-5" />,
|
||||
title: 'Published to Site',
|
||||
value: content.filter(c => c.external_id).length.toLocaleString(),
|
||||
subtitle: 'on WordPress',
|
||||
icon: <RocketLaunchIcon className="w-5 h-5" />,
|
||||
accentColor: 'blue',
|
||||
href: '/writer/approved',
|
||||
},
|
||||
]}
|
||||
progress={{
|
||||
label: 'WordPress Publishing Progress',
|
||||
label: 'Site Publishing Progress',
|
||||
value: totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0,
|
||||
color: 'success',
|
||||
}}
|
||||
@@ -242,15 +242,7 @@ export default function Content() {
|
||||
status: statusFilter,
|
||||
source: sourceFilter,
|
||||
}}
|
||||
nextAction={selectedIds.length > 0 ? {
|
||||
label: 'Generate Images',
|
||||
message: `${selectedIds.length} selected`,
|
||||
onClick: () => handleRowAction('generate_images', { id: selectedIds[0] }),
|
||||
} : content.filter(c => c.status === 'draft').length > 0 ? {
|
||||
label: 'Generate Images',
|
||||
href: '/writer/images',
|
||||
message: `${content.filter(c => c.status === 'draft').length} drafts`,
|
||||
} : undefined}
|
||||
getRowClassName={(row) => row.status === 'review' || row.status === 'published' ? 'bg-success-50 dark:bg-success-500/10' : ''}
|
||||
onFilterChange={(key: string, value: any) => {
|
||||
if (key === 'search') {
|
||||
setSearchTerm(value);
|
||||
|
||||
@@ -243,6 +243,52 @@ export default function Review() {
|
||||
}
|
||||
}, [loadContent, toast]);
|
||||
|
||||
// Approve content - single item (changes status from 'review' to 'approved')
|
||||
const handleApproveSingle = useCallback(async (row: Content) => {
|
||||
try {
|
||||
await fetchAPI(`/v1/writer/content/${row.id}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status: 'approved' })
|
||||
});
|
||||
toast.success(`Approved "${row.title}"`);
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to approve: ${error.message || 'Network error'}`);
|
||||
}
|
||||
}, [loadContent, toast]);
|
||||
|
||||
// Approve content - bulk (changes status from 'review' to 'approved')
|
||||
const handleApproveBulk = useCallback(async (ids: string[]) => {
|
||||
try {
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
for (const id of ids) {
|
||||
try {
|
||||
await fetchAPI(`/v1/writer/content/${id}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status: 'approved' })
|
||||
});
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
failedCount++;
|
||||
console.error(`Error approving content ${id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
toast.success(`Successfully approved ${successCount} item(s)`);
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
toast.warning(`${failedCount} item(s) failed to approve`);
|
||||
}
|
||||
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to approve: ${error.message || 'Network error'}`);
|
||||
}
|
||||
}, [loadContent, toast]);
|
||||
|
||||
// Publish to WordPress - bulk
|
||||
const handlePublishBulk = useCallback(async (ids: string[]) => {
|
||||
try {
|
||||
@@ -291,21 +337,25 @@ export default function Review() {
|
||||
|
||||
// Bulk action handler
|
||||
const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
|
||||
if (action === 'bulk_publish_wordpress') {
|
||||
if (action === 'bulk_approve') {
|
||||
await handleApproveBulk(ids);
|
||||
} else if (action === 'bulk_publish_wordpress') {
|
||||
await handlePublishBulk(ids);
|
||||
} else {
|
||||
toast.info(`Bulk action "${action}" for ${ids.length} items`);
|
||||
}
|
||||
}, [handlePublishBulk, toast]);
|
||||
}, [handleApproveBulk, handlePublishBulk, toast]);
|
||||
|
||||
// Row action handler
|
||||
const handleRowAction = useCallback(async (action: string, row: Content) => {
|
||||
if (action === 'publish_wordpress') {
|
||||
if (action === 'approve') {
|
||||
await handleApproveSingle(row);
|
||||
} else if (action === 'publish_wordpress') {
|
||||
await handlePublishSingle(row);
|
||||
} else if (action === 'view') {
|
||||
navigate(`/writer/content/${row.id}`);
|
||||
}
|
||||
}, [handlePublishSingle, navigate]);
|
||||
}, [handleApproveSingle, handlePublishSingle, navigate]);
|
||||
|
||||
// Delete handler (single)
|
||||
const handleDelete = useCallback(async (id: string) => {
|
||||
@@ -360,15 +410,13 @@ export default function Review() {
|
||||
filterValues={{
|
||||
search: searchTerm,
|
||||
}}
|
||||
nextAction={selectedIds.length > 0 ? {
|
||||
label: 'Publish Selected',
|
||||
message: `${selectedIds.length} selected`,
|
||||
onClick: () => handleBulkAction('publish', selectedIds),
|
||||
} : content.filter(c => c.status === 'review').length > 0 ? {
|
||||
label: 'View Published',
|
||||
href: '/writer/published',
|
||||
message: `${content.filter(c => c.status === 'review').length} in review`,
|
||||
} : undefined}
|
||||
primaryAction={{
|
||||
label: 'Approve Selected',
|
||||
icon: <CheckCircleIcon className="w-4 h-4" />,
|
||||
onClick: () => handleBulkAction('bulk_approve', selectedIds),
|
||||
variant: 'success',
|
||||
}}
|
||||
getRowClassName={(row) => row.status === 'approved' ? 'bg-success-50 dark:bg-success-500/10' : ''}
|
||||
onFilterChange={(key, value) => {
|
||||
const stringValue = value === null || value === undefined ? '' : String(value);
|
||||
if (key === 'search') {
|
||||
|
||||
@@ -385,15 +385,7 @@ export default function Tasks() {
|
||||
content_type: typeFilter,
|
||||
source: sourceFilter,
|
||||
}}
|
||||
nextAction={selectedIds.length > 0 ? {
|
||||
label: 'Generate Content',
|
||||
message: `${selectedIds.length} selected`,
|
||||
onClick: () => handleBulkAction('generate_content', selectedIds),
|
||||
} : tasks.filter(t => t.status === 'queued').length > 0 ? {
|
||||
label: 'View Drafts',
|
||||
href: '/writer/content',
|
||||
message: `${tasks.filter(t => t.status === 'queued').length} queued`,
|
||||
} : undefined}
|
||||
getRowClassName={(row) => row.status === 'completed' ? 'bg-success-50 dark:bg-success-500/10' : ''}
|
||||
onFilterChange={(key, value) => {
|
||||
const stringValue = value === null || value === undefined ? '' : String(value);
|
||||
if (key === 'search') {
|
||||
|
||||
Reference in New Issue
Block a user