|
| 1 | +'use client' |
| 2 | + |
| 3 | +import type React from 'react' |
| 4 | +import { Circle } from 'lucide-react' |
| 5 | +import { Checkbox } from '@/components/emcn' |
| 6 | +import { Loader } from '@/components/emcn/icons/loader' |
| 7 | +import { cn } from '@/lib/core/utils/cn' |
| 8 | +import type { RowExecutionMetadata } from '@/lib/table' |
| 9 | +import type { SaveReason } from '../../../types' |
| 10 | +import { storageToDisplay } from '../../../utils' |
| 11 | +import type { DisplayColumn } from '../types' |
| 12 | +import { InlineEditor } from './inline-editors' |
| 13 | + |
| 14 | +interface CellContentProps { |
| 15 | + value: unknown |
| 16 | + exec?: RowExecutionMetadata |
| 17 | + column: DisplayColumn |
| 18 | + isEditing: boolean |
| 19 | + initialCharacter?: string | null |
| 20 | + onSave: (value: unknown, reason: SaveReason) => void |
| 21 | + onCancel: () => void |
| 22 | + workflowNameById?: Record<string, string> |
| 23 | +} |
| 24 | + |
| 25 | +/** |
| 26 | + * Renders the visible content of a single cell. Workflow-output cells follow |
| 27 | + * a status-state-machine (block error / value / running / waiting / cancelled |
| 28 | + * / dash); plain cells render the typed value. When `isEditing` is true the |
| 29 | + * `InlineEditor` overlay sits on top of the static content. |
| 30 | + */ |
| 31 | +export function CellContent({ |
| 32 | + value, |
| 33 | + exec, |
| 34 | + column, |
| 35 | + isEditing, |
| 36 | + initialCharacter, |
| 37 | + onSave, |
| 38 | + onCancel, |
| 39 | +}: CellContentProps) { |
| 40 | + const isNull = value === null || value === undefined |
| 41 | + |
| 42 | + let displayContent: React.ReactNode = null |
| 43 | + if (column.workflowGroupId) { |
| 44 | + const blockId = column.outputBlockId |
| 45 | + const blockError = blockId ? exec?.blockErrors?.[blockId] : undefined |
| 46 | + const blockRunning = blockId ? (exec?.runningBlockIds?.includes(blockId) ?? false) : false |
| 47 | + const hasValue = !isNull |
| 48 | + const valueText = |
| 49 | + typeof value === 'string' |
| 50 | + ? value |
| 51 | + : value === null || value === undefined |
| 52 | + ? '' |
| 53 | + : JSON.stringify(value) |
| 54 | + |
| 55 | + // Once any block in the group has reported an error, downstream cells |
| 56 | + // that haven't started won't run on this attempt — collapse them to dash |
| 57 | + // instead of leaving a stale "Waiting" spinner if the cell task didn't |
| 58 | + // reach a clean terminal state. |
| 59 | + const groupHasBlockErrors = !!(exec?.blockErrors && Object.keys(exec.blockErrors).length > 0) |
| 60 | + if (blockError) { |
| 61 | + displayContent = ( |
| 62 | + <span |
| 63 | + className='block overflow-clip text-ellipsis text-[var(--text-error)]' |
| 64 | + title={blockError} |
| 65 | + > |
| 66 | + Error |
| 67 | + </span> |
| 68 | + ) |
| 69 | + } else if (hasValue) { |
| 70 | + displayContent = ( |
| 71 | + <span className='block overflow-clip text-ellipsis text-[var(--text-primary)]'> |
| 72 | + {valueText} |
| 73 | + </span> |
| 74 | + ) |
| 75 | + } else if ( |
| 76 | + (exec?.status === 'running' || exec?.status === 'pending') && |
| 77 | + !(groupHasBlockErrors && !blockRunning) |
| 78 | + ) { |
| 79 | + // Motion only when this cell's own block is in flight. Pending and |
| 80 | + // upstream-blocked Waiting render as static dots — the moving spinner |
| 81 | + // is reserved for "right now, actually running". |
| 82 | + if (blockRunning) { |
| 83 | + displayContent = ( |
| 84 | + <div className='flex min-h-[20px] min-w-0 items-center gap-1.5'> |
| 85 | + <Loader animate className='h-3.5 w-3.5 shrink-0 text-[var(--text-tertiary)]' /> |
| 86 | + <span className='min-w-0 overflow-clip text-ellipsis whitespace-nowrap text-[var(--text-tertiary)]'> |
| 87 | + Running |
| 88 | + </span> |
| 89 | + </div> |
| 90 | + ) |
| 91 | + } else { |
| 92 | + const label = exec.status === 'pending' ? 'Pending' : 'Waiting' |
| 93 | + displayContent = ( |
| 94 | + <div className='flex min-h-[20px] min-w-0 items-center gap-1.5'> |
| 95 | + <Circle className='h-[10px] w-[10px] shrink-0 text-[var(--text-tertiary)]' /> |
| 96 | + <span className='min-w-0 overflow-clip text-ellipsis whitespace-nowrap text-[var(--text-tertiary)]'> |
| 97 | + {label} |
| 98 | + </span> |
| 99 | + </div> |
| 100 | + ) |
| 101 | + } |
| 102 | + } else if (exec?.status === 'cancelled') { |
| 103 | + displayContent = ( |
| 104 | + <span className='block overflow-clip text-ellipsis text-[var(--text-tertiary)]'> |
| 105 | + Cancelled |
| 106 | + </span> |
| 107 | + ) |
| 108 | + } else { |
| 109 | + displayContent = <span className='text-[var(--text-tertiary)]'>—</span> |
| 110 | + } |
| 111 | + return displayContent |
| 112 | + } |
| 113 | + if (column.type === 'boolean') { |
| 114 | + displayContent = ( |
| 115 | + <div |
| 116 | + className={cn('flex min-h-[20px] items-center justify-center', isEditing && 'invisible')} |
| 117 | + > |
| 118 | + <Checkbox size='sm' checked={Boolean(value)} className='pointer-events-none' /> |
| 119 | + </div> |
| 120 | + ) |
| 121 | + } else if (!isNull && column.type === 'json') { |
| 122 | + displayContent = ( |
| 123 | + <span |
| 124 | + className={cn( |
| 125 | + 'block overflow-clip text-ellipsis text-[var(--text-primary)]', |
| 126 | + isEditing && 'invisible' |
| 127 | + )} |
| 128 | + > |
| 129 | + {JSON.stringify(value)} |
| 130 | + </span> |
| 131 | + ) |
| 132 | + } else if (!isNull && column.type === 'date') { |
| 133 | + displayContent = ( |
| 134 | + <span className={cn('text-[var(--text-primary)]', isEditing && 'invisible')}> |
| 135 | + {storageToDisplay(String(value))} |
| 136 | + </span> |
| 137 | + ) |
| 138 | + } else if (!isNull) { |
| 139 | + displayContent = ( |
| 140 | + <span |
| 141 | + className={cn( |
| 142 | + 'block overflow-clip text-ellipsis text-[var(--text-primary)]', |
| 143 | + isEditing && 'invisible' |
| 144 | + )} |
| 145 | + > |
| 146 | + {String(value)} |
| 147 | + </span> |
| 148 | + ) |
| 149 | + } |
| 150 | + |
| 151 | + return ( |
| 152 | + <> |
| 153 | + {isEditing && ( |
| 154 | + <div className='absolute inset-0 z-10 flex items-start px-0'> |
| 155 | + <InlineEditor |
| 156 | + value={value} |
| 157 | + column={column} |
| 158 | + initialCharacter={initialCharacter ?? undefined} |
| 159 | + onSave={onSave} |
| 160 | + onCancel={onCancel} |
| 161 | + /> |
| 162 | + </div> |
| 163 | + )} |
| 164 | + {displayContent} |
| 165 | + </> |
| 166 | + ) |
| 167 | +} |
0 commit comments