Unexpected token
TablePageTemplate.tsx:
This commit is contained in:
@@ -28,12 +28,10 @@ import Input from '../components/form/input/InputField';
|
||||
import SelectDropdown from '../components/form/SelectDropdown';
|
||||
import { Dropdown } from '../components/ui/dropdown/Dropdown';
|
||||
import { DropdownItem } from '../components/ui/dropdown/DropdownItem';
|
||||
import Alert from '../components/ui/alert/Alert';
|
||||
import AlertModal from '../components/ui/alert/AlertModal';
|
||||
import { ChevronDownIcon, MoreDotIcon, PlusIcon } from '../icons';
|
||||
import { useHeaderMetrics } from '../context/HeaderMetricsContext';
|
||||
import { useToast } from '../components/ui/toast/ToastContainer';
|
||||
import { pageNotifications } from '../config/pages/notifications.config';
|
||||
import { getDeleteModalConfig } from '../config/pages/delete-modal.config';
|
||||
import { getBulkActionModalConfig } from '../config/pages/bulk-action-modal.config';
|
||||
import { getTableActionsConfig } from '../config/pages/table-actions.config';
|
||||
@@ -102,7 +100,7 @@ interface TablePageTemplateProps {
|
||||
onCreate?: () => void;
|
||||
createLabel?: string;
|
||||
onCreateIcon?: ReactNode; // Icon for create button
|
||||
onExport?: () => void;
|
||||
onExportCSV?: () => void; // CSV export button handler
|
||||
onExportIcon?: ReactNode; // Icon for export button
|
||||
onImport?: () => void;
|
||||
onImportIcon?: ReactNode; // Icon for import button
|
||||
@@ -146,7 +144,7 @@ export default function TablePageTemplate({
|
||||
subtitle,
|
||||
columns,
|
||||
data,
|
||||
loading = false,
|
||||
loading: _loading = false, // Unused - component uses showContent for loading state
|
||||
showContent = true,
|
||||
filters = [],
|
||||
filterValues = {},
|
||||
@@ -157,7 +155,7 @@ export default function TablePageTemplate({
|
||||
onCreate,
|
||||
createLabel = '+ Add',
|
||||
onCreateIcon,
|
||||
onExport,
|
||||
onExportCSV,
|
||||
onExportIcon,
|
||||
onImport,
|
||||
onImportIcon,
|
||||
@@ -172,17 +170,17 @@ export default function TablePageTemplate({
|
||||
onBulkUpdateStatus,
|
||||
onBulkAction,
|
||||
onRowAction,
|
||||
onExport,
|
||||
getItemDisplayName = (row: any) => row.name || row.keyword || row.title || String(row.id),
|
||||
className = '',
|
||||
}: TablePageTemplateProps) {
|
||||
const location = useLocation();
|
||||
const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false);
|
||||
const [openRowActions, setOpenRowActions] = useState<Map<string | number, boolean>>(new Map());
|
||||
const rowActionButtonRefs = React.useRef<Map<string | number, React.RefObject<HTMLButtonElement>>>(new Map());
|
||||
const rowActionButtonRefs = React.useRef<Map<string | number, React.RefObject<HTMLButtonElement | null>>>(new Map());
|
||||
const bulkActionsButtonRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Get notification config for current page
|
||||
const notificationConfig = pageNotifications[location.pathname];
|
||||
const deleteModalConfig = getDeleteModalConfig(location.pathname);
|
||||
const bulkActionModalConfig = getBulkActionModalConfig(location.pathname);
|
||||
const tableActionsConfig = getTableActionsConfig(location.pathname);
|
||||
@@ -303,7 +301,7 @@ export default function TablePageTemplate({
|
||||
} else if (actionKey === 'delete' && onDelete && deleteModalConfig) {
|
||||
handleDeleteClick(row);
|
||||
} else if (actionKey === 'export' && onExport) {
|
||||
onExport(row);
|
||||
await onExport(row);
|
||||
} else if (onRowAction) {
|
||||
// For custom row actions, use onRowAction with full row object
|
||||
onRowAction(actionKey, row).catch((error: any) => {
|
||||
@@ -649,7 +647,7 @@ export default function TablePageTemplate({
|
||||
<Dropdown
|
||||
isOpen={isBulkActionsDropdownOpen && selectedIds.length > 0}
|
||||
onClose={() => setIsBulkActionsDropdownOpen(false)}
|
||||
anchorRef={bulkActionsButtonRef}
|
||||
anchorRef={bulkActionsButtonRef as React.RefObject<HTMLElement>}
|
||||
placement="bottom-left"
|
||||
className="w-48 p-2"
|
||||
>
|
||||
@@ -683,12 +681,12 @@ export default function TablePageTemplate({
|
||||
|
||||
{/* Action Buttons - Right aligned */}
|
||||
<div className="flex gap-2">
|
||||
{onExport && (
|
||||
{onExportCSV && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="md"
|
||||
endIcon={onExportIcon}
|
||||
onClick={onExport}
|
||||
onClick={onExportCSV}
|
||||
>
|
||||
Export CSV
|
||||
</Button>
|
||||
@@ -747,16 +745,24 @@ export default function TablePageTemplate({
|
||||
key={column.key}
|
||||
isHeader
|
||||
className={`px-5 py-3 font-medium text-gray-500 text-${column.align || 'start'} text-theme-xs dark:text-gray-400 ${column.sortable ? 'cursor-pointer hover:text-gray-700 dark:hover:text-gray-300' : ''} ${isLastColumn && rowActions.length > 0 ? 'pr-16' : ''}`}
|
||||
onClick={() => column.sortable && handleSort(column)}
|
||||
>
|
||||
{column.sortable ? (
|
||||
<div onClick={() => handleSort(column)} className="flex items-center">
|
||||
{column.label}
|
||||
{getSortIcon(column)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{column.label}
|
||||
{getSortIcon(column)}
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="divide-y divide-gray-100 dark:divide-white/[0.05] igny8-table-body" style={{ position: 'relative' }}>
|
||||
<TableBody className="divide-y divide-gray-100 dark:divide-white/[0.05] igny8-table-body">
|
||||
{!showContent ? (
|
||||
// Loading Skeleton Rows - Always show 10 rows to match page size
|
||||
Array.from({ length: 10 }).map((_, index) => (
|
||||
@@ -769,7 +775,6 @@ export default function TablePageTemplate({
|
||||
))
|
||||
) : data.length === 0 ? null : (
|
||||
data.map((row, index) => {
|
||||
const rowId = row.id?.toString() || index.toString();
|
||||
// Use consistent key for expandedRows (number or index)
|
||||
const rowKey = row.id || index;
|
||||
const isRowExpanded = expandedRows.has(rowKey);
|
||||
@@ -811,7 +816,6 @@ export default function TablePageTemplate({
|
||||
<React.Fragment key={row.id || index}>
|
||||
<TableRow
|
||||
className={`igny8-data-row ${isRowAdded ? 'bg-blue-50 dark:bg-blue-500/10' : ''}`}
|
||||
style={{ animationDelay: `${index * 0.03}s` }}
|
||||
>
|
||||
{selection && (
|
||||
<TableCell className="px-5 py-4 text-start">
|
||||
@@ -830,7 +834,8 @@ export default function TablePageTemplate({
|
||||
// Get or create ref for this row's actions button
|
||||
if (isLastColumn && rowActions.length > 0) {
|
||||
if (!rowActionButtonRefs.current.has(rowId)) {
|
||||
rowActionButtonRefs.current.set(rowId, React.createRef<HTMLButtonElement>());
|
||||
const ref = React.createRef<HTMLButtonElement>();
|
||||
rowActionButtonRefs.current.set(rowId, ref);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -838,7 +843,6 @@ export default function TablePageTemplate({
|
||||
<TableCell
|
||||
key={column.key}
|
||||
className={`px-5 py-4 text-${column.align || 'start'} text-gray-800 dark:text-white/90 ${isLastColumn && rowActions.length > 0 ? 'relative pr-16' : ''}`}
|
||||
style={isLastColumn && rowActions.length > 0 ? { position: 'relative', overflow: 'visible' } : undefined}
|
||||
>
|
||||
<div className={`flex items-center ${column.toggleable && hasToggleContent ? 'justify-between w-full' : ''} gap-2`}>
|
||||
<div className="flex-1">
|
||||
@@ -849,14 +853,15 @@ export default function TablePageTemplate({
|
||||
)}
|
||||
</div>
|
||||
{column.toggleable && hasToggleContent && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<ToggleButton
|
||||
isExpanded={isRowExpanded}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick={() => {
|
||||
handleToggle(!isRowExpanded, rowKey);
|
||||
}}
|
||||
hasContent={hasToggleContent}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -901,7 +906,8 @@ export default function TablePageTemplate({
|
||||
}
|
||||
|
||||
// Multiple actions - use dropdown
|
||||
const buttonRef = rowActionButtonRefs.current.get(rowId)!;
|
||||
const buttonRef = rowActionButtonRefs.current.get(rowId);
|
||||
if (!buttonRef) return null;
|
||||
const isOpen = openRowActions.get(rowId) || false;
|
||||
|
||||
return (
|
||||
@@ -936,11 +942,11 @@ export default function TablePageTemplate({
|
||||
return newMap;
|
||||
});
|
||||
}}
|
||||
anchorRef={buttonRef}
|
||||
anchorRef={buttonRef as React.RefObject<HTMLElement>}
|
||||
placement="right"
|
||||
className="w-48 p-2"
|
||||
>
|
||||
{rowActions.map((action, actionIndex) => {
|
||||
{rowActions.map((action) => {
|
||||
const isEdit = action.key === 'edit';
|
||||
const isDelete = action.key === 'delete';
|
||||
const isExport = action.key === 'export';
|
||||
@@ -949,21 +955,21 @@ export default function TablePageTemplate({
|
||||
const getIconWithColor = () => {
|
||||
if (!action.icon) return null;
|
||||
const iconElement = action.icon as React.ReactElement;
|
||||
const existingClassName = iconElement.props?.className || "";
|
||||
const existingClassName = (iconElement.props as any)?.className || "";
|
||||
const baseSize = existingClassName.includes("w-") ? "" : "w-5 h-5 ";
|
||||
|
||||
if (isEdit) {
|
||||
return React.cloneElement(iconElement, {
|
||||
className: `${baseSize}text-blue-light-500 ${existingClassName}`.trim()
|
||||
});
|
||||
} as any);
|
||||
} else if (isDelete) {
|
||||
return React.cloneElement(iconElement, {
|
||||
className: `${baseSize}text-error-500 ${existingClassName}`.trim()
|
||||
});
|
||||
} as any);
|
||||
} else if (isExport) {
|
||||
return React.cloneElement(iconElement, {
|
||||
className: `${baseSize}text-gray-600 dark:text-gray-400 ${existingClassName}`.trim()
|
||||
});
|
||||
} as any);
|
||||
}
|
||||
return action.icon;
|
||||
};
|
||||
@@ -1090,276 +1096,6 @@ export default function TablePageTemplate({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Request Logs Section - DEPRECATED: Now using backend console logging */}
|
||||
{/* <AIRequestLogsSection /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// AI Request Logs Component - DEPRECATED: Now using backend console logging
|
||||
// All AI logging is now handled by backend console logging (ConsoleStepTracker)
|
||||
/*
|
||||
function AIRequestLogsSection() {
|
||||
const { logs, clearLogs } = useAIRequestLogsStore();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (logs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'text-green-600 dark:text-green-400';
|
||||
case 'error':
|
||||
return 'text-red-600 dark:text-red-400';
|
||||
case 'pending':
|
||||
return 'text-yellow-600 dark:text-yellow-400';
|
||||
default:
|
||||
return 'text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
|
||||
case 'error':
|
||||
return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
|
||||
case 'pending':
|
||||
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6 border-t border-gray-200 dark:border-gray-800 pt-6">
|
||||
{isExpanded && (
|
||||
<div className="flex justify-center">
|
||||
<div className="w-1/2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800/50">
|
||||
{/* Header inside panel */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
AI Debug Logs
|
||||
</h3>
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400">
|
||||
{logs.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-xs"
|
||||
>
|
||||
{isExpanded ? 'Collapse' : 'Expand'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={clearLogs}
|
||||
className="text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="p-4 max-h-96 overflow-y-auto">
|
||||
<div className="space-y-4">
|
||||
{logs.map((log) => (
|
||||
<React.Fragment key={log.id}>
|
||||
<div
|
||||
className="p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-semibold text-gray-700 dark:text-gray-300 font-mono-custom">
|
||||
{log.function}
|
||||
</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(log.status)}`}>
|
||||
{log.status.toUpperCase()}
|
||||
</span>
|
||||
{log.duration && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{log.duration}ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatTime(log.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 mb-3 font-mono-custom">
|
||||
{log.endpoint}
|
||||
</div>
|
||||
|
||||
{/* Request/Response Details (Collapsible) */}
|
||||
<details className="mt-3">
|
||||
<summary className="text-xs text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-300">
|
||||
Request/Response Details
|
||||
</summary>
|
||||
<div className="mt-2 space-y-2">
|
||||
<details className="text-xs">
|
||||
<summary className="text-gray-500 dark:text-gray-400 cursor-pointer">Request</summary>
|
||||
<div className="mt-1 p-2 bg-gray-50 dark:bg-gray-800 rounded text-xs overflow-x-auto font-mono-custom">
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{JSON.stringify(log.request.body || log.request.params || {}, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</details>
|
||||
{log.response && (
|
||||
<details className="text-xs">
|
||||
<summary className="text-gray-500 dark:text-gray-400 cursor-pointer">
|
||||
Response ({log.response.status})
|
||||
{log.response.errorType && (
|
||||
<span className="ml-2 px-1.5 py-0.5 rounded bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 text-[10px] font-semibold">
|
||||
{log.response.errorType}
|
||||
</span>
|
||||
)}
|
||||
</summary>
|
||||
<div className="mt-1 p-2 bg-gray-50 dark:bg-gray-800 rounded text-xs overflow-x-auto font-mono-custom">
|
||||
{log.response.error ? (
|
||||
<div className="text-red-600 dark:text-red-400">
|
||||
{log.response.error}
|
||||
</div>
|
||||
) : log.response.data ? (
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{JSON.stringify(log.response.data, null, 2)}
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{/* AI Debug Steps */}
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-900">
|
||||
<h4 className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||||
AI Debug Steps ({log.requestSteps.length + log.responseSteps.length} total steps)
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{(() => {
|
||||
// Combine and sort all steps by stepNumber
|
||||
const allSteps = [
|
||||
...log.requestSteps.map((step, idx) => ({ ...step, type: 'request', originalIdx: idx })),
|
||||
...log.responseSteps.map((step, idx) => ({ ...step, type: 'response', originalIdx: idx }))
|
||||
].sort((a, b) => (a.stepNumber || 0) - (b.stepNumber || 0));
|
||||
|
||||
return allSteps.length > 0 ? (
|
||||
allSteps.map((step, idx) => (
|
||||
<div
|
||||
key={`${step.type}-${step.originalIdx}-${idx}`}
|
||||
className="p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs font-mono text-gray-500 dark:text-gray-400">
|
||||
{step.stepNumber}.
|
||||
</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(step.status)}`}>
|
||||
{step.status.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||
{step.type === 'request' ? 'Request' : 'Response'}
|
||||
</span>
|
||||
{step.duration && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{step.duration}ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1">
|
||||
{step.stepName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 font-mono mb-1">
|
||||
{step.functionName}
|
||||
</div>
|
||||
{step.message && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
{step.message}
|
||||
</div>
|
||||
)}
|
||||
{step.error && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mt-2">
|
||||
Error: {step.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 italic">
|
||||
No steps logged yet
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isExpanded && logs.length > 0 && (
|
||||
<div className="flex justify-center">
|
||||
<div className="w-1/2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800/50">
|
||||
{/* Header when collapsed */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
AI Debug Logs
|
||||
</h3>
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400">
|
||||
{logs.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-xs"
|
||||
>
|
||||
{isExpanded ? 'Collapse' : 'Expand'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={clearLogs}
|
||||
className="text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Click "Expand" to view {logs.length} AI request log{logs.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user