more fixes
This commit is contained in:
@@ -1,183 +0,0 @@
|
|||||||
/**
|
|
||||||
* StatusMetricsCard Component
|
|
||||||
* Displays status metrics in a card format with colored left border
|
|
||||||
* Used in table action rows to show page-specific status metrics
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
export interface StatusMetricItem {
|
|
||||||
label: string;
|
|
||||||
value: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StatusMetricsCardProps {
|
|
||||||
/** Title for the card */
|
|
||||||
title: string;
|
|
||||||
/** Subtitle text shown below the main count */
|
|
||||||
subtitle?: string;
|
|
||||||
/** Icon to display */
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
/** Color variant - matches page badge colors */
|
|
||||||
color: 'blue' | 'orange' | 'pink' | 'emerald' | 'green' | 'purple' | 'amber' | 'red' | 'indigo' | 'cyan' | 'teal';
|
|
||||||
/** Main count value */
|
|
||||||
count: number;
|
|
||||||
/** Array of metric items for 2-column layout */
|
|
||||||
metrics?: StatusMetricItem[];
|
|
||||||
/** Review count to display */
|
|
||||||
reviewCount?: number;
|
|
||||||
/** Link to review page */
|
|
||||||
reviewLink?: string;
|
|
||||||
/** Custom action button */
|
|
||||||
actionButton?: {
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorClasses: Record<string, { border: string; bg: string; text: string; iconBg: string }> = {
|
|
||||||
blue: {
|
|
||||||
border: 'border-l-blue-500',
|
|
||||||
bg: 'bg-blue-50 dark:bg-blue-500/10',
|
|
||||||
text: 'text-blue-700 dark:text-blue-300',
|
|
||||||
iconBg: 'bg-blue-100 dark:bg-blue-500/20'
|
|
||||||
},
|
|
||||||
orange: {
|
|
||||||
border: 'border-l-orange-500',
|
|
||||||
bg: 'bg-orange-50 dark:bg-orange-500/10',
|
|
||||||
text: 'text-orange-700 dark:text-orange-300',
|
|
||||||
iconBg: 'bg-orange-100 dark:bg-orange-500/20'
|
|
||||||
},
|
|
||||||
pink: {
|
|
||||||
border: 'border-l-pink-500',
|
|
||||||
bg: 'bg-pink-50 dark:bg-pink-500/10',
|
|
||||||
text: 'text-pink-700 dark:text-pink-300',
|
|
||||||
iconBg: 'bg-pink-100 dark:bg-pink-500/20'
|
|
||||||
},
|
|
||||||
emerald: {
|
|
||||||
border: 'border-l-emerald-500',
|
|
||||||
bg: 'bg-emerald-50 dark:bg-emerald-500/10',
|
|
||||||
text: 'text-emerald-700 dark:text-emerald-300',
|
|
||||||
iconBg: 'bg-emerald-100 dark:bg-emerald-500/20'
|
|
||||||
},
|
|
||||||
green: {
|
|
||||||
border: 'border-l-green-500',
|
|
||||||
bg: 'bg-green-50 dark:bg-green-500/10',
|
|
||||||
text: 'text-green-700 dark:text-green-300',
|
|
||||||
iconBg: 'bg-green-100 dark:bg-green-500/20'
|
|
||||||
},
|
|
||||||
purple: {
|
|
||||||
border: 'border-l-purple-500',
|
|
||||||
bg: 'bg-purple-50 dark:bg-purple-500/10',
|
|
||||||
text: 'text-purple-700 dark:text-purple-300',
|
|
||||||
iconBg: 'bg-purple-100 dark:bg-purple-500/20'
|
|
||||||
},
|
|
||||||
amber: {
|
|
||||||
border: 'border-l-amber-500',
|
|
||||||
bg: 'bg-amber-50 dark:bg-amber-500/10',
|
|
||||||
text: 'text-amber-700 dark:text-amber-300',
|
|
||||||
iconBg: 'bg-amber-100 dark:bg-amber-500/20'
|
|
||||||
},
|
|
||||||
red: {
|
|
||||||
border: 'border-l-red-500',
|
|
||||||
bg: 'bg-red-50 dark:bg-red-500/10',
|
|
||||||
text: 'text-red-700 dark:text-red-300',
|
|
||||||
iconBg: 'bg-red-100 dark:bg-red-500/20'
|
|
||||||
},
|
|
||||||
indigo: {
|
|
||||||
border: 'border-l-indigo-500',
|
|
||||||
bg: 'bg-indigo-50 dark:bg-indigo-500/10',
|
|
||||||
text: 'text-indigo-700 dark:text-indigo-300',
|
|
||||||
iconBg: 'bg-indigo-100 dark:bg-indigo-500/20'
|
|
||||||
},
|
|
||||||
cyan: {
|
|
||||||
border: 'border-l-cyan-500',
|
|
||||||
bg: 'bg-cyan-50 dark:bg-cyan-500/10',
|
|
||||||
text: 'text-cyan-700 dark:text-cyan-300',
|
|
||||||
iconBg: 'bg-cyan-100 dark:bg-cyan-500/20'
|
|
||||||
},
|
|
||||||
teal: {
|
|
||||||
border: 'border-l-teal-500',
|
|
||||||
bg: 'bg-teal-50 dark:bg-teal-500/10',
|
|
||||||
text: 'text-teal-700 dark:text-teal-300',
|
|
||||||
iconBg: 'bg-teal-100 dark:bg-teal-500/20'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function StatusMetricsCard({
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
icon,
|
|
||||||
color,
|
|
||||||
count,
|
|
||||||
metrics,
|
|
||||||
reviewCount,
|
|
||||||
reviewLink = '/writer/review',
|
|
||||||
actionButton,
|
|
||||||
}: StatusMetricsCardProps) {
|
|
||||||
const colors = colorClasses[color] || colorClasses.blue;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${colors.bg} border-l-4 ${colors.border} rounded-lg px-4 py-3 min-w-[280px]`}>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
{/* Icon */}
|
|
||||||
{icon && (
|
|
||||||
<div className={`${colors.iconBg} rounded-lg p-2 flex-shrink-0`}>
|
|
||||||
<div className={`${colors.text} w-5 h-5`}>
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
{/* Title and Count Row */}
|
|
||||||
<div className="flex items-center justify-between gap-4 mb-1">
|
|
||||||
<h4 className={`text-sm font-semibold ${colors.text}`}>{title}</h4>
|
|
||||||
<span className="text-2xl font-bold text-gray-900 dark:text-white">{count}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Subtitle */}
|
|
||||||
{subtitle && (
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">{subtitle}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 2-column metrics */}
|
|
||||||
{metrics && metrics.length > 0 && (
|
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 mt-2 text-sm">
|
|
||||||
{metrics.map((metric, index) => (
|
|
||||||
<div key={index} className="flex justify-between">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">{metric.label}</span>
|
|
||||||
<span className="font-medium text-gray-900 dark:text-white">{metric.value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Review link/button */}
|
|
||||||
{(reviewCount !== undefined || actionButton) && (
|
|
||||||
<div className="mt-3 pt-2 border-t border-gray-200/50 dark:border-gray-700/50 flex items-center justify-between gap-2">
|
|
||||||
{reviewCount !== undefined && (
|
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<span className="font-semibold text-gray-900 dark:text-white">{reviewCount}</span> awaiting review
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{actionButton && (
|
|
||||||
<Link
|
|
||||||
to={actionButton.href}
|
|
||||||
className="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-white bg-emerald-600 hover:bg-emerald-700 rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
{actionButton.label}
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -139,10 +139,23 @@ const LayoutContent: React.FC = () => {
|
|||||||
accentColor = 'purple';
|
accentColor = 'purple';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set content balance - show as "X/Y Content" for simplicity
|
// Format credit value with K/M suffix for large numbers
|
||||||
|
const formatCredits = (val: number): string => {
|
||||||
|
if (val >= 1000000) {
|
||||||
|
const millions = val / 1000000;
|
||||||
|
return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (val >= 1000) {
|
||||||
|
const thousands = val / 1000;
|
||||||
|
return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`;
|
||||||
|
}
|
||||||
|
return val.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set credit balance - show as "used/total Credits"
|
||||||
setMetrics([{
|
setMetrics([{
|
||||||
label: 'Content',
|
label: 'Credits',
|
||||||
value: total > 0 ? `${remaining}/${total}` : remaining,
|
value: total > 0 ? `${formatCredits(remaining)}/${formatCredits(total)}` : formatCredits(remaining),
|
||||||
accentColor,
|
accentColor,
|
||||||
}]);
|
}]);
|
||||||
}, [balance, isAuthenticated, setMetrics]);
|
}, [balance, isAuthenticated, setMetrics]);
|
||||||
@@ -160,8 +173,8 @@ const LayoutContent: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
{/* Pending Payment Banner - Shows when account status is 'pending_payment' */}
|
{/* Pending Payment Banner - Shows when account status is 'pending_payment' */}
|
||||||
<PendingPaymentBanner className="mx-4 mt-4 md:mx-6 md:mt-6" />
|
<PendingPaymentBanner className="mx-4 mt-2 md:mx-6 md:mt-2" />
|
||||||
<div className="p-4 pb-20 md:p-6 md:pb-24">
|
<div className="px-4 pt-1.5 pb-20 md:px-6 md:pt-1.5 md:pb-24">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { useProgressModal } from '../../hooks/useProgressModal';
|
|||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||||
import { PencilSquareIcon } from '@heroicons/react/24/outline';
|
import { PencilSquareIcon } from '@heroicons/react/24/outline';
|
||||||
import StatusMetricsCard from '../../components/common/StatusMetricsCard';
|
|
||||||
|
|
||||||
export default function Content() {
|
export default function Content() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -56,22 +55,6 @@ export default function Content() {
|
|||||||
const progressModal = useProgressModal();
|
const progressModal = useProgressModal();
|
||||||
const hasReloadedRef = useRef(false);
|
const hasReloadedRef = useRef(false);
|
||||||
|
|
||||||
// Review count state
|
|
||||||
const [reviewCount, setReviewCount] = useState(0);
|
|
||||||
|
|
||||||
// Load review count
|
|
||||||
useEffect(() => {
|
|
||||||
const loadReviewCount = async () => {
|
|
||||||
try {
|
|
||||||
const data = await fetchContent({ status: 'review', page_size: 1 });
|
|
||||||
setReviewCount(data.count || 0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching review count:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadReviewCount();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Load content - wrapped in useCallback
|
// Load content - wrapped in useCallback
|
||||||
const loadContent = useCallback(async () => {
|
const loadContent = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -290,24 +273,6 @@ export default function Content() {
|
|||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onBulkDelete={handleBulkDelete}
|
onBulkDelete={handleBulkDelete}
|
||||||
getItemDisplayName={(row: ContentType) => row.title || `Content #${row.id}`}
|
getItemDisplayName={(row: ContentType) => row.title || `Content #${row.id}`}
|
||||||
statusExplainer={
|
|
||||||
<StatusMetricsCard
|
|
||||||
title="Content Drafts"
|
|
||||||
color="orange"
|
|
||||||
icon={<PencilSquareIcon className="w-5 h-5" />}
|
|
||||||
count={totalCount}
|
|
||||||
subtitle="draft content items"
|
|
||||||
metrics={[
|
|
||||||
{ label: 'Image Prompts', value: `${content.filter(c => c.has_image_prompts).length}/${content.length}` },
|
|
||||||
{ label: 'Images Generated', value: `${content.filter(c => c.has_generated_images).length}/${content.length}` },
|
|
||||||
]}
|
|
||||||
reviewCount={reviewCount}
|
|
||||||
actionButton={{
|
|
||||||
label: 'Review',
|
|
||||||
href: '/writer/review',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
|
{/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
fetchAPI,
|
fetchAPI,
|
||||||
deleteContent,
|
deleteContent,
|
||||||
bulkDeleteContent,
|
bulkDeleteContent,
|
||||||
fetchContent,
|
|
||||||
} from '../../services/api';
|
} from '../../services/api';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { FileIcon, DownloadIcon, ArrowRightIcon } from '../../icons';
|
import { FileIcon, DownloadIcon, ArrowRightIcon } from '../../icons';
|
||||||
@@ -27,7 +26,6 @@ import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQu
|
|||||||
import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordStatusUpdateModal';
|
import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordStatusUpdateModal';
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { Modal } from '../../components/ui/modal';
|
import { Modal } from '../../components/ui/modal';
|
||||||
import StatusMetricsCard from '../../components/common/StatusMetricsCard';
|
|
||||||
|
|
||||||
export default function Images() {
|
export default function Images() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -70,22 +68,6 @@ export default function Images() {
|
|||||||
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
|
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
|
||||||
const [modalImageUrl, setModalImageUrl] = useState<string | null>(null);
|
const [modalImageUrl, setModalImageUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
// Review count state
|
|
||||||
const [reviewCount, setReviewCount] = useState(0);
|
|
||||||
|
|
||||||
// Load review count
|
|
||||||
useEffect(() => {
|
|
||||||
const loadReviewCount = async () => {
|
|
||||||
try {
|
|
||||||
const data = await fetchContent({ status: 'review', page_size: 1 });
|
|
||||||
setReviewCount(data.count || 0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching review count:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadReviewCount();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Load images - wrapped in useCallback
|
// Load images - wrapped in useCallback
|
||||||
const loadImages = useCallback(async () => {
|
const loadImages = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -533,24 +515,6 @@ export default function Images() {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
onRowAction={handleRowAction}
|
onRowAction={handleRowAction}
|
||||||
statusExplainer={
|
|
||||||
<StatusMetricsCard
|
|
||||||
title="Content Images"
|
|
||||||
color="pink"
|
|
||||||
icon={<PhotoIcon className="w-5 h-5" />}
|
|
||||||
count={totalCount}
|
|
||||||
subtitle="content items with images"
|
|
||||||
metrics={[
|
|
||||||
{ label: 'Need Images', value: images.filter(i => i.overall_status === 'pending').length },
|
|
||||||
{ label: 'Images Complete', value: images.filter(i => i.overall_status === 'complete').length },
|
|
||||||
]}
|
|
||||||
reviewCount={reviewCount}
|
|
||||||
actionButton={{
|
|
||||||
label: 'Review',
|
|
||||||
href: '/writer/review',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<ImageQueueModal
|
<ImageQueueModal
|
||||||
isOpen={isQueueModalOpen}
|
isOpen={isQueueModalOpen}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { useSectorStore } from '../../store/sectorStore';
|
|||||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter';
|
import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter';
|
||||||
import StatusMetricsCard from '../../components/common/StatusMetricsCard';
|
|
||||||
|
|
||||||
export default function Review() {
|
export default function Review() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -454,19 +453,6 @@ export default function Review() {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
onRowAction={handleRowAction}
|
onRowAction={handleRowAction}
|
||||||
statusExplainer={
|
|
||||||
<StatusMetricsCard
|
|
||||||
title="In Review"
|
|
||||||
color="emerald"
|
|
||||||
icon={<ClipboardDocumentCheckIcon className="w-5 h-5" />}
|
|
||||||
count={totalCount}
|
|
||||||
subtitle="awaiting approval"
|
|
||||||
actionButton={{
|
|
||||||
label: 'Approved',
|
|
||||||
href: '/writer/approved',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<ModuleMetricsFooter
|
<ModuleMetricsFooter
|
||||||
metrics={[
|
metrics={[
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
TaskCreateData,
|
TaskCreateData,
|
||||||
fetchClusters,
|
fetchClusters,
|
||||||
Cluster,
|
Cluster,
|
||||||
fetchContent,
|
|
||||||
} from '../../services/api';
|
} from '../../services/api';
|
||||||
import FormModal from '../../components/common/FormModal';
|
import FormModal from '../../components/common/FormModal';
|
||||||
import ProgressModal from '../../components/common/ProgressModal';
|
import ProgressModal from '../../components/common/ProgressModal';
|
||||||
@@ -33,7 +32,6 @@ import { usePageSizeStore } from '../../store/pageSizeStore';
|
|||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||||
import { DocumentTextIcon } from '@heroicons/react/24/outline';
|
import { DocumentTextIcon } from '@heroicons/react/24/outline';
|
||||||
import StatusMetricsCard from '../../components/common/StatusMetricsCard';
|
|
||||||
|
|
||||||
export default function Tasks() {
|
export default function Tasks() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -82,27 +80,8 @@ export default function Tasks() {
|
|||||||
// Progress modal for AI functions
|
// Progress modal for AI functions
|
||||||
const progressModal = useProgressModal();
|
const progressModal = useProgressModal();
|
||||||
|
|
||||||
// Review count state
|
|
||||||
const [reviewCount, setReviewCount] = useState(0);
|
|
||||||
|
|
||||||
// AI Function Logs state
|
|
||||||
|
|
||||||
|
|
||||||
const hasReloadedRef = useRef<boolean>(false);
|
const hasReloadedRef = useRef<boolean>(false);
|
||||||
|
|
||||||
// Load review count
|
|
||||||
useEffect(() => {
|
|
||||||
const loadReviewCount = async () => {
|
|
||||||
try {
|
|
||||||
const data = await fetchContent({ status: 'review', page_size: 1 });
|
|
||||||
setReviewCount(data.count || 0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching review count:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadReviewCount();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Load clusters for filter dropdown
|
// Load clusters for filter dropdown
|
||||||
@@ -486,24 +465,6 @@ export default function Tasks() {
|
|||||||
setTypeFilter('');
|
setTypeFilter('');
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
statusExplainer={
|
|
||||||
<StatusMetricsCard
|
|
||||||
title="In Queue"
|
|
||||||
color="blue"
|
|
||||||
icon={<DocumentTextIcon className="w-5 h-5" />}
|
|
||||||
count={totalCount}
|
|
||||||
subtitle="content items queued"
|
|
||||||
metrics={[
|
|
||||||
{ label: 'Queued', value: tasks.filter(t => t.status === 'queued').length },
|
|
||||||
{ label: 'Processing', value: tasks.filter(t => t.status === 'in_progress').length },
|
|
||||||
]}
|
|
||||||
reviewCount={reviewCount}
|
|
||||||
actionButton={{
|
|
||||||
label: 'Review',
|
|
||||||
href: '/writer/review',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
|
{/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
|
||||||
|
|||||||
@@ -240,7 +240,7 @@
|
|||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
background-color: #f8fafc !important; /* Light gray background */
|
background-color: #f8fafc !important; /* Light gray background */
|
||||||
border-bottom: 2px solid #e2e8f0 !important; /* Thicker bottom border */
|
border-bottom: 2px solid #e2e8f0 !important; /* Thicker bottom border */
|
||||||
text-transform: uppercase;
|
text-transform: capitalize;
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,10 +365,10 @@ select.igny8-select-styled option:checked {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.igny8-header-metric-label {
|
.igny8-header-metric-label {
|
||||||
font-size: 10px;
|
font-size: 13px; /* increased from 10px by 25%+ */
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
text-transform: capitalize;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.3px;
|
||||||
color: rgb(100 116 139); /* slate-500 */
|
color: rgb(100 116 139); /* slate-500 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +377,7 @@ select.igny8-select-styled option:checked {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.igny8-header-metric-value {
|
.igny8-header-metric-value {
|
||||||
font-size: 14px;
|
font-size: 16px; /* increased from 14px */
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: rgb(30 41 59); /* slate-800 */
|
color: rgb(30 41 59); /* slate-800 */
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
|||||||
@@ -34,6 +34,20 @@ const formatColumnKey = (key: string): string => {
|
|||||||
.join(' ');
|
.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to format metric values with K/M suffixes
|
||||||
|
// 3 digits show actual, 4+ digits show K, 1M+ show M
|
||||||
|
const formatMetricValue = (value: number): string => {
|
||||||
|
if (value >= 1000000) {
|
||||||
|
const millions = value / 1000000;
|
||||||
|
return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (value >= 1000) {
|
||||||
|
const thousands = value / 1000;
|
||||||
|
return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`;
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
import Checkbox from '../components/form/input/Checkbox';
|
import Checkbox from '../components/form/input/Checkbox';
|
||||||
import Button from '../components/ui/button/Button';
|
import Button from '../components/ui/button/Button';
|
||||||
import Input from '../components/form/input/InputField';
|
import Input from '../components/form/input/InputField';
|
||||||
@@ -41,9 +55,8 @@ import SelectDropdown from '../components/form/SelectDropdown';
|
|||||||
import { Dropdown } from '../components/ui/dropdown/Dropdown';
|
import { Dropdown } from '../components/ui/dropdown/Dropdown';
|
||||||
import { DropdownItem } from '../components/ui/dropdown/DropdownItem';
|
import { DropdownItem } from '../components/ui/dropdown/DropdownItem';
|
||||||
import AlertModal from '../components/ui/alert/AlertModal';
|
import AlertModal from '../components/ui/alert/AlertModal';
|
||||||
import { ChevronDownIcon, MoreDotIcon, PlusIcon } from '../icons';
|
import { ChevronDownIcon, MoreDotIcon, PlusIcon, InfoIcon } from '../icons';
|
||||||
import { FunnelIcon } from '@heroicons/react/24/outline';
|
import { FunnelIcon } from '@heroicons/react/24/outline';
|
||||||
import { useHeaderMetrics } from '../context/HeaderMetricsContext';
|
|
||||||
import { useToast } from '../components/ui/toast/ToastContainer';
|
import { useToast } from '../components/ui/toast/ToastContainer';
|
||||||
import { getDeleteModalConfig } from '../config/pages/delete-modal.config';
|
import { getDeleteModalConfig } from '../config/pages/delete-modal.config';
|
||||||
import { getBulkActionModalConfig } from '../config/pages/bulk-action-modal.config';
|
import { getBulkActionModalConfig } from '../config/pages/bulk-action-modal.config';
|
||||||
@@ -55,6 +68,7 @@ import { usePageSizeStore } from '../store/pageSizeStore';
|
|||||||
import { useColumnVisibilityStore } from '../store/columnVisibilityStore';
|
import { useColumnVisibilityStore } from '../store/columnVisibilityStore';
|
||||||
import ToggleTableRow, { ToggleButton } from '../components/common/ToggleTableRow';
|
import ToggleTableRow, { ToggleButton } from '../components/common/ToggleTableRow';
|
||||||
import ColumnSelector from '../components/common/ColumnSelector';
|
import ColumnSelector from '../components/common/ColumnSelector';
|
||||||
|
import { Tooltip } from '../components/ui/tooltip/Tooltip';
|
||||||
|
|
||||||
interface ColumnConfig {
|
interface ColumnConfig {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -94,6 +108,7 @@ interface HeaderMetrics {
|
|||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
accentColor: 'blue' | 'green' | 'amber' | 'purple';
|
accentColor: 'blue' | 'green' | 'amber' | 'purple';
|
||||||
|
tooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TablePageTemplateProps {
|
interface TablePageTemplateProps {
|
||||||
@@ -164,8 +179,6 @@ interface TablePageTemplateProps {
|
|||||||
};
|
};
|
||||||
// Custom row highlight function (returns bg class based on row data)
|
// Custom row highlight function (returns bg class based on row data)
|
||||||
getRowClassName?: (row: any) => string;
|
getRowClassName?: (row: any) => string;
|
||||||
// Status explainer component to display on right side of table actions row
|
|
||||||
statusExplainer?: ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TablePageTemplate({
|
export default function TablePageTemplate({
|
||||||
@@ -204,7 +217,6 @@ export default function TablePageTemplate({
|
|||||||
bulkActions: customBulkActions,
|
bulkActions: customBulkActions,
|
||||||
primaryAction,
|
primaryAction,
|
||||||
getRowClassName,
|
getRowClassName,
|
||||||
statusExplainer,
|
|
||||||
}: TablePageTemplateProps) {
|
}: TablePageTemplateProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false);
|
const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false);
|
||||||
@@ -264,7 +276,6 @@ export default function TablePageTemplate({
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setMetrics } = useHeaderMetrics();
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { pageSize, setPageSize } = usePageSizeStore();
|
const { pageSize, setPageSize } = usePageSizeStore();
|
||||||
const { getVisibleColumns, setVisibleColumns: saveVisibleColumns } = useColumnVisibilityStore();
|
const { getVisibleColumns, setVisibleColumns: saveVisibleColumns } = useColumnVisibilityStore();
|
||||||
@@ -443,50 +454,6 @@ export default function TablePageTemplate({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set header metrics when provided
|
|
||||||
// Use a ref to track previous metrics and only update when values actually change
|
|
||||||
const prevMetricsRef = useRef<string>('');
|
|
||||||
const hasSetMetricsRef = useRef(false);
|
|
||||||
|
|
||||||
// Create a stable key for comparison - only when headerMetrics has values
|
|
||||||
const metricsKey = useMemo(() => {
|
|
||||||
if (!headerMetrics || headerMetrics.length === 0) return '';
|
|
||||||
try {
|
|
||||||
return headerMetrics.map(m => `${m.label}:${String(m.value)}`).join('|');
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}, [headerMetrics]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Skip if metrics haven't actually changed
|
|
||||||
if (metricsKey === prevMetricsRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update metrics if we have new values
|
|
||||||
// HeaderMetricsContext will automatically merge these with credit balance
|
|
||||||
if (metricsKey) {
|
|
||||||
setMetrics(headerMetrics);
|
|
||||||
hasSetMetricsRef.current = true;
|
|
||||||
prevMetricsRef.current = metricsKey;
|
|
||||||
} else if (hasSetMetricsRef.current) {
|
|
||||||
// Clear page metrics (credit balance will be preserved by HeaderMetricsContext)
|
|
||||||
setMetrics([]);
|
|
||||||
hasSetMetricsRef.current = false;
|
|
||||||
prevMetricsRef.current = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup: clear page metrics when component unmounts (credit balance preserved)
|
|
||||||
return () => {
|
|
||||||
if (hasSetMetricsRef.current) {
|
|
||||||
setMetrics([]);
|
|
||||||
hasSetMetricsRef.current = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [metricsKey]); // Only depend on the stable key
|
|
||||||
|
|
||||||
// Check if any filters are applied
|
// Check if any filters are applied
|
||||||
// When using renderFilters, check filterValues directly; otherwise check filters prop
|
// When using renderFilters, check filterValues directly; otherwise check filters prop
|
||||||
const hasActiveFilters = (renderFilters || filters.length > 0) && Object.values(filterValues).some((value) => {
|
const hasActiveFilters = (renderFilters || filters.length > 0) && Object.values(filterValues).some((value) => {
|
||||||
@@ -577,124 +544,193 @@ export default function TablePageTemplate({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{/* Bulk Actions and Action Buttons Row - Fixed height container */}
|
{/* Bulk Actions and Action Buttons Row - Reduced padding */}
|
||||||
<div className="flex items-center justify-between min-h-[65px] mb-4 mt-[10px]">
|
<div className="flex flex-col gap-2.5">
|
||||||
{/* Left side - Primary Action, Bulk Actions, and Filter Toggle */}
|
{/* Main row with buttons and metrics */}
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex items-center justify-between py-1.5">
|
||||||
{/* Primary Workflow Action Button - Only enabled with selection */}
|
{/* Left side - Create Button, Primary Action, Bulk Actions, Filter Toggle */}
|
||||||
{primaryAction && (
|
<div className="flex gap-1.5 items-center">
|
||||||
<Button
|
{/* Create Button - moved to leftmost position */}
|
||||||
size="md"
|
{onCreate && (
|
||||||
onClick={primaryAction.onClick}
|
<Button
|
||||||
disabled={selectedIds.length === 0}
|
variant="primary"
|
||||||
variant="primary"
|
size="sm"
|
||||||
tone={primaryAction.variant === 'success' ? 'success' : primaryAction.variant === 'warning' ? 'warning' : 'brand'}
|
endIcon={onCreateIcon}
|
||||||
startIcon={primaryAction.icon}
|
onClick={onCreate}
|
||||||
className={selectedIds.length === 0 ? "opacity-50 cursor-not-allowed" : ""}
|
>
|
||||||
>
|
{createLabel}
|
||||||
{primaryAction.label}
|
</Button>
|
||||||
{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">
|
|
||||||
|
{/* Primary Workflow Action Button - Only shown with selection */}
|
||||||
|
{primaryAction && selectedIds.length > 0 && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={primaryAction.onClick}
|
||||||
|
variant="primary"
|
||||||
|
tone={primaryAction.variant === 'success' ? 'success' : primaryAction.variant === 'warning' ? 'warning' : 'brand'}
|
||||||
|
startIcon={primaryAction.icon}
|
||||||
|
>
|
||||||
|
{primaryAction.label}
|
||||||
|
<span className="ml-1.5 inline-flex items-center justify-center px-1.5 py-0.5 text-xs font-medium rounded-full bg-white/20 text-white">
|
||||||
{selectedIds.length}
|
{selectedIds.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)}
|
|
||||||
|
{/* Bulk Actions - Only shown with selection */}
|
||||||
{/* Bulk Actions - Single button if only one action, dropdown if multiple */}
|
{bulkActions.length > 0 && selectedIds.length > 0 && (
|
||||||
{bulkActions.length > 0 && (
|
<div className="inline-block">
|
||||||
<div className="inline-block">
|
{bulkActions.length === 1 ? (
|
||||||
{bulkActions.length === 1 ? (
|
<Button
|
||||||
// Single button for single action
|
size="sm"
|
||||||
<Button
|
onClick={() => handleBulkActionClick(bulkActions[0].key, selectedIds)}
|
||||||
size="md"
|
variant="primary"
|
||||||
onClick={() => {
|
tone={bulkActions[0].variant === 'success' ? 'success' : bulkActions[0].variant === 'danger' ? 'danger' : 'brand'}
|
||||||
if (selectedIds.length > 0) {
|
startIcon={bulkActions[0].icon}
|
||||||
handleBulkActionClick(bulkActions[0].key, selectedIds);
|
>
|
||||||
}
|
{bulkActions[0].label}
|
||||||
}}
|
<span className="ml-1.5 inline-flex items-center justify-center px-1.5 py-0.5 text-xs font-medium rounded-full bg-white/20 text-white">
|
||||||
disabled={selectedIds.length === 0}
|
|
||||||
variant="primary"
|
|
||||||
tone={bulkActions[0].variant === 'success' ? 'success' : bulkActions[0].variant === 'danger' ? 'danger' : 'brand'}
|
|
||||||
startIcon={bulkActions[0].icon}
|
|
||||||
className={selectedIds.length === 0 ? "opacity-50 cursor-not-allowed" : ""}
|
|
||||||
>
|
|
||||||
{bulkActions[0].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}
|
{selectedIds.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
</Button>
|
||||||
</Button>
|
) : (
|
||||||
) : (
|
<>
|
||||||
// Dropdown for multiple actions
|
<Button
|
||||||
<>
|
ref={bulkActionsButtonRef}
|
||||||
<Button
|
size="sm"
|
||||||
ref={bulkActionsButtonRef}
|
onClick={() => setIsBulkActionsDropdownOpen(!isBulkActionsDropdownOpen)}
|
||||||
size="md"
|
className="dropdown-toggle"
|
||||||
onClick={() => selectedIds.length > 0 && setIsBulkActionsDropdownOpen(!isBulkActionsDropdownOpen)}
|
endIcon={<ChevronDownIcon className="w-3.5 h-3.5" />}
|
||||||
disabled={selectedIds.length === 0}
|
>
|
||||||
className={`dropdown-toggle ${selectedIds.length === 0 ? "opacity-50 cursor-not-allowed" : ""}`}
|
Bulk Actions
|
||||||
endIcon={<ChevronDownIcon className="w-4 h-4" />}
|
<span className="ml-1.5 inline-flex items-center justify-center px-1.5 py-0.5 text-xs font-medium rounded-full bg-blue-100 text-blue-800 dark:bg-blue-500/20 dark:text-blue-300">
|
||||||
>
|
|
||||||
Bulk Actions
|
|
||||||
{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-blue-100 text-blue-800 dark:bg-blue-500/20 dark:text-blue-300">
|
|
||||||
{selectedIds.length}
|
{selectedIds.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
</Button>
|
||||||
</Button>
|
<Dropdown
|
||||||
<Dropdown
|
isOpen={isBulkActionsDropdownOpen}
|
||||||
isOpen={isBulkActionsDropdownOpen && selectedIds.length > 0}
|
onClose={() => setIsBulkActionsDropdownOpen(false)}
|
||||||
onClose={() => setIsBulkActionsDropdownOpen(false)}
|
anchorRef={bulkActionsButtonRef as React.RefObject<HTMLElement>}
|
||||||
anchorRef={bulkActionsButtonRef as React.RefObject<HTMLElement>}
|
placement="bottom-left"
|
||||||
placement="bottom-left"
|
className="w-48 p-2"
|
||||||
className="w-48 p-2"
|
>
|
||||||
>
|
{bulkActions.map((action, index) => {
|
||||||
{bulkActions.map((action, index) => {
|
const isDelete = action.key === 'delete';
|
||||||
const isDelete = action.key === 'delete';
|
const showDivider = isDelete && index > 0;
|
||||||
const showDivider = isDelete && index > 0;
|
return (
|
||||||
return (
|
<React.Fragment key={action.key}>
|
||||||
<React.Fragment key={action.key}>
|
{showDivider && <div className="my-2 border-t border-gray-200 dark:border-gray-800"></div>}
|
||||||
{showDivider && <div className="my-2 border-t border-gray-200 dark:border-gray-800"></div>}
|
<DropdownItem
|
||||||
<DropdownItem
|
onItemClick={() => {
|
||||||
onItemClick={() => {
|
handleBulkActionClick(action.key, selectedIds);
|
||||||
handleBulkActionClick(action.key, selectedIds);
|
}}
|
||||||
}}
|
className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${
|
||||||
className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${
|
isDelete
|
||||||
isDelete
|
? "text-error-500 hover:bg-error-50 hover:text-error-600 dark:text-error-400 dark:hover:bg-error-500/15 dark:hover:text-error-300"
|
||||||
? "text-error-500 hover:bg-error-50 hover:text-error-600 dark:text-error-400 dark:hover:bg-error-500/15 dark:hover:text-error-300"
|
: "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||||
: "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
{action.icon && <span className="flex-shrink-0 w-5 h-5">{action.icon}</span>}
|
||||||
{action.icon && <span className="flex-shrink-0 w-5 h-5">{action.icon}</span>}
|
<span className="text-left">{action.label}</span>
|
||||||
<span className="text-left">{action.label}</span>
|
</DropdownItem>
|
||||||
</DropdownItem>
|
</React.Fragment>
|
||||||
</React.Fragment>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</Dropdown>
|
||||||
</Dropdown>
|
</>
|
||||||
</>
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filter Toggle Button */}
|
||||||
|
{(renderFilters || filters.length > 0) && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
|
startIcon={<FunnelIcon className="w-3.5 h-3.5" />}
|
||||||
|
>
|
||||||
|
{showFilters ? 'Hide Filters' : 'Show Filters'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side - Inline Metrics and Action Buttons */}
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
{/* Inline Metrics - with 25% larger label fonts */}
|
||||||
|
{headerMetrics && headerMetrics.length > 0 && (
|
||||||
|
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-50 dark:bg-gray-800/30 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
{headerMetrics.map((metric, index) => {
|
||||||
|
const metricElement = (
|
||||||
|
<div className={`flex items-center gap-2 ${metric.tooltip ? 'cursor-help' : ''}`}>
|
||||||
|
<div className={`w-1 h-5 rounded-full ${
|
||||||
|
metric.accentColor === 'blue' ? 'bg-blue-500' :
|
||||||
|
metric.accentColor === 'green' ? 'bg-green-500' :
|
||||||
|
metric.accentColor === 'amber' ? 'bg-amber-500' :
|
||||||
|
metric.accentColor === 'purple' ? 'bg-purple-500' : 'bg-gray-500'
|
||||||
|
}`}></div>
|
||||||
|
<span className="text-[13px] font-medium text-gray-500 dark:text-gray-400 flex items-center gap-1">
|
||||||
|
{metric.label}
|
||||||
|
{metric.tooltip && (
|
||||||
|
<InfoIcon className="w-3 h-3 text-gray-400 dark:text-gray-500" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-bold text-gray-900 dark:text-white">
|
||||||
|
{typeof metric.value === 'number' ? formatMetricValue(metric.value) : metric.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{metric.tooltip ? (
|
||||||
|
<Tooltip text={metric.tooltip} placement="bottom">
|
||||||
|
{metricElement}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
metricElement
|
||||||
|
)}
|
||||||
|
{index < headerMetrics.length - 1 && (
|
||||||
|
<div className="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-1"></div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-1.5 items-center">
|
||||||
|
{/* Custom Actions */}
|
||||||
|
{customActions}
|
||||||
|
{onExportCSV && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
endIcon={onExportIcon}
|
||||||
|
onClick={onExportCSV}
|
||||||
|
>
|
||||||
|
Export CSV
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onImport && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
endIcon={onImportIcon}
|
||||||
|
onClick={onImport}
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Filter Toggle Button */}
|
|
||||||
{(renderFilters || filters.length > 0) && (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="md"
|
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
|
||||||
startIcon={<FunnelIcon className="w-4 h-4" />}
|
|
||||||
>
|
|
||||||
{showFilters ? 'Hide Filters' : 'Show Filters'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center - Filters (when toggled on) */}
|
{/* Filters Row - Below action buttons, centered */}
|
||||||
{showFilters && (renderFilters || filters.length > 0) && (
|
{showFilters && (renderFilters || filters.length > 0) && (
|
||||||
<div className="flex-1 mx-4">
|
<div className="flex justify-center py-1.5">
|
||||||
<div className="bg-gray-50 dark:bg-gray-800/30 rounded-lg px-4 py-2 border border-gray-200 dark:border-gray-700">
|
<div className="bg-gray-50 dark:bg-gray-800/30 rounded-lg px-4 py-2 border border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex gap-3 items-center flex-wrap">
|
<div className="flex gap-3 items-center flex-wrap">
|
||||||
{renderFilters ? (
|
{renderFilters ? (
|
||||||
@@ -702,7 +738,6 @@ export default function TablePageTemplate({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{filters.map((filter) => {
|
{filters.map((filter) => {
|
||||||
// Handle custom render filters (for complex filters like volume range)
|
|
||||||
if (filter.type === 'custom' && (filter as any).customRender) {
|
if (filter.type === 'custom' && (filter as any).customRender) {
|
||||||
return <React.Fragment key={filter.key}>{(filter as any).customRender()}</React.Fragment>;
|
return <React.Fragment key={filter.key}>{(filter as any).customRender()}</React.Fragment>;
|
||||||
}
|
}
|
||||||
@@ -717,7 +752,7 @@ export default function TablePageTemplate({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onFilterChange?.(filter.key, e.target.value);
|
onFilterChange?.(filter.key, e.target.value);
|
||||||
}}
|
}}
|
||||||
className="w-full sm:flex-1 h-9"
|
className="w-full sm:flex-1 h-8"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (filter.type === 'select') {
|
} else if (filter.type === 'select') {
|
||||||
@@ -729,7 +764,6 @@ export default function TablePageTemplate({
|
|||||||
placeholder={filter.label}
|
placeholder={filter.label}
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
// Ensure we pass the value even if it's an empty string
|
|
||||||
const newValue = value === null || value === undefined ? '' : String(value);
|
const newValue = value === null || value === undefined ? '' : String(value);
|
||||||
onFilterChange?.(filter.key, newValue);
|
onFilterChange?.(filter.key, newValue);
|
||||||
}}
|
}}
|
||||||
@@ -754,52 +788,6 @@ export default function TablePageTemplate({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Right side - Status Explainer and Action Buttons */}
|
|
||||||
<div className="flex gap-4 items-start">
|
|
||||||
{/* Status Explainer */}
|
|
||||||
{statusExplainer && (
|
|
||||||
<div className="text-right">
|
|
||||||
{statusExplainer}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
{/* Custom Actions */}
|
|
||||||
{customActions}
|
|
||||||
{onExportCSV && (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="md"
|
|
||||||
endIcon={onExportIcon}
|
|
||||||
onClick={onExportCSV}
|
|
||||||
>
|
|
||||||
Export CSV
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{onImport && (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="md"
|
|
||||||
endIcon={onImportIcon}
|
|
||||||
onClick={onImport}
|
|
||||||
>
|
|
||||||
Import
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{onCreate && (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
endIcon={onCreateIcon}
|
|
||||||
onClick={onCreate}
|
|
||||||
>
|
|
||||||
{createLabel}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Data Table - Match Keywords.tsx exact styling */}
|
{/* Data Table - Match Keywords.tsx exact styling */}
|
||||||
|
|||||||
Reference in New Issue
Block a user