visual-notebook / frontend /src /components /TableCellView.tsx
Sid-WC121
bugfix2 + update
8ac8526
Raw History Blame Contribute Delete
8.69 kB
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { useRows, useEditCell } from '../api/hooks';
import { useSessionStore } from '../store/session';
import { formatCellValue, typeBadgeClass, typeLabel } from '../lib/format';
interface Props {
cellId: string;
config: Record<string, any>;
}
export function TableCellView({ cellId, config }: Props) {
const { columns } = useSessionStore();
const { fetch: fetchRows, loading, error } = useRows();
const { edit: editCell, loading: editing } = useEditCell();
const [rows, setRows] = useState<Record<string, any>[]>([]);
const [colNames, setColNames] = useState<string[]>([]);
const [offset, setOffset] = useState(0);
const [total, setTotal] = useState(0);
const [editMode, setEditMode] = useState(false);
const limit = 50;
// Inline editing state
const [editingCell, setEditingCell] = useState<{ rowIdx: number; colName: string } | null>(null);
const [editValue, setEditValue] = useState<string>('');
const editInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
loadData();
}, [offset]);
useEffect(() => {
if (editingCell && editInputRef.current) {
editInputRef.current.focus();
editInputRef.current.select();
}
}, [editingCell]);
const loadData = async () => {
const data = await fetchRows(offset, limit);
if (data) {
setRows(data.rows);
setColNames(data.columns);
setTotal(data.total);
}
};
const handleDoubleClick = useCallback((rowIdx: number, colName: string, currentValue: any) => {
if (!editMode) return;
setEditingCell({ rowIdx, colName });
setEditValue(currentValue == null ? '' : String(currentValue));
}, [editMode]);
const handleEditSubmit = useCallback(async () => {
if (!editingCell) return;
const { rowIdx, colName } = editingCell;
try {
const result = await editCell(rowIdx, colName, editValue);
if (result) {
// Refresh data after successful edit
await loadData();
}
} catch {
// Error handled by hook
}
setEditingCell(null);
setEditValue('');
}, [editingCell, editValue, editCell]);
const handleEditKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleEditSubmit();
} else if (e.key === 'Escape') {
setEditingCell(null);
setEditValue('');
}
}, [handleEditSubmit]);
const totalPages = Math.ceil(total / limit);
const currentPage = Math.floor(offset / limit) + 1;
return (
<div>
{loading && rows.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<div className="animate-spin w-6 h-6 border-2 border-brand-500 border-t-transparent rounded-full mx-auto mb-2" />
Loading data...
</div>
) : error ? (
<div className="text-red-500 text-sm">{error}</div>
) : (
<>
{/* Table toolbar with edit mode toggle */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<button
onClick={() => {
setEditMode(!editMode);
if (editMode) {
setEditingCell(null);
setEditValue('');
}
}}
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium transition-colors ${
editMode
? 'bg-brand-100 text-brand-700 ring-1 ring-brand-300'
: 'bg-gray-100 text-gray-500 hover:bg-gray-200 hover:text-gray-700'
}`}
title={editMode ? 'Disable editing' : 'Enable editing'}
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
{editMode ? 'Editing' : 'Edit'}
</button>
{editMode && (
<span className="edit-mode-hint">
<svg className="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Double-click a cell to edit
</span>
)}
</div>
</div>
<div className="overflow-x-auto max-h-[500px] overflow-y-auto rounded-lg border border-gray-200">
<table className="data-table w-full">
<thead>
<tr>
<th className="w-10 text-center">#</th>
{colNames.map((name) => {
const col = columns.find((c) => c.name === name);
return (
<th key={name}>
<div className="flex items-center gap-1">
<span className={`badge ${typeBadgeClass(col?.column_type || 'unknown')} text-[9px]`}>
{typeLabel(col?.column_type || 'unknown')}
</span>
<span className="truncate max-w-[150px]" title={name}>{name}</span>
</div>
</th>
);
})}
</tr>
</thead>
<tbody>
{rows.map((row, i) => {
const globalRowIdx = offset + i;
return (
<tr key={i}>
<td className="text-center text-gray-400 text-xs font-mono">{globalRowIdx + 1}</td>
{colNames.map((name) => {
const isEditing = editingCell?.rowIdx === globalRowIdx && editingCell?.colName === name;
const cellValue = row[name];
if (isEditing) {
return (
<td key={name} className="p-0 editing-active">
<input
ref={editInputRef}
type="text"
className="w-full px-3 py-2 text-sm border-2 border-brand-500 outline-none bg-white"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={handleEditSubmit}
onKeyDown={handleEditKeyDown}
disabled={editing}
/>
</td>
);
}
return (
<td
key={name}
className={editMode ? 'editable' : ''}
onDoubleClick={() => handleDoubleClick(globalRowIdx, name, cellValue)}
title={editMode ? 'Double-click to edit' : undefined}
>
{formatCellValue(cellValue)}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
<div className="flex items-center justify-between mt-3">
<span className="text-xs text-gray-500">
{offset + 1}-{Math.min(offset + limit, total)} of {total.toLocaleString()} rows
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setOffset(Math.max(0, offset - limit))}
disabled={offset === 0}
className="px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-30"
>
Prev
</button>
<span className="text-xs text-gray-500 px-2">
Page {currentPage} / {totalPages}
</span>
<button
onClick={() => setOffset(offset + limit)}
disabled={offset + limit >= total}
className="px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-30"
>
Next
</button>
</div>
</div>
</>
)}
</div>
);
}