ui improvements
This commit is contained in:
@@ -35,7 +35,7 @@ const ContentView = lazy(() => import("./pages/Writer/ContentView"));
|
||||
const Drafts = lazy(() => import("./pages/Writer/Drafts"));
|
||||
const Images = lazy(() => import("./pages/Writer/Images"));
|
||||
const Review = lazy(() => import("./pages/Writer/Review"));
|
||||
const Published = lazy(() => import("./pages/Writer/Published"));
|
||||
const Approved = lazy(() => import("./pages/Writer/Approved"));
|
||||
|
||||
// Automation Module - Lazy loaded
|
||||
const AutomationPage = lazy(() => import("./pages/Automation/AutomationPage"));
|
||||
@@ -160,7 +160,9 @@ export default function App() {
|
||||
<Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} />
|
||||
<Route path="/writer/images" element={<Images />} />
|
||||
<Route path="/writer/review" element={<Review />} />
|
||||
<Route path="/writer/published" element={<Published />} />
|
||||
<Route path="/writer/approved" element={<Approved />} />
|
||||
{/* Legacy route - redirect published to approved */}
|
||||
<Route path="/writer/published" element={<Navigate to="/writer/approved" replace />} />
|
||||
|
||||
{/* Automation Module */}
|
||||
<Route path="/automation" element={<AutomationPage />} />
|
||||
|
||||
@@ -26,7 +26,7 @@ const SEARCH_ITEMS: SearchResult[] = [
|
||||
{ title: 'Drafts', path: '/writer/content', type: 'page' },
|
||||
{ title: 'Images', path: '/writer/images', type: 'page' },
|
||||
{ title: 'Review', path: '/writer/review', type: 'page' },
|
||||
{ title: 'Published', path: '/writer/published', type: 'page' },
|
||||
{ title: 'Approved', path: '/writer/approved', type: 'page' },
|
||||
// Setup
|
||||
{ title: 'Sites', path: '/sites', type: 'page' },
|
||||
{ title: 'Add Keywords', path: '/add-keywords', type: 'page' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Published Page Configuration
|
||||
* Centralized config for Published page table, filters, and actions
|
||||
* Approved Page Configuration
|
||||
* Centralized config for Approved page table, filters, and actions
|
||||
*/
|
||||
|
||||
import { Content } from '../../services/api';
|
||||
@@ -39,23 +39,21 @@ export interface HeaderMetricConfig {
|
||||
calculate: (data: { content: Content[]; totalCount: number }) => number;
|
||||
}
|
||||
|
||||
export interface PublishedPageConfig {
|
||||
export interface ApprovedPageConfig {
|
||||
columns: ColumnConfig[];
|
||||
filters: FilterConfig[];
|
||||
headerMetrics: HeaderMetricConfig[];
|
||||
}
|
||||
|
||||
export function createPublishedPageConfig(params: {
|
||||
export function createApprovedPageConfig(params: {
|
||||
searchTerm: string;
|
||||
setSearchTerm: (value: string) => void;
|
||||
statusFilter: string;
|
||||
setStatusFilter: (value: string) => void;
|
||||
publishStatusFilter: string;
|
||||
setPublishStatusFilter: (value: string) => void;
|
||||
setCurrentPage: (page: number) => void;
|
||||
activeSector: { id: number; name: string } | null;
|
||||
onRowClick?: (row: Content) => void;
|
||||
}): PublishedPageConfig {
|
||||
}): ApprovedPageConfig {
|
||||
const showSectorColumn = !params.activeSector;
|
||||
|
||||
const columns: ColumnConfig[] = [
|
||||
@@ -92,35 +90,16 @@ export function createPublishedPageConfig(params: {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Content Status',
|
||||
sortable: true,
|
||||
sortField: 'status',
|
||||
width: '120px',
|
||||
render: (value: string) => {
|
||||
const statusConfig: Record<string, { color: 'amber' | 'success'; label: string }> = {
|
||||
draft: { color: 'amber', label: 'Draft' },
|
||||
published: { color: 'success', label: 'Published' },
|
||||
};
|
||||
const config = statusConfig[value] || { color: 'amber' as const, label: value };
|
||||
return (
|
||||
<Badge color={config.color} size="xs" variant="soft">
|
||||
<span className="text-[11px] font-normal">{config.label}</span>
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'wordpress_status',
|
||||
label: 'WP Status',
|
||||
label: 'Site Status',
|
||||
sortable: false,
|
||||
width: '120px',
|
||||
render: (_value: any, row: Content) => {
|
||||
// Check if content has been published to WordPress
|
||||
if (!row.external_id) {
|
||||
return (
|
||||
<Badge color="gray" size="xs" variant="soft">
|
||||
<Badge color="amber" size="xs" variant="soft">
|
||||
<span className="text-[11px] font-normal">Not Published</span>
|
||||
</Badge>
|
||||
);
|
||||
@@ -279,25 +258,15 @@ export function createPublishedPageConfig(params: {
|
||||
key: 'search',
|
||||
label: 'Search',
|
||||
type: 'text',
|
||||
placeholder: 'Search published content...',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Content Status',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Statuses' },
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'published', label: 'Published' },
|
||||
],
|
||||
placeholder: 'Search approved content...',
|
||||
},
|
||||
{
|
||||
key: 'publishStatus',
|
||||
label: 'WordPress Status',
|
||||
label: 'Site Status',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All' },
|
||||
{ value: 'published', label: 'Published to WP' },
|
||||
{ value: 'published', label: 'Published to Site' },
|
||||
{ value: 'not_published', label: 'Not Published' },
|
||||
],
|
||||
},
|
||||
@@ -305,38 +274,24 @@ export function createPublishedPageConfig(params: {
|
||||
|
||||
const headerMetrics: HeaderMetricConfig[] = [
|
||||
{
|
||||
label: 'Published',
|
||||
label: 'Approved',
|
||||
accentColor: 'green',
|
||||
calculate: (data: { totalCount: number }) => data.totalCount,
|
||||
tooltip: 'Total published content. Track your complete content library.',
|
||||
tooltip: 'Total approved content ready for publishing.',
|
||||
},
|
||||
{
|
||||
label: 'Synced',
|
||||
label: 'On Site',
|
||||
accentColor: 'blue',
|
||||
calculate: (data: { content: Content[] }) =>
|
||||
data.content.filter(c => c.external_id).length,
|
||||
tooltip: 'Content synced to WordPress. Successfully published on your website.',
|
||||
tooltip: 'Content published to your website.',
|
||||
},
|
||||
{
|
||||
label: 'This Month',
|
||||
accentColor: 'purple',
|
||||
calculate: (data: { content: Content[] }) => {
|
||||
const now = new Date();
|
||||
const thisMonth = now.getMonth();
|
||||
const thisYear = now.getFullYear();
|
||||
return data.content.filter(c => {
|
||||
const date = new Date(c.created_at);
|
||||
return date.getMonth() === thisMonth && date.getFullYear() === thisYear;
|
||||
}).length;
|
||||
},
|
||||
tooltip: 'Content published this month. Track your monthly publishing velocity.',
|
||||
},
|
||||
{
|
||||
label: 'Pending Sync',
|
||||
label: 'Pending',
|
||||
accentColor: 'amber',
|
||||
calculate: (data: { content: Content[] }) =>
|
||||
data.content.filter(c => !c.external_id).length,
|
||||
tooltip: 'Published content not yet synced to WordPress. Sync these to make them live.',
|
||||
tooltip: 'Approved content not yet published to site.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -295,6 +295,45 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
|
||||
},
|
||||
],
|
||||
},
|
||||
'/writer/approved': {
|
||||
rowActions: [
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Edit Content',
|
||||
icon: EditIcon,
|
||||
variant: 'primary',
|
||||
},
|
||||
{
|
||||
key: 'publish_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 Site',
|
||||
icon: <CheckCircleIcon className="w-5 h-5 text-blue-500" />,
|
||||
variant: 'secondary',
|
||||
shouldShow: (row: any) => !!row.external_id, // Only show if published
|
||||
},
|
||||
],
|
||||
bulkActions: [
|
||||
{
|
||||
key: 'bulk_publish_wordpress',
|
||||
label: 'Publish to Site',
|
||||
icon: <ArrowRightIcon className="w-4 h-4" />,
|
||||
variant: 'success',
|
||||
},
|
||||
{
|
||||
key: 'export',
|
||||
label: 'Export Selected',
|
||||
icon: <DownloadIcon className="w-4 h-4 text-blue-light-500" />,
|
||||
variant: 'secondary',
|
||||
},
|
||||
],
|
||||
},
|
||||
// Legacy route - keep for backwards compatibility
|
||||
'/writer/published': {
|
||||
rowActions: [
|
||||
{
|
||||
@@ -352,19 +391,31 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
|
||||
},
|
||||
'/writer/review': {
|
||||
rowActions: [
|
||||
{
|
||||
key: 'approve',
|
||||
label: 'Approve',
|
||||
icon: <CheckCircleIcon className="w-5 h-5" />,
|
||||
variant: 'success',
|
||||
},
|
||||
{
|
||||
key: 'publish_wordpress',
|
||||
label: 'Publish to WordPress',
|
||||
icon: <ArrowRightIcon className="w-5 h-5" />,
|
||||
variant: 'success',
|
||||
variant: 'primary',
|
||||
},
|
||||
],
|
||||
bulkActions: [
|
||||
{
|
||||
key: 'bulk_approve',
|
||||
label: 'Approve Selected',
|
||||
icon: <CheckCircleIcon className="w-5 h-5" />,
|
||||
variant: 'success',
|
||||
},
|
||||
{
|
||||
key: 'bulk_publish_wordpress',
|
||||
label: 'Publish to WordPress',
|
||||
icon: <ArrowRightIcon className="w-5 h-5" />,
|
||||
variant: 'success',
|
||||
variant: 'primary',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ export const routes: RouteConfig[] = [
|
||||
{ path: '/writer', label: 'Dashboard', breadcrumb: 'Writer Dashboard' },
|
||||
{ path: '/writer/tasks', label: 'Tasks', breadcrumb: 'Tasks' },
|
||||
{ path: '/writer/content', label: 'Content', breadcrumb: 'Content' },
|
||||
{ path: '/writer/published', label: 'Published', breadcrumb: 'Published' },
|
||||
{ path: '/writer/approved', label: 'Approved', breadcrumb: 'Approved' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -135,7 +135,7 @@ const AppSidebar: React.FC = () => {
|
||||
{ name: "Drafts", path: "/writer/content" },
|
||||
{ name: "Images", path: "/writer/images" },
|
||||
{ name: "Review", path: "/writer/review" },
|
||||
{ name: "Published", path: "/writer/published" },
|
||||
{ name: "Approved", path: "/writer/approved" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -142,14 +142,15 @@ interface TablePageTemplateProps {
|
||||
icon?: ReactNode;
|
||||
variant?: 'primary' | 'success' | 'danger';
|
||||
}>;
|
||||
// Next action button for workflow guidance (shown in action bar)
|
||||
nextAction?: {
|
||||
// Primary workflow action button (shown before Bulk Actions, enabled only with selection)
|
||||
primaryAction?: {
|
||||
label: string;
|
||||
message?: string; // Message to show above button (e.g., "5 selected")
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
disabled?: boolean;
|
||||
icon?: ReactNode;
|
||||
onClick: () => void;
|
||||
variant?: 'primary' | 'success' | 'warning';
|
||||
};
|
||||
// Custom row highlight function (returns bg class based on row data)
|
||||
getRowClassName?: (row: any) => string;
|
||||
}
|
||||
|
||||
export default function TablePageTemplate({
|
||||
@@ -186,7 +187,8 @@ export default function TablePageTemplate({
|
||||
className = '',
|
||||
customActions,
|
||||
bulkActions: customBulkActions,
|
||||
nextAction,
|
||||
primaryAction,
|
||||
getRowClassName,
|
||||
}: TablePageTemplateProps) {
|
||||
const location = useLocation();
|
||||
const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false);
|
||||
@@ -561,8 +563,27 @@ export default function TablePageTemplate({
|
||||
<div className={className}>
|
||||
{/* Bulk Actions and Action Buttons Row - Fixed height container */}
|
||||
<div className="flex items-center justify-between min-h-[65px] mb-4 mt-[10px]">
|
||||
{/* Left side - Bulk Actions and Filter Toggle */}
|
||||
{/* Left side - Primary Action, Bulk Actions, and Filter Toggle */}
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* Primary Workflow Action Button - Only enabled with selection */}
|
||||
{primaryAction && (
|
||||
<Button
|
||||
size="md"
|
||||
onClick={primaryAction.onClick}
|
||||
disabled={selectedIds.length === 0}
|
||||
variant={primaryAction.variant === 'success' ? 'success' : primaryAction.variant === 'warning' ? 'primary' : 'primary'}
|
||||
startIcon={primaryAction.icon}
|
||||
className={selectedIds.length === 0 ? "opacity-50 cursor-not-allowed" : ""}
|
||||
>
|
||||
{primaryAction.label}
|
||||
{selectedIds.length > 0 && (
|
||||
<span className="ml-2 inline-flex items-center justify-center px-2 py-0.5 text-xs font-medium rounded-full bg-white/20 text-white">
|
||||
{selectedIds.length}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Bulk Actions - Single button if only one action, dropdown if multiple */}
|
||||
{bulkActions.length > 0 && (
|
||||
<div className="inline-block">
|
||||
@@ -750,44 +771,6 @@ export default function TablePageTemplate({
|
||||
{createLabel}
|
||||
</Button>
|
||||
)}
|
||||
{/* Next Action Button - Workflow Guidance */}
|
||||
{nextAction && (
|
||||
<div className="flex flex-col items-end ml-2">
|
||||
{nextAction.message && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 mb-1">{nextAction.message}</span>
|
||||
)}
|
||||
{nextAction.href ? (
|
||||
<a
|
||||
href={nextAction.href}
|
||||
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
|
||||
nextAction.disabled
|
||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed dark:bg-gray-700 dark:text-gray-500'
|
||||
: 'bg-success-500 text-white hover:bg-success-600 dark:bg-success-600 dark:hover:bg-success-500'
|
||||
}`}
|
||||
>
|
||||
{nextAction.label}
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
onClick={nextAction.onClick}
|
||||
disabled={nextAction.disabled}
|
||||
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
|
||||
nextAction.disabled
|
||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed dark:bg-gray-700 dark:text-gray-500'
|
||||
: 'bg-success-500 text-white hover:bg-success-600 dark:bg-success-600 dark:hover:bg-success-500'
|
||||
}`}
|
||||
>
|
||||
{nextAction.label}
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -906,10 +889,13 @@ export default function TablePageTemplate({
|
||||
// Use same logic as handleBulkAddSelected - check if isAdded is truthy
|
||||
const isRowAdded = !!(row as any).isAdded;
|
||||
|
||||
// Get custom row class from prop if provided
|
||||
const customRowClass = getRowClassName ? getRowClassName(row) : '';
|
||||
|
||||
return (
|
||||
<React.Fragment key={row.id || index}>
|
||||
<TableRow
|
||||
className={`igny8-data-row ${isRowAdded ? 'bg-blue-50 dark:bg-blue-500/10' : ''}`}
|
||||
className={`igny8-data-row ${isRowAdded ? 'bg-blue-50 dark:bg-blue-500/10' : ''} ${customRowClass}`}
|
||||
>
|
||||
{selection && (
|
||||
<TableCell className="px-5 py-4 text-start">
|
||||
|
||||
Reference in New Issue
Block a user