fix: Add comprehensive null guards to all dashboard components
This commit fixes React Error #306 (text content mismatch) by ensuring no component ever tries to render undefined values as text content. Changes made across all dashboard components: 1. **Component Entry Guards**: - Added early returns for null/undefined data - Changed `if (loading)` to `if (loading || !data)` pattern - Components now show loading skeleton when data is undefined 2. **Property Access Guards**: - Added `|| fallback` operators for all rendered text - Added optional chaining for nested property access - Added conditional rendering for optional/array fields 3. **Specific Component Fixes**: **HealthStatusCard.tsx**: - Added early return for !healthStatus - Added fallback for status: `healthStatus.status || 'green'` - Added conditional rendering for nextScheduledRun - Added conditional rendering for checklistItems array - Added text fallback: `item.text || ''` **InsightsGrid.tsx**: - Added null check: `if (!insights || !insights.savings || ...)` - Added fallbacks: `insights.savings?.label || 'Savings'` - Added fallbacks for all 4 insight cards **OrchestrationSummaryCard.tsx**: - Added early return for !summary - Added fallback: `summary.message || ''` - Added fallback: `summary.runNumber || 0` - Added array checks: `summary.purchaseOrdersSummary && ...` - Added fallbacks for PO items: `po.supplierName || 'Unknown Supplier'` - Added fallbacks for batch items: `batch.quantity || 0` - Added safe date parsing: `batch.readyByTime ? new Date(...) : 'TBD'` **ProductionTimelineCard.tsx**: - Added early return for !timeline - Added array check: `!timeline.timeline || timeline.timeline.length === 0` - Added fallbacks for all item properties: - `item.statusIcon || '🔵'` - `item.productName || 'Product'` - `item.quantity || 0` - `item.unit || 'units'` - `item.batchNumber || 'N/A'` - `item.priority || 'NORMAL'` - `item.statusText || 'Status'` - `item.progress || 0` **ActionQueueCard.tsx**: - Added early return for !actionQueue - Added safe config lookup with fallback to normal urgency - Added array check: `!actionQueue.actions || ...` - Added property fallbacks: - `action.title || 'Action Required'` - `action.urgency || 'normal'` - `action.subtitle || ''` - `action.estimatedTimeMinutes || 5` - Added array guard: `(action.actions || []).map(...)` - Added fallbacks for counts: `actionQueue.totalActions || 0` **useReasoningTranslation.ts**: - Enhanced formatPOAction with explicit fallbacks - Enhanced formatBatchAction with explicit fallbacks - All translation functions return strings with || operators **DashboardPage.tsx**: - Removed conditional rendering operators (&&) - Components now always render and handle their own null states - Pattern: `<Component data={data} loading={loading} />` 4. **Testing Strategy**: - All components gracefully handle undefined data - Loading states shown when data is undefined - No undefined values can be rendered as text content - Multiple fallback layers ensure strings are always returned This comprehensive fix addresses the root cause of React Error #306 by making all components bulletproof against undefined values.
This commit is contained in:
@@ -65,7 +65,7 @@ function ActionItemCard({
|
||||
onModify?: (id: string) => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const config = urgencyConfig[action.urgency];
|
||||
const config = urgencyConfig[action.urgency as keyof typeof urgencyConfig] || urgencyConfig.normal;
|
||||
const UrgencyIcon = config.icon;
|
||||
const { formatPOAction } = useReasoningFormatter();
|
||||
const { t } = useTranslation('reasoning');
|
||||
@@ -84,12 +84,12 @@ function ActionItemCard({
|
||||
<UrgencyIcon className={`w-6 h-6 flex-shrink-0 ${config.badge.split(' ')[1]}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<h3 className="font-bold text-lg">{action.title}</h3>
|
||||
<h3 className="font-bold text-lg">{action.title || 'Action Required'}</h3>
|
||||
<span className={`${config.badge} px-2 py-1 rounded text-xs font-semibold uppercase flex-shrink-0`}>
|
||||
{action.urgency}
|
||||
{action.urgency || 'normal'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{action.subtitle}</p>
|
||||
<p className="text-sm text-gray-600">{action.subtitle || ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -139,13 +139,13 @@ function ActionItemCard({
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500 mb-4">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>
|
||||
{t('jtbd.action_queue.estimated_time')}: {action.estimatedTimeMinutes} min
|
||||
{t('jtbd.action_queue.estimated_time')}: {action.estimatedTimeMinutes || 5} min
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{action.actions.map((button, index) => {
|
||||
{(action.actions || []).map((button, index) => {
|
||||
const buttonStyles = {
|
||||
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
|
||||
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
|
||||
@@ -188,10 +188,9 @@ export function ActionQueueCard({
|
||||
onModify,
|
||||
}: ActionQueueCardProps) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const displayedActions = showAll ? actionQueue.actions : actionQueue.actions.slice(0, 3);
|
||||
const { t } = useTranslation('reasoning');
|
||||
|
||||
if (loading) {
|
||||
if (loading || !actionQueue) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
@@ -203,7 +202,7 @@ export function ActionQueueCard({
|
||||
);
|
||||
}
|
||||
|
||||
if (actionQueue.actions.length === 0) {
|
||||
if (!actionQueue.actions || actionQueue.actions.length === 0) {
|
||||
return (
|
||||
<div className="bg-green-50 border-2 border-green-200 rounded-xl p-8 text-center">
|
||||
<CheckCircle2 className="w-16 h-16 text-green-600 mx-auto mb-4" />
|
||||
@@ -215,29 +214,31 @@ export function ActionQueueCard({
|
||||
);
|
||||
}
|
||||
|
||||
const displayedActions = showAll ? actionQueue.actions : actionQueue.actions.slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900">{t('jtbd.action_queue.title')}</h2>
|
||||
{actionQueue.totalActions > 3 && (
|
||||
{(actionQueue.totalActions || 0) > 3 && (
|
||||
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||
{actionQueue.totalActions} {t('jtbd.action_queue.total')}
|
||||
{actionQueue.totalActions || 0} {t('jtbd.action_queue.total')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Summary Badges */}
|
||||
{(actionQueue.criticalCount > 0 || actionQueue.importantCount > 0) && (
|
||||
{((actionQueue.criticalCount || 0) > 0 || (actionQueue.importantCount || 0) > 0) && (
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{actionQueue.criticalCount > 0 && (
|
||||
{(actionQueue.criticalCount || 0) > 0 && (
|
||||
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||
{actionQueue.criticalCount} {t('jtbd.action_queue.critical')}
|
||||
{actionQueue.criticalCount || 0} {t('jtbd.action_queue.critical')}
|
||||
</span>
|
||||
)}
|
||||
{actionQueue.importantCount > 0 && (
|
||||
{(actionQueue.importantCount || 0) > 0 && (
|
||||
<span className="bg-amber-100 text-amber-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||
{actionQueue.importantCount} {t('jtbd.action_queue.important')}
|
||||
{actionQueue.importantCount || 0} {t('jtbd.action_queue.important')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -257,14 +258,14 @@ export function ActionQueueCard({
|
||||
</div>
|
||||
|
||||
{/* Show More/Less */}
|
||||
{actionQueue.totalActions > 3 && (
|
||||
{(actionQueue.totalActions || 0) > 3 && (
|
||||
<button
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
className="w-full mt-4 py-3 bg-gray-100 hover:bg-gray-200 rounded-lg font-semibold text-gray-700 transition-colors duration-200"
|
||||
>
|
||||
{showAll
|
||||
? t('jtbd.action_queue.show_less')
|
||||
: t('jtbd.action_queue.show_more', { count: actionQueue.totalActions - 3 })}
|
||||
: t('jtbd.action_queue.show_more', { count: (actionQueue.totalActions || 3) - 3 })}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user