import { memo, useCallback, useMemo } from "react"; import { flushSync } from "react-dom"; import { Cell } from "@tanstack/react-table"; import { Popover, Tooltip } from "@mantine/core"; import { IconArrowsDiagonal } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { useAtom, useAtomValue, useSetAtom, type PrimitiveAtom } from "jotai"; import { selectAtom } from "jotai/utils"; import { IBaseRow, EditingCell, FocusedCell } from "@/ee/base/types/base.types"; import { editingCellAtomFamily, focusedCellAtomFamily, activeFormulaEditorAtomFamily, FormulaEditorTarget, } from "@/ee/base/atoms/base-atoms"; import { FormulaPropertyEditor } from "@/ee/base/components/formula/formula-property-editor"; import { isSystemPropertyType, getDescriptor, } from "@/ee/base/property-types/property-type.registry"; import { cellValuesEqual } from "@/ee/base/components/cells/cell-value-equal"; import { computeNextCell } from "@/ee/base/utils/grid-cell-nav"; import { useBaseEditable } from "@/ee/base/context/base-editable"; import { useRowExpand } from "@/ee/base/context/row-expand"; import { RowNumberCell } from "./row-number-cell"; import classes from "@/ee/base/styles/grid.module.css"; type GridCellProps = { cell: Cell; rowIndex: number; colIndex?: number; onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void; pageId: string; }; export const GridCell = memo(function GridCell({ cell, rowIndex, colIndex, onCellUpdate, pageId, }: GridCellProps) { const property = cell.column.columnDef.meta?.property; const isRowNumber = cell.column.id === "__row_number"; const isPinned = cell.column.getIsPinned(); const pinOffset = isPinned ? cell.column.getStart("left") : undefined; const [editingCell, setEditingCell] = useAtom(editingCellAtomFamily(pageId)) as unknown as [EditingCell, (val: EditingCell) => void]; const [activeFormulaEditor, setActiveFormulaEditor] = useAtom( activeFormulaEditorAtomFamily(pageId), ) as unknown as [FormulaEditorTarget, (val: FormulaEditorTarget) => void]; const setFocusedCell = useSetAtom(focusedCellAtomFamily(pageId) as PrimitiveAtom); const isFocused = useAtomValue( useMemo( () => selectAtom( focusedCellAtomFamily(pageId), (fc) => fc?.rowId === cell.row.id && fc?.propertyId === property?.id, ), [pageId, cell.row.id, property?.id], ), ); const { t } = useTranslation(); const editable = useBaseEditable(); const readOnly = !editable; const onExpandRow = useRowExpand(); const rowId = cell.row.id; const isEditing = editingCell?.rowId === rowId && editingCell?.propertyId === property?.id && (editable || property?.type === "file"); const handleEdit = useCallback(() => { if (!property || isRowNumber) return; if (property.type === "checkbox") return; if (readOnly) { // Read-only: only the file cell opens (a download-only popover) so // attachments stay reachable. if (property.type === "file") { flushSync(() => setEditingCell({ rowId, propertyId: property.id })); } return; } if (property.type === "formula") { setActiveFormulaEditor({ propertyId: property.id, rowId }); return; } if (isSystemPropertyType(property.type)) return; flushSync(() => setEditingCell({ rowId, propertyId: property.id })); }, [property, isRowNumber, rowId, readOnly, setEditingCell, setActiveFormulaEditor]); const handleMouseDown = useCallback( (e: React.MouseEvent) => { if (!property || e.button !== 0) return; setFocusedCell({ rowId, propertyId: property.id }); }, [property, rowId, setFocusedCell], ); const handleClick = useCallback( (e: React.MouseEvent) => { if (!property) return; setFocusedCell({ rowId, propertyId: property.id }); (e.currentTarget.closest('[role="grid"]') as HTMLElement | null)?.focus({ preventScroll: true, }); }, [property, rowId, setFocusedCell], ); const cellReadOnly = property ? readOnly || isSystemPropertyType(property.type) : false; const closeFormulaEditor = useCallback( () => setActiveFormulaEditor(null), [setActiveFormulaEditor], ); const handleValueChange = useCallback( (value: unknown) => { if (!property) return; if (!cellValuesEqual(value, cell.getValue())) { onCellUpdate(rowId, property.id, value); } }, [property, rowId, cell, onCellUpdate], ); const handleCommit = useCallback( (value: unknown) => { handleValueChange(value); setEditingCell(null); }, [handleValueChange, setEditingCell], ); const handleCancel = useCallback(() => { setEditingCell(null); }, [setEditingCell]); const handleTabNavigate = useCallback( (shiftKey: boolean) => { if (!property) return; const tableInstance = cell.getContext().table; const colIds = tableInstance .getVisibleLeafColumns() .filter((c) => c.id !== "__row_number") .map((c) => c.id); const rowIds = tableInstance.getRowModel().rows.map((r) => r.id); const next = computeNextCell( rowIds, colIds, { rowId, propertyId: property.id }, 0, shiftKey ? -1 : 1, true, ); if (next) { setEditingCell(next); setFocusedCell(next); } }, [cell, rowId, property, setEditingCell, setFocusedCell], ); if (isRowNumber) { return ( ); } if (!property) return null; const CellComponent = getDescriptor(property.type)?.cellComponent; if (!CellComponent) return null; const value = cell.getValue(); const cellInner = (
{property.isPrimary && onExpandRow && !isEditing && ( )}
); if (property.type !== "formula") return cellInner; const formulaEditorOpen = activeFormulaEditor?.propertyId === property.id && activeFormulaEditor?.rowId === rowId; return ( { if (!o) closeFormulaEditor(); }} position="bottom-start" width={460} shadow="md" withinPortal closeOnClickOutside closeOnEscape={false} trapFocus > {cellInner} e.stopPropagation()} onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Escape") { e.preventDefault(); closeFormulaEditor(); } }} style={{ maxWidth: "calc(100vw - 32px)" }} > {formulaEditorOpen && ( )} ); }, gridCellPropsEqual); // Cell instances are re-created whenever the table data identity changes; // compare by coordinates + value so unchanged cells skip re-rendering. function gridCellPropsEqual(prev: GridCellProps, next: GridCellProps) { if ( prev.rowIndex !== next.rowIndex || prev.colIndex !== next.colIndex || prev.pageId !== next.pageId || prev.onCellUpdate !== next.onCellUpdate ) { return false; } if (prev.cell === next.cell) return true; return ( prev.cell.row.id === next.cell.row.id && prev.cell.column.id === next.cell.column.id && prev.cell.column.columnDef.meta?.property === next.cell.column.columnDef.meta?.property && cellValuesEqual(prev.cell.getValue(), next.cell.getValue()) ); }