From 302af6337e2c52aec95a032038005a12845fe7e1 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sat, 27 Dec 2025 06:08:29 +0000 Subject: [PATCH] ui improvements --- frontend/src/App.tsx | 6 +- .../src/components/common/SearchModal.tsx | 2 +- ...blished.config.tsx => approved.config.tsx} | 77 ++++-------------- .../src/config/pages/table-actions.config.tsx | 55 ++++++++++++- frontend/src/config/routes.config.ts | 2 +- frontend/src/layout/AppSidebar.tsx | 2 +- frontend/src/pages/Planner/Clusters.tsx | 14 ++-- frontend/src/pages/Planner/Ideas.tsx | 14 ++-- frontend/src/pages/Planner/Keywords.tsx | 20 ++--- .../Writer/{Published.tsx => Approved.tsx} | 64 +++++++-------- frontend/src/pages/Writer/Content.tsx | 10 +-- frontend/src/pages/Writer/Review.tsx | 74 ++++++++++++++--- frontend/src/pages/Writer/Tasks.tsx | 10 +-- frontend/src/templates/TablePageTemplate.tsx | 80 ++++++++----------- 14 files changed, 219 insertions(+), 211 deletions(-) rename frontend/src/config/pages/{published.config.tsx => approved.config.tsx} (79%) rename frontend/src/pages/Writer/{Published.tsx => Approved.tsx} (85%) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d8a19896..55596f31 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() { } /> } /> } /> - } /> + } /> + {/* Legacy route - redirect published to approved */} + } /> {/* Automation Module */} } /> diff --git a/frontend/src/components/common/SearchModal.tsx b/frontend/src/components/common/SearchModal.tsx index 75490558..b2312b28 100644 --- a/frontend/src/components/common/SearchModal.tsx +++ b/frontend/src/components/common/SearchModal.tsx @@ -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' }, diff --git a/frontend/src/config/pages/published.config.tsx b/frontend/src/config/pages/approved.config.tsx similarity index 79% rename from frontend/src/config/pages/published.config.tsx rename to frontend/src/config/pages/approved.config.tsx index 1d45a159..f092770a 100644 --- a/frontend/src/config/pages/published.config.tsx +++ b/frontend/src/config/pages/approved.config.tsx @@ -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: { ), }, - { - key: 'status', - label: 'Content Status', - sortable: true, - sortField: 'status', - width: '120px', - render: (value: string) => { - const statusConfig: Record = { - draft: { color: 'amber', label: 'Draft' }, - published: { color: 'success', label: 'Published' }, - }; - const config = statusConfig[value] || { color: 'amber' as const, label: value }; - return ( - - {config.label} - - ); - }, - }, { 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 ( - + Not Published ); @@ -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.', }, ]; diff --git a/frontend/src/config/pages/table-actions.config.tsx b/frontend/src/config/pages/table-actions.config.tsx index c53c537a..288352c5 100644 --- a/frontend/src/config/pages/table-actions.config.tsx +++ b/frontend/src/config/pages/table-actions.config.tsx @@ -295,6 +295,45 @@ const tableActionsConfigs: Record = { }, ], }, + '/writer/approved': { + rowActions: [ + { + key: 'edit', + label: 'Edit Content', + icon: EditIcon, + variant: 'primary', + }, + { + key: 'publish_wordpress', + label: 'Publish to Site', + icon: , + variant: 'success', + shouldShow: (row: any) => !row.external_id, // Only show if not published + }, + { + key: 'view_on_wordpress', + label: 'View on Site', + icon: , + variant: 'secondary', + shouldShow: (row: any) => !!row.external_id, // Only show if published + }, + ], + bulkActions: [ + { + key: 'bulk_publish_wordpress', + label: 'Publish to Site', + icon: , + variant: 'success', + }, + { + key: 'export', + label: 'Export Selected', + icon: , + variant: 'secondary', + }, + ], + }, + // Legacy route - keep for backwards compatibility '/writer/published': { rowActions: [ { @@ -352,19 +391,31 @@ const tableActionsConfigs: Record = { }, '/writer/review': { rowActions: [ + { + key: 'approve', + label: 'Approve', + icon: , + variant: 'success', + }, { key: 'publish_wordpress', label: 'Publish to WordPress', icon: , - variant: 'success', + variant: 'primary', }, ], bulkActions: [ + { + key: 'bulk_approve', + label: 'Approve Selected', + icon: , + variant: 'success', + }, { key: 'bulk_publish_wordpress', label: 'Publish to WordPress', icon: , - variant: 'success', + variant: 'primary', }, ], }, diff --git a/frontend/src/config/routes.config.ts b/frontend/src/config/routes.config.ts index ba8dd933..21fd434b 100644 --- a/frontend/src/config/routes.config.ts +++ b/frontend/src/config/routes.config.ts @@ -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' }, ], }, { diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index 270c4feb..68ba84aa 100644 --- a/frontend/src/layout/AppSidebar.tsx +++ b/frontend/src/layout/AppSidebar.tsx @@ -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" }, ], }); } diff --git a/frontend/src/pages/Planner/Clusters.tsx b/frontend/src/pages/Planner/Clusters.tsx index 47435979..bdabaed2 100644 --- a/frontend/src/pages/Planner/Clusters.tsx +++ b/frontend/src/pages/Planner/Clusters.tsx @@ -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: , + 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') { diff --git a/frontend/src/pages/Planner/Ideas.tsx b/frontend/src/pages/Planner/Ideas.tsx index f0287ef5..cb392835 100644 --- a/frontend/src/pages/Planner/Ideas.tsx +++ b/frontend/src/pages/Planner/Ideas.tsx @@ -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: , 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') { diff --git a/frontend/src/pages/Planner/Keywords.tsx b/frontend/src/pages/Planner/Keywords.tsx index b098c7cb..d6afa5c2 100644 --- a/frontend/src/pages/Planner/Keywords.tsx +++ b/frontend/src/pages/Planner/Keywords.tsx @@ -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: , + 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); diff --git a/frontend/src/pages/Writer/Published.tsx b/frontend/src/pages/Writer/Approved.tsx similarity index 85% rename from frontend/src/pages/Writer/Published.tsx rename to frontend/src/pages/Writer/Approved.tsx index e7269459..3364de83 100644 --- a/frontend/src/pages/Writer/Published.tsx +++ b/frontend/src/pages/Writer/Approved.tsx @@ -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([]); 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([]); @@ -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 ( <> , color: 'green' }} + title="Approved" + badge={{ icon: , color: 'green' }} parent="Writer" /> 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: , + 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() { 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: , accentColor: 'green', - href: '/writer/published', }, { - title: 'Draft Content', - value: content.filter(c => c.status === 'draft').length.toLocaleString(), - subtitle: 'Not yet published', - icon: , + title: 'Published to Site', + value: content.filter(c => c.external_id).length.toLocaleString(), + subtitle: 'on WordPress', + icon: , 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', }} diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index e10148f6..f95a8e3b 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -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); diff --git a/frontend/src/pages/Writer/Review.tsx b/frontend/src/pages/Writer/Review.tsx index a81c7efa..c04511af 100644 --- a/frontend/src/pages/Writer/Review.tsx +++ b/frontend/src/pages/Writer/Review.tsx @@ -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: , + 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') { diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx index 3e7834d1..bdad9afb 100644 --- a/frontend/src/pages/Writer/Tasks.tsx +++ b/frontend/src/pages/Writer/Tasks.tsx @@ -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') { diff --git a/frontend/src/templates/TablePageTemplate.tsx b/frontend/src/templates/TablePageTemplate.tsx index 4e48d3e2..17c6313b 100644 --- a/frontend/src/templates/TablePageTemplate.tsx +++ b/frontend/src/templates/TablePageTemplate.tsx @@ -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({
{/* Bulk Actions and Action Buttons Row - Fixed height container */}
- {/* Left side - Bulk Actions and Filter Toggle */} + {/* Left side - Primary Action, Bulk Actions, and Filter Toggle */}
+ {/* Primary Workflow Action Button - Only enabled with selection */} + {primaryAction && ( + + )} + {/* Bulk Actions - Single button if only one action, dropdown if multiple */} {bulkActions.length > 0 && (
@@ -750,44 +771,6 @@ export default function TablePageTemplate({ {createLabel} )} - {/* Next Action Button - Workflow Guidance */} - {nextAction && ( -
- {nextAction.message && ( - {nextAction.message} - )} - {nextAction.href ? ( - - {nextAction.label} - - - - - ) : ( - - )} -
- )}
@@ -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 ( {selection && (