diff --git a/public/r/data-grid.json b/public/r/data-grid.json index fc564ad76..ec9c50850 100644 --- a/public/r/data-grid.json +++ b/public/r/data-grid.json @@ -47,7 +47,7 @@ }, { "path": "src/components/data-grid/data-grid-cell-variants.tsx", - "content": "\"use client\";\n\nimport { Check, Upload, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\nimport { DataGridCellWrapper } from \"@/components/data-grid/data-grid-cell-wrapper\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverAnchor,\n PopoverContent,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { useBadgeOverflow } from \"@/hooks/use-badge-overflow\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport {\n formatDateForDisplay,\n formatDateToString,\n formatFileSize,\n getCellKey,\n getFileIcon,\n getLineCount,\n getUrlHref,\n parseLocalDate,\n} from \"@/lib/data-grid\";\nimport { cn } from \"@/lib/utils\";\nimport type { DataGridCellProps, FileCellData } from \"@/types/data-grid\";\n\nexport function ShortTextCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isEditing,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue);\n const cellRef = React.useRef(null);\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue);\n if (cellRef.current && !isEditing) {\n cellRef.current.textContent = initialValue;\n }\n }\n\n const onBlur = React.useCallback(() => {\n // Read the current value directly from the DOM to avoid stale state\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: currentValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, readOnly]);\n\n const onInput = React.useCallback(\n (event: React.FormEvent) => {\n const currentValue = event.currentTarget.textContent ?? \"\";\n setValue(currentValue);\n },\n [],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Enter\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue,\n });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue,\n });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n cellRef.current?.blur();\n }\n } else if (\n isFocused &&\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n // Handle typing to pre-fill the value when editing starts\n setValue(event.key);\n\n queueMicrotask(() => {\n if (cellRef.current && cellRef.current.contentEditable === \"true\") {\n cellRef.current.textContent = event.key;\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n });\n }\n },\n [isEditing, isFocused, initialValue, tableMeta, rowIndex, columnId],\n );\n\n React.useEffect(() => {\n if (isEditing && cellRef.current) {\n cellRef.current.focus();\n\n if (!cellRef.current.textContent && value) {\n cellRef.current.textContent = value;\n }\n\n if (cellRef.current.textContent) {\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n }\n }, [isEditing, value]);\n\n const displayValue = !isEditing ? (value ?? \"\") : \"\";\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n \n {displayValue}\n \n \n );\n}\n\nexport function LongTextCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const textareaRef = React.useRef(null);\n const containerRef = React.useRef(null);\n const pendingCharRef = React.useRef(null);\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n }\n\n const debouncedSave = useDebouncedCallback((newValue: string) => {\n if (!readOnly) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\n }\n }, 300);\n\n const onSave = React.useCallback(() => {\n // Immediately save any pending changes and close the popover\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, value, initialValue, rowIndex, columnId, readOnly]);\n\n const onCancel = React.useCallback(() => {\n // Restore the original value\n setValue(initialValue ?? \"\");\n if (!readOnly) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: initialValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, initialValue, rowIndex, columnId, readOnly]);\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n // Immediately save any pending changes when closing\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, value, initialValue, rowIndex, columnId, readOnly],\n );\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n if (textareaRef.current) {\n textareaRef.current.focus();\n const length = textareaRef.current.value.length;\n textareaRef.current.setSelectionRange(length, length);\n\n // Insert pending character using execCommand so it's part of undo history\n // Use requestAnimationFrame to ensure focus has fully settled\n if (pendingCharRef.current) {\n const char = pendingCharRef.current;\n pendingCharRef.current = null;\n requestAnimationFrame(() => {\n if (\n textareaRef.current &&\n document.activeElement === textareaRef.current\n ) {\n document.execCommand(\"insertText\", false, char);\n textareaRef.current.scrollTop = textareaRef.current.scrollHeight;\n }\n });\n } else {\n textareaRef.current.scrollTop = textareaRef.current.scrollHeight;\n }\n }\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n isFocused &&\n !isEditing &&\n !readOnly &&\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n // Store the character to be inserted after textarea focuses\n // This ensures it's part of the textarea's undo history\n pendingCharRef.current = event.key;\n }\n },\n [isFocused, isEditing, readOnly],\n );\n\n const onBlur = React.useCallback(() => {\n // Immediately save any pending changes on blur\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, value, initialValue, rowIndex, columnId, readOnly]);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const newValue = event.target.value;\n setValue(newValue);\n debouncedSave(newValue);\n },\n [debouncedSave],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n onCancel();\n } else if (event.key === \"Enter\" && (event.ctrlKey || event.metaKey)) {\n event.preventDefault();\n onSave();\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n // Save any pending changes\n if (value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n return;\n }\n // Stop propagation to prevent grid navigation\n event.stopPropagation();\n },\n [onSave, onCancel, value, initialValue, tableMeta, rowIndex, columnId],\n );\n\n return (\n \n \n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {value}\n \n \n \n \n \n \n );\n}\n\nexport function NumberCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as number;\n const [value, setValue] = React.useState(String(initialValue ?? \"\"));\n const inputRef = React.useRef(null);\n const containerRef = React.useRef(null);\n\n const cellOpts = cell.column.columnDef.meta?.cell;\n const numberCellOpts = cellOpts?.variant === \"number\" ? cellOpts : null;\n const min = numberCellOpts?.min;\n const max = numberCellOpts?.max;\n const step = numberCellOpts?.step;\n\n const prevIsEditingRef = React.useRef(isEditing);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(String(initialValue ?? \"\"));\n }\n\n const onBlur = React.useCallback(() => {\n const numValue = value === \"\" ? null : Number(value);\n if (!readOnly && numValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, value, readOnly]);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n setValue(event.target.value);\n },\n [],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Enter\") {\n event.preventDefault();\n const numValue = value === \"\" ? null : Number(value);\n if (numValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const numValue = value === \"\" ? null : Number(value);\n if (numValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(String(initialValue ?? \"\"));\n inputRef.current?.blur();\n }\n } else if (isFocused) {\n // Handle Backspace to start editing with empty value\n if (event.key === \"Backspace\") {\n setValue(\"\");\n } else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {\n // Handle typing to pre-fill the value when editing starts\n setValue(event.key);\n }\n }\n },\n [isEditing, isFocused, initialValue, tableMeta, rowIndex, columnId, value],\n );\n\n React.useEffect(() => {\n const wasEditing = prevIsEditingRef.current;\n prevIsEditingRef.current = isEditing;\n\n // Only focus when we start editing (transition from false to true)\n if (isEditing && !wasEditing && inputRef.current) {\n inputRef.current.focus();\n }\n }, [isEditing]);\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n ) : (\n {value}\n )}\n \n );\n}\n\nexport function UrlCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isEditing,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const cellRef = React.useRef(null);\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n if (cellRef.current && !isEditing) {\n cellRef.current.textContent = initialValue ?? \"\";\n }\n }\n\n const onBlur = React.useCallback(() => {\n const currentValue = cellRef.current?.textContent?.trim() ?? \"\";\n\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, readOnly]);\n\n const onInput = React.useCallback(\n (event: React.FormEvent) => {\n const currentValue = event.currentTarget.textContent ?? \"\";\n setValue(currentValue);\n },\n [],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Enter\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent?.trim() ?? \"\";\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent?.trim() ?? \"\";\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue ?? \"\");\n cellRef.current?.blur();\n }\n } else if (\n isFocused &&\n !readOnly &&\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n // Handle typing to pre-fill the value when editing starts\n setValue(event.key);\n\n queueMicrotask(() => {\n if (cellRef.current && cellRef.current.contentEditable === \"true\") {\n cellRef.current.textContent = event.key;\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n });\n }\n },\n [\n isEditing,\n isFocused,\n initialValue,\n tableMeta,\n rowIndex,\n columnId,\n readOnly,\n ],\n );\n\n const onLinkClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (isEditing) {\n event.preventDefault();\n return;\n }\n\n // Check if URL was rejected due to dangerous protocol\n const href = getUrlHref(value);\n if (!href) {\n event.preventDefault();\n toast.error(\"Invalid URL\", {\n description:\n \"URL contains a dangerous protocol (javascript:, data:, vbscript:, or file:)\",\n });\n return;\n }\n\n // Stop propagation to prevent grid from interfering with link navigation\n event.stopPropagation();\n },\n [isEditing, value],\n );\n\n React.useEffect(() => {\n if (isEditing && cellRef.current) {\n cellRef.current.focus();\n\n if (!cellRef.current.textContent && value) {\n cellRef.current.textContent = value;\n }\n\n if (cellRef.current.textContent) {\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n }\n }, [isEditing, value]);\n\n const displayValue = !isEditing ? (value ?? \"\") : \"\";\n const urlHref = displayValue ? getUrlHref(displayValue) : \"\";\n const isDangerousUrl = displayValue && !urlHref;\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {!isEditing && displayValue ? (\n \n \n {displayValue}\n \n \n ) : (\n \n {displayValue}\n \n )}\n \n );\n}\n\nexport function CheckboxCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: Omit, \"isEditing\">) {\n const initialValue = cell.getValue() as boolean;\n const [value, setValue] = React.useState(Boolean(initialValue));\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(Boolean(initialValue));\n }\n\n const onCheckedChange = React.useCallback(\n (checked: boolean) => {\n if (readOnly) return;\n setValue(checked);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: checked });\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n isFocused &&\n !readOnly &&\n (event.key === \" \" || event.key === \"Enter\")\n ) {\n event.preventDefault();\n event.stopPropagation();\n onCheckedChange(!value);\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isFocused, value, onCheckedChange, tableMeta, readOnly],\n );\n\n const onWrapperClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (isFocused && !readOnly) {\n event.preventDefault();\n event.stopPropagation();\n onCheckedChange(!value);\n }\n },\n [isFocused, value, onCheckedChange, readOnly],\n );\n\n const onCheckboxClick = React.useCallback((event: React.MouseEvent) => {\n event.stopPropagation();\n }, []);\n\n const onCheckboxMouseDown = React.useCallback(\n (event: React.MouseEvent) => {\n event.stopPropagation();\n },\n [],\n );\n\n const onCheckboxDoubleClick = React.useCallback(\n (event: React.MouseEvent) => {\n event.stopPropagation();\n },\n [],\n );\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={false}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n className=\"flex size-full justify-center\"\n onClick={onWrapperClick}\n onKeyDown={onWrapperKeyDown}\n >\n \n \n );\n}\n\nexport function SelectCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue);\n const containerRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const options = cellOpts?.variant === \"select\" ? cellOpts.options : [];\n const optionByValue = React.useMemo(\n () => new Map(options.map((option) => [option.value, option])),\n [options],\n );\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue);\n }\n\n const onValueChange = React.useCallback(\n (newValue: string) => {\n if (readOnly) return;\n setValue(newValue);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\n tableMeta?.onCellEditingStop?.();\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, initialValue, tableMeta],\n );\n\n const displayLabel = optionByValue.get(value)?.label ?? value;\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n \n {displayLabel ? (\n \n \n \n ) : (\n \n )}\n \n \n {options.map((option) => (\n \n {option.label}\n \n ))}\n \n \n ) : displayLabel ? (\n \n {displayLabel}\n \n ) : null}\n \n );\n}\n\nexport function MultiSelectCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const cellValue = React.useMemo(() => {\n const value = cell.getValue() as string[];\n return value ?? [];\n }, [cell]);\n\n const cellKey = getCellKey(rowIndex, columnId);\n const prevCellKeyRef = React.useRef(cellKey);\n\n const [selectedValues, setSelectedValues] =\n React.useState(cellValue);\n const [searchValue, setSearchValue] = React.useState(\"\");\n const containerRef = React.useRef(null);\n const inputRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const options = cellOpts?.variant === \"multi-select\" ? cellOpts.options : [];\n const optionByValue = React.useMemo(\n () => new Map(options.map((option) => [option.value, option])),\n [options],\n );\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const prevCellValueRef = React.useRef(cellValue);\n if (cellValue !== prevCellValueRef.current) {\n prevCellValueRef.current = cellValue;\n setSelectedValues(cellValue);\n }\n\n if (prevCellKeyRef.current !== cellKey) {\n prevCellKeyRef.current = cellKey;\n setSearchValue(\"\");\n }\n\n const onValueChange = React.useCallback(\n (value: string) => {\n if (readOnly) return;\n let newValues: string[] = [];\n setSelectedValues((curr) => {\n newValues = curr.includes(value)\n ? curr.filter((v) => v !== value)\n : [...curr, value];\n return newValues;\n });\n queueMicrotask(() => {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n inputRef.current?.focus();\n });\n setSearchValue(\"\");\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const removeValue = React.useCallback(\n (valueToRemove: string, event?: React.MouseEvent) => {\n if (readOnly) return;\n event?.stopPropagation();\n event?.preventDefault();\n let newValues: string[] = [];\n setSelectedValues((curr) => {\n newValues = curr.filter((v) => v !== valueToRemove);\n return newValues;\n });\n queueMicrotask(() => {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n inputRef.current?.focus();\n });\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const clearAll = React.useCallback(() => {\n if (readOnly) return;\n setSelectedValues([]);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: [] });\n queueMicrotask(() => inputRef.current?.focus());\n }, [tableMeta, rowIndex, columnId, readOnly]);\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n inputRef.current?.focus();\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && event.key === \"Escape\") {\n event.preventDefault();\n setSelectedValues(cellValue);\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, cellValue, tableMeta],\n );\n\n const onInputKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"Backspace\" && searchValue === \"\") {\n event.preventDefault();\n let newValues: string[] | null = null;\n setSelectedValues((curr) => {\n if (curr.length === 0) return curr;\n newValues = curr.slice(0, -1);\n return newValues;\n });\n queueMicrotask(() => {\n if (newValues !== null) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n }\n inputRef.current?.focus();\n });\n }\n if (event.key === \"Escape\") {\n event.stopPropagation();\n }\n },\n [searchValue, tableMeta, rowIndex, columnId],\n );\n\n const displayLabels = selectedValues\n .map((val) => optionByValue.get(val)?.label ?? val)\n .filter(Boolean);\n\n const selectedValuesSet = React.useMemo(\n () => new Set(selectedValues),\n [selectedValues],\n );\n\n const lineCount = getLineCount(rowHeight);\n\n const { visibleItems: visibleLabels, hiddenCount: hiddenBadgeCount } =\n useBadgeOverflow({\n items: displayLabels,\n getLabel: (label) => label,\n containerRef,\n lineCount,\n });\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n \n
\n \n \n \n
\n {selectedValues.map((value) => {\n const label = optionByValue.get(value)?.label ?? value;\n\n return (\n \n {label}\n removeValue(value, event)}\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n }}\n >\n \n \n \n );\n })}\n \n
\n \n No options found.\n \n {options.map((option) => {\n const isSelected = selectedValuesSet.has(option.value);\n\n return (\n onValueChange(option.value)}\n >\n \n \n
\n {option.label}\n \n );\n })}\n \n {selectedValues.length > 0 && (\n <>\n \n \n \n Clear all\n \n \n \n )}\n \n \n \n
\n ) : null}\n {displayLabels.length > 0 ? (\n
\n {visibleLabels.map((label, index) => (\n \n {label}\n \n ))}\n {hiddenBadgeCount > 0 && (\n \n +{hiddenBadgeCount}\n \n )}\n
\n ) : null}\n \n );\n}\n\nexport function DateCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n }\n\n // Parse date as local time to avoid timezone shifts\n const selectedDate = value ? (parseLocalDate(value) ?? undefined) : undefined;\n\n const onDateSelect = React.useCallback(\n (date: Date | undefined) => {\n if (!date || readOnly) return;\n\n // Format using local date components to avoid timezone issues\n const formattedDate = formatDateToString(date);\n setValue(formattedDate);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: formattedDate });\n tableMeta?.onCellEditingStop?.();\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, initialValue, tableMeta],\n );\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n \n \n \n {formatDateForDisplay(value)}\n \n \n {isEditing && (\n \n \n \n )}\n \n \n );\n}\n\nexport function FileCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const cellValue = React.useMemo(\n () => (cell.getValue() as FileCellData[]) ?? [],\n [cell],\n );\n\n const cellKey = getCellKey(rowIndex, columnId);\n const prevCellKeyRef = React.useRef(cellKey);\n\n const labelId = React.useId();\n const descriptionId = React.useId();\n\n const [files, setFiles] = React.useState(cellValue);\n const [uploadingFiles, setUploadingFiles] = React.useState>(\n new Set(),\n );\n const [deletingFiles, setDeletingFiles] = React.useState>(\n new Set(),\n );\n const [isDraggingOver, setIsDraggingOver] = React.useState(false);\n const [isDragging, setIsDragging] = React.useState(false);\n const [error, setError] = React.useState(null);\n\n const isUploading = uploadingFiles.size > 0;\n const isDeleting = deletingFiles.size > 0;\n const isPending = isUploading || isDeleting;\n const containerRef = React.useRef(null);\n const fileInputRef = React.useRef(null);\n const dropzoneRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const fileCellOpts = cellOpts?.variant === \"file\" ? cellOpts : null;\n const maxFileSize = fileCellOpts?.maxFileSize ?? 10 * 1024 * 1024;\n const maxFiles = fileCellOpts?.maxFiles ?? 10;\n const accept = fileCellOpts?.accept;\n const multiple = fileCellOpts?.multiple ?? false;\n\n const acceptedTypes = React.useMemo(\n () => (accept ? accept.split(\",\").map((t) => t.trim()) : null),\n [accept],\n );\n\n const prevCellValueRef = React.useRef(cellValue);\n if (cellValue !== prevCellValueRef.current) {\n prevCellValueRef.current = cellValue;\n for (const file of files) {\n if (file.url) {\n URL.revokeObjectURL(file.url);\n }\n }\n setFiles(cellValue);\n setError(null);\n }\n\n if (prevCellKeyRef.current !== cellKey) {\n prevCellKeyRef.current = cellKey;\n setError(null);\n }\n\n const validateFile = React.useCallback(\n (file: File): string | null => {\n if (maxFileSize && file.size > maxFileSize) {\n return `File size exceeds ${formatFileSize(maxFileSize)}`;\n }\n if (acceptedTypes) {\n const fileExtension = `.${file.name.split(\".\").pop()}`;\n const isAccepted = acceptedTypes.some((type) => {\n if (type.endsWith(\"/*\")) {\n const baseType = type.slice(0, -2);\n return file.type.startsWith(`${baseType}/`);\n }\n if (type.startsWith(\".\")) {\n return fileExtension.toLowerCase() === type.toLowerCase();\n }\n return file.type === type;\n });\n if (!isAccepted) {\n return \"File type not accepted\";\n }\n }\n return null;\n },\n [maxFileSize, acceptedTypes],\n );\n\n const addFiles = React.useCallback(\n async (newFiles: File[], skipUpload = false) => {\n if (readOnly || isPending) return;\n setError(null);\n\n if (maxFiles && files.length + newFiles.length > maxFiles) {\n const errorMessage = `Maximum ${maxFiles} files allowed`;\n setError(errorMessage);\n toast(errorMessage);\n setTimeout(() => {\n setError(null);\n }, 2000);\n return;\n }\n\n const rejectedFiles: Array<{ name: string; reason: string }> = [];\n const filesToValidate: File[] = [];\n\n for (const file of newFiles) {\n const validationError = validateFile(file);\n if (validationError) {\n rejectedFiles.push({ name: file.name, reason: validationError });\n continue;\n }\n filesToValidate.push(file);\n }\n\n if (rejectedFiles.length > 0) {\n const firstError = rejectedFiles[0];\n if (firstError) {\n setError(firstError.reason);\n\n const truncatedName =\n firstError.name.length > 20\n ? `${firstError.name.slice(0, 20)}...`\n : firstError.name;\n\n if (rejectedFiles.length === 1) {\n toast(firstError.reason, {\n description: `\"${truncatedName}\" has been rejected`,\n });\n } else {\n toast(firstError.reason, {\n description: `\"${truncatedName}\" and ${rejectedFiles.length - 1} more rejected`,\n });\n }\n\n setTimeout(() => {\n setError(null);\n }, 2000);\n }\n }\n\n if (filesToValidate.length > 0) {\n if (!skipUpload) {\n const tempFiles = filesToValidate.map((f) => ({\n id: crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: undefined,\n }));\n const filesWithTemp = [...files, ...tempFiles];\n setFiles(filesWithTemp);\n\n const uploadingIds = new Set(tempFiles.map((f) => f.id));\n setUploadingFiles(uploadingIds);\n\n let uploadedFiles: FileCellData[] = [];\n\n if (tableMeta?.onFilesUpload) {\n try {\n uploadedFiles = await tableMeta.onFilesUpload({\n files: filesToValidate,\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error\n ? error.message\n : `Failed to upload ${filesToValidate.length} file${filesToValidate.length !== 1 ? \"s\" : \"\"}`,\n );\n setFiles((prev) => prev.filter((f) => !uploadingIds.has(f.id)));\n setUploadingFiles(new Set());\n return;\n }\n } else {\n uploadedFiles = filesToValidate.map((f, i) => ({\n id: tempFiles[i]?.id ?? crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: URL.createObjectURL(f),\n }));\n }\n\n const finalFiles = filesWithTemp\n .map((f) => {\n if (uploadingIds.has(f.id)) {\n return uploadedFiles.find((uf) => uf.name === f.name) ?? f;\n }\n return f;\n })\n .filter((f) => f.url !== undefined);\n\n setFiles(finalFiles);\n setUploadingFiles(new Set());\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: finalFiles });\n } else {\n const newFilesData: FileCellData[] = filesToValidate.map((f) => ({\n id: crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: URL.createObjectURL(f),\n }));\n const updatedFiles = [...files, ...newFilesData];\n setFiles(updatedFiles);\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: updatedFiles,\n });\n }\n }\n },\n [\n files,\n maxFiles,\n validateFile,\n tableMeta,\n rowIndex,\n columnId,\n readOnly,\n isPending,\n ],\n );\n\n const removeFile = React.useCallback(\n async (fileId: string) => {\n if (readOnly || isPending) return;\n setError(null);\n\n const fileToRemove = files.find((f) => f.id === fileId);\n if (!fileToRemove) return;\n\n setDeletingFiles((prev) => new Set(prev).add(fileId));\n\n if (tableMeta?.onFilesDelete) {\n try {\n await tableMeta.onFilesDelete({\n fileIds: [fileId],\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error\n ? error.message\n : `Failed to delete ${fileToRemove.name}`,\n );\n setDeletingFiles((prev) => {\n const next = new Set(prev);\n next.delete(fileId);\n return next;\n });\n return;\n }\n }\n\n if (fileToRemove.url?.startsWith(\"blob:\")) {\n URL.revokeObjectURL(fileToRemove.url);\n }\n\n const updatedFiles = files.filter((f) => f.id !== fileId);\n setFiles(updatedFiles);\n setDeletingFiles((prev) => {\n const next = new Set(prev);\n next.delete(fileId);\n return next;\n });\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: updatedFiles });\n },\n [files, tableMeta, rowIndex, columnId, readOnly, isPending],\n );\n\n const clearAll = React.useCallback(async () => {\n if (readOnly || isPending) return;\n setError(null);\n\n const fileIds = files.map((f) => f.id);\n setDeletingFiles(new Set(fileIds));\n\n if (tableMeta?.onFilesDelete && files.length > 0) {\n try {\n await tableMeta.onFilesDelete({\n fileIds,\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error ? error.message : \"Failed to delete files\",\n );\n setDeletingFiles(new Set());\n return;\n }\n }\n\n for (const file of files) {\n if (file.url?.startsWith(\"blob:\")) {\n URL.revokeObjectURL(file.url);\n }\n }\n setFiles([]);\n setDeletingFiles(new Set());\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: [] });\n }, [files, tableMeta, rowIndex, columnId, readOnly, isPending]);\n\n const onCellDragEnter = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n if (event.dataTransfer.types.includes(\"Files\")) {\n setIsDraggingOver(true);\n }\n }, []);\n\n const onCellDragLeave = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const rect = event.currentTarget.getBoundingClientRect();\n const x = event.clientX;\n const y = event.clientY;\n\n if (\n x <= rect.left ||\n x >= rect.right ||\n y <= rect.top ||\n y >= rect.bottom\n ) {\n setIsDraggingOver(false);\n }\n }, []);\n\n const onCellDragOver = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n }, []);\n\n const onCellDrop = React.useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDraggingOver(false);\n\n const droppedFiles = Array.from(event.dataTransfer.files);\n if (droppedFiles.length > 0) {\n addFiles(droppedFiles, false);\n }\n },\n [addFiles],\n );\n\n const onDropzoneDragEnter = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDragging(true);\n }, []);\n\n const onDropzoneDragLeave = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const rect = event.currentTarget.getBoundingClientRect();\n const x = event.clientX;\n const y = event.clientY;\n\n if (\n x <= rect.left ||\n x >= rect.right ||\n y <= rect.top ||\n y >= rect.bottom\n ) {\n setIsDragging(false);\n }\n }, []);\n\n const onDropzoneDragOver = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n }, []);\n\n const onDropzoneDrop = React.useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDragging(false);\n\n const droppedFiles = Array.from(event.dataTransfer.files);\n addFiles(droppedFiles, false);\n },\n [addFiles],\n );\n\n const onDropzoneClick = React.useCallback(() => {\n fileInputRef.current?.click();\n }, []);\n\n const onDropzoneKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onDropzoneClick();\n }\n },\n [onDropzoneClick],\n );\n\n const onFileInputChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const selectedFiles = Array.from(event.target.files ?? []);\n addFiles(selectedFiles, false);\n event.target.value = \"\";\n },\n [addFiles],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n setError(null);\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n setError(null);\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onEscapeKeyDown: NonNullable<\n React.ComponentProps[\"onEscapeKeyDown\"]\n > = React.useCallback((event) => {\n // Prevent the escape key from propagating to the data grid's keyboard handler\n // which would call blurCell() and remove focus from the cell\n event.stopPropagation();\n }, []);\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n queueMicrotask(() => {\n dropzoneRef.current?.focus();\n });\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Escape\") {\n event.preventDefault();\n setFiles(cellValue);\n setError(null);\n tableMeta?.onCellEditingStop?.();\n } else if (event.key === \" \") {\n event.preventDefault();\n onDropzoneClick();\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n } else if (isFocused && event.key === \"Enter\") {\n event.preventDefault();\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [\n isEditing,\n isFocused,\n cellValue,\n tableMeta,\n onDropzoneClick,\n rowIndex,\n columnId,\n ],\n );\n\n React.useEffect(() => {\n return () => {\n for (const file of files) {\n if (file.url) {\n URL.revokeObjectURL(file.url);\n }\n }\n };\n }, [files]);\n\n const lineCount = getLineCount(rowHeight);\n\n const { visibleItems: visibleFiles, hiddenCount: hiddenFileCount } =\n useBadgeOverflow({\n items: files,\n getLabel: (file) => file.name,\n containerRef,\n lineCount,\n cacheKeyPrefix: \"file\",\n iconSize: 12,\n maxWidth: 100,\n });\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n className={cn({\n \"ring-1 ring-primary/80 ring-inset\": isDraggingOver,\n })}\n onDragEnter={onCellDragEnter}\n onDragLeave={onCellDragLeave}\n onDragOver={onCellDragOver}\n onDrop={onCellDrop}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n \n
\n \n \n
\n \n File upload\n \n \n \n
\n

\n {isDragging ? \"Drop files here\" : \"Drag files here\"}\n

\n

\n or click to browse\n

\n
\n

\n {maxFileSize\n ? `Max size: ${formatFileSize(maxFileSize)}${maxFiles ? ` • Max ${maxFiles} files` : \"\"}`\n : maxFiles\n ? `Max ${maxFiles} files`\n : \"Select files to upload\"}\n

\n
\n \n {files.length > 0 && (\n
\n
\n

\n {files.length} {files.length === 1 ? \"file\" : \"files\"}\n

\n \n Clear all\n \n
\n
\n {files.map((file) => {\n const FileIcon = getFileIcon(file.type);\n const isFileUploading = uploadingFiles.has(file.id);\n const isFileDeleting = deletingFiles.has(file.id);\n const isFilePending = isFileUploading || isFileDeleting;\n\n return (\n \n {FileIcon && (\n \n )}\n
\n

{file.name}

\n

\n {isFileUploading\n ? \"Uploading...\"\n : isFileDeleting\n ? \"Deleting...\"\n : formatFileSize(file.size)}\n

\n
\n removeFile(file.id)}\n disabled={isPending}\n >\n \n \n
\n );\n })}\n
\n
\n )}\n \n \n
\n ) : null}\n {isDraggingOver ? (\n
\n \n Drop files here\n
\n ) : files.length > 0 ? (\n
\n {visibleFiles.map((file) => {\n const isUploading = uploadingFiles.has(file.id);\n\n if (isUploading) {\n return (\n \n );\n }\n\n const FileIcon = getFileIcon(file.type);\n\n return (\n \n {FileIcon && }\n {file.name}\n \n );\n })}\n {hiddenFileCount > 0 && (\n \n +{hiddenFileCount}\n \n )}\n
\n ) : null}\n \n );\n}\n", + "content": "\"use client\";\n\nimport { Check, Upload, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\nimport { DataGridCellWrapper } from \"@/components/data-grid/data-grid-cell-wrapper\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverAnchor,\n PopoverContent,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { useBadgeOverflow } from \"@/hooks/use-badge-overflow\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport {\n formatDateForDisplay,\n formatDateToString,\n formatFileSize,\n getCellKey,\n getFileIcon,\n getLineCount,\n getUrlHref,\n parseLocalDate,\n} from \"@/lib/data-grid\";\nimport { cn } from \"@/lib/utils\";\nimport type { DataGridCellProps, FileCellData } from \"@/types/data-grid\";\n\nexport function ShortTextCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isEditing,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue);\n const cellRef = React.useRef(null);\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue);\n if (cellRef.current && !isEditing) {\n cellRef.current.textContent = initialValue;\n }\n }\n\n const onBlur = React.useCallback(() => {\n // Read the current value directly from the DOM to avoid stale state\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: currentValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, readOnly]);\n\n const onInput = React.useCallback(\n (event: React.FormEvent) => {\n const currentValue = event.currentTarget.textContent ?? \"\";\n setValue(currentValue);\n },\n [],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Enter\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue,\n });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue,\n });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n cellRef.current?.blur();\n }\n } else if (\n isFocused &&\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n // Handle typing to pre-fill the value when editing starts\n setValue(event.key);\n\n queueMicrotask(() => {\n if (cellRef.current && cellRef.current.contentEditable === \"true\") {\n cellRef.current.textContent = event.key;\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n });\n }\n },\n [isEditing, isFocused, initialValue, tableMeta, rowIndex, columnId],\n );\n\n React.useEffect(() => {\n if (isEditing && cellRef.current) {\n cellRef.current.focus();\n\n if (!cellRef.current.textContent && value) {\n cellRef.current.textContent = value;\n }\n\n if (cellRef.current.textContent) {\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n }\n }, [isEditing, value]);\n\n const displayValue = !isEditing ? (value ?? \"\") : \"\";\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n \n {displayValue}\n \n \n );\n}\n\nexport function LongTextCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const textareaRef = React.useRef(null);\n const containerRef = React.useRef(null);\n const pendingCharRef = React.useRef(null);\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n }\n\n const debouncedSave = useDebouncedCallback((newValue: string) => {\n if (!readOnly) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\n }\n }, 300);\n\n const onSave = React.useCallback(() => {\n // Immediately save any pending changes and close the popover\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, value, initialValue, rowIndex, columnId, readOnly]);\n\n const onCancel = React.useCallback(() => {\n // Restore the original value\n setValue(initialValue ?? \"\");\n if (!readOnly) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: initialValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, initialValue, rowIndex, columnId, readOnly]);\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n // Immediately save any pending changes when closing\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, value, initialValue, rowIndex, columnId, readOnly],\n );\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n if (textareaRef.current) {\n textareaRef.current.focus();\n const length = textareaRef.current.value.length;\n textareaRef.current.setSelectionRange(length, length);\n\n // Insert pending character using execCommand so it's part of undo history\n // Use requestAnimationFrame to ensure focus has fully settled\n if (pendingCharRef.current) {\n const char = pendingCharRef.current;\n pendingCharRef.current = null;\n requestAnimationFrame(() => {\n if (\n textareaRef.current &&\n document.activeElement === textareaRef.current\n ) {\n document.execCommand(\"insertText\", false, char);\n textareaRef.current.scrollTop = textareaRef.current.scrollHeight;\n }\n });\n } else {\n textareaRef.current.scrollTop = textareaRef.current.scrollHeight;\n }\n }\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n isFocused &&\n !isEditing &&\n !readOnly &&\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n // Store the character to be inserted after textarea focuses\n // This ensures it's part of the textarea's undo history\n pendingCharRef.current = event.key;\n }\n },\n [isFocused, isEditing, readOnly],\n );\n\n const onBlur = React.useCallback(() => {\n // Immediately save any pending changes on blur\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, value, initialValue, rowIndex, columnId, readOnly]);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const newValue = event.target.value;\n setValue(newValue);\n debouncedSave(newValue);\n },\n [debouncedSave],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n onCancel();\n } else if (event.key === \"Enter\" && (event.ctrlKey || event.metaKey)) {\n event.preventDefault();\n onSave();\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n // Save any pending changes\n if (value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n return;\n }\n // Stop propagation to prevent grid navigation\n event.stopPropagation();\n },\n [onSave, onCancel, value, initialValue, tableMeta, rowIndex, columnId],\n );\n\n return (\n \n \n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {value}\n \n \n \n \n \n \n );\n}\n\nexport function NumberCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as number;\n const [value, setValue] = React.useState(String(initialValue ?? \"\"));\n const inputRef = React.useRef(null);\n const containerRef = React.useRef(null);\n\n const cellOpts = cell.column.columnDef.meta?.cell;\n const numberCellOpts = cellOpts?.variant === \"number\" ? cellOpts : null;\n const min = numberCellOpts?.min;\n const max = numberCellOpts?.max;\n const step = numberCellOpts?.step;\n\n const prevIsEditingRef = React.useRef(isEditing);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(String(initialValue ?? \"\"));\n }\n\n const onBlur = React.useCallback(() => {\n const numValue = value === \"\" ? null : Number(value);\n if (!readOnly && numValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, value, readOnly]);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n setValue(event.target.value);\n },\n [],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Enter\") {\n event.preventDefault();\n const numValue = value === \"\" ? null : Number(value);\n if (numValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const numValue = value === \"\" ? null : Number(value);\n if (numValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(String(initialValue ?? \"\"));\n inputRef.current?.blur();\n }\n } else if (isFocused) {\n // Handle Backspace to start editing with empty value\n if (event.key === \"Backspace\") {\n setValue(\"\");\n } else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {\n // Handle typing to pre-fill the value when editing starts\n setValue(event.key);\n }\n }\n },\n [isEditing, isFocused, initialValue, tableMeta, rowIndex, columnId, value],\n );\n\n React.useEffect(() => {\n const wasEditing = prevIsEditingRef.current;\n prevIsEditingRef.current = isEditing;\n\n // Only focus when we start editing (transition from false to true)\n if (isEditing && !wasEditing && inputRef.current) {\n inputRef.current.focus();\n }\n }, [isEditing]);\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n ) : (\n {value}\n )}\n \n );\n}\n\nexport function UrlCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isEditing,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const cellRef = React.useRef(null);\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n if (cellRef.current && !isEditing) {\n cellRef.current.textContent = initialValue ?? \"\";\n }\n }\n\n const onBlur = React.useCallback(() => {\n const currentValue = cellRef.current?.textContent?.trim() ?? \"\";\n\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, readOnly]);\n\n const onInput = React.useCallback(\n (event: React.FormEvent) => {\n const currentValue = event.currentTarget.textContent ?? \"\";\n setValue(currentValue);\n },\n [],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Enter\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent?.trim() ?? \"\";\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent?.trim() ?? \"\";\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue ?? \"\");\n cellRef.current?.blur();\n }\n } else if (\n isFocused &&\n !readOnly &&\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n // Handle typing to pre-fill the value when editing starts\n setValue(event.key);\n\n queueMicrotask(() => {\n if (cellRef.current && cellRef.current.contentEditable === \"true\") {\n cellRef.current.textContent = event.key;\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n });\n }\n },\n [\n isEditing,\n isFocused,\n initialValue,\n tableMeta,\n rowIndex,\n columnId,\n readOnly,\n ],\n );\n\n const onLinkClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (isEditing) {\n event.preventDefault();\n return;\n }\n\n // Check if URL was rejected due to dangerous protocol\n const href = getUrlHref(value);\n if (!href) {\n event.preventDefault();\n toast.error(\"Invalid URL\", {\n description:\n \"URL contains a dangerous protocol (javascript:, data:, vbscript:, or file:)\",\n });\n return;\n }\n\n // Stop propagation to prevent grid from interfering with link navigation\n event.stopPropagation();\n },\n [isEditing, value],\n );\n\n React.useEffect(() => {\n if (isEditing && cellRef.current) {\n cellRef.current.focus();\n\n if (!cellRef.current.textContent && value) {\n cellRef.current.textContent = value;\n }\n\n if (cellRef.current.textContent) {\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n }\n }, [isEditing, value]);\n\n const displayValue = !isEditing ? (value ?? \"\") : \"\";\n const urlHref = displayValue ? getUrlHref(displayValue) : \"\";\n const isDangerousUrl = displayValue && !urlHref;\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {!isEditing && displayValue ? (\n \n \n {displayValue}\n \n \n ) : (\n \n {displayValue}\n \n )}\n \n );\n}\n\nexport function CheckboxCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: Omit, \"isEditing\">) {\n const initialValue = cell.getValue() as boolean;\n const [value, setValue] = React.useState(Boolean(initialValue));\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(Boolean(initialValue));\n }\n\n const onCheckedChange = React.useCallback(\n (checked: boolean) => {\n if (readOnly) return;\n setValue(checked);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: checked });\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n isFocused &&\n !readOnly &&\n (event.key === \" \" || event.key === \"Enter\")\n ) {\n event.preventDefault();\n event.stopPropagation();\n onCheckedChange(!value);\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isFocused, value, onCheckedChange, tableMeta, readOnly],\n );\n\n const onWrapperClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (isFocused && !readOnly) {\n event.preventDefault();\n event.stopPropagation();\n onCheckedChange(!value);\n }\n },\n [isFocused, value, onCheckedChange, readOnly],\n );\n\n const onCheckboxClick = React.useCallback((event: React.MouseEvent) => {\n event.stopPropagation();\n }, []);\n\n const onCheckboxMouseDown = React.useCallback(\n (event: React.MouseEvent) => {\n event.stopPropagation();\n },\n [],\n );\n\n const onCheckboxDoubleClick = React.useCallback(\n (event: React.MouseEvent) => {\n event.stopPropagation();\n },\n [],\n );\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={false}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n className=\"flex size-full justify-center\"\n onClick={onWrapperClick}\n onKeyDown={onWrapperKeyDown}\n >\n \n \n );\n}\n\nexport function SelectCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue);\n const containerRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const options = React.useMemo(\n () => (cellOpts?.variant === \"select\" ? cellOpts.options : []),\n [cellOpts],\n );\n const optionByValue = React.useMemo(\n () => new Map(options.map((option) => [option.value, option])),\n [options],\n );\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue);\n }\n\n const onValueChange = React.useCallback(\n (newValue: string) => {\n if (readOnly) return;\n setValue(newValue);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\n tableMeta?.onCellEditingStop?.();\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, initialValue, tableMeta],\n );\n\n const displayLabel = optionByValue.get(value)?.label ?? value;\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n \n {displayLabel ? (\n \n \n \n ) : (\n \n )}\n \n \n {options.map((option) => (\n \n {option.label}\n \n ))}\n \n \n ) : displayLabel ? (\n \n {displayLabel}\n \n ) : null}\n \n );\n}\n\nexport function MultiSelectCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const cellValue = React.useMemo(() => {\n const value = cell.getValue() as string[];\n return value ?? [];\n }, [cell]);\n\n const cellKey = getCellKey(rowIndex, columnId);\n const prevCellKeyRef = React.useRef(cellKey);\n\n const [selectedValues, setSelectedValues] =\n React.useState(cellValue);\n const [searchValue, setSearchValue] = React.useState(\"\");\n const containerRef = React.useRef(null);\n const inputRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const options = React.useMemo(\n () => (cellOpts?.variant === \"multi-select\" ? cellOpts.options : []),\n [cellOpts],\n );\n const optionByValue = React.useMemo(\n () => new Map(options.map((option) => [option.value, option])),\n [options],\n );\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const prevCellValueRef = React.useRef(cellValue);\n if (cellValue !== prevCellValueRef.current) {\n prevCellValueRef.current = cellValue;\n setSelectedValues(cellValue);\n }\n\n if (prevCellKeyRef.current !== cellKey) {\n prevCellKeyRef.current = cellKey;\n setSearchValue(\"\");\n }\n\n const onValueChange = React.useCallback(\n (value: string) => {\n if (readOnly) return;\n let newValues: string[] = [];\n setSelectedValues((curr) => {\n newValues = curr.includes(value)\n ? curr.filter((v) => v !== value)\n : [...curr, value];\n return newValues;\n });\n queueMicrotask(() => {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n inputRef.current?.focus();\n });\n setSearchValue(\"\");\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const removeValue = React.useCallback(\n (valueToRemove: string, event?: React.MouseEvent) => {\n if (readOnly) return;\n event?.stopPropagation();\n event?.preventDefault();\n let newValues: string[] = [];\n setSelectedValues((curr) => {\n newValues = curr.filter((v) => v !== valueToRemove);\n return newValues;\n });\n queueMicrotask(() => {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n inputRef.current?.focus();\n });\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const clearAll = React.useCallback(() => {\n if (readOnly) return;\n setSelectedValues([]);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: [] });\n queueMicrotask(() => inputRef.current?.focus());\n }, [tableMeta, rowIndex, columnId, readOnly]);\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n inputRef.current?.focus();\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && event.key === \"Escape\") {\n event.preventDefault();\n setSelectedValues(cellValue);\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, cellValue, tableMeta],\n );\n\n const onInputKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"Backspace\" && searchValue === \"\") {\n event.preventDefault();\n let newValues: string[] | null = null;\n setSelectedValues((curr) => {\n if (curr.length === 0) return curr;\n newValues = curr.slice(0, -1);\n return newValues;\n });\n queueMicrotask(() => {\n if (newValues !== null) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n }\n inputRef.current?.focus();\n });\n }\n if (event.key === \"Escape\") {\n event.stopPropagation();\n }\n },\n [searchValue, tableMeta, rowIndex, columnId],\n );\n\n const displayLabels = selectedValues\n .map((val) => optionByValue.get(val)?.label ?? val)\n .filter(Boolean);\n\n const selectedValuesSet = React.useMemo(\n () => new Set(selectedValues),\n [selectedValues],\n );\n\n const lineCount = getLineCount(rowHeight);\n\n const { visibleItems: visibleLabels, hiddenCount: hiddenBadgeCount } =\n useBadgeOverflow({\n items: displayLabels,\n getLabel: (label) => label,\n containerRef,\n lineCount,\n });\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n \n
\n \n \n \n
\n {selectedValues.map((value) => {\n const label = optionByValue.get(value)?.label ?? value;\n\n return (\n \n {label}\n removeValue(value, event)}\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n }}\n >\n \n \n \n );\n })}\n \n
\n \n No options found.\n \n {options.map((option) => {\n const isSelected = selectedValuesSet.has(option.value);\n\n return (\n onValueChange(option.value)}\n >\n \n \n
\n {option.label}\n \n );\n })}\n \n {selectedValues.length > 0 && (\n <>\n \n \n \n Clear all\n \n \n \n )}\n \n \n \n
\n ) : null}\n {displayLabels.length > 0 ? (\n
\n {visibleLabels.map((label, index) => (\n \n {label}\n \n ))}\n {hiddenBadgeCount > 0 && (\n \n +{hiddenBadgeCount}\n \n )}\n
\n ) : null}\n \n );\n}\n\nexport function DateCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n }\n\n // Parse date as local time to avoid timezone shifts\n const selectedDate = value ? (parseLocalDate(value) ?? undefined) : undefined;\n\n const onDateSelect = React.useCallback(\n (date: Date | undefined) => {\n if (!date || readOnly) return;\n\n // Format using local date components to avoid timezone issues\n const formattedDate = formatDateToString(date);\n setValue(formattedDate);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: formattedDate });\n tableMeta?.onCellEditingStop?.();\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, initialValue, tableMeta],\n );\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n \n \n \n {formatDateForDisplay(value)}\n \n \n {isEditing && (\n \n \n \n )}\n \n \n );\n}\n\nexport function FileCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const cellValue = React.useMemo(\n () => (cell.getValue() as FileCellData[]) ?? [],\n [cell],\n );\n\n const cellKey = getCellKey(rowIndex, columnId);\n const prevCellKeyRef = React.useRef(cellKey);\n\n const labelId = React.useId();\n const descriptionId = React.useId();\n\n const [files, setFiles] = React.useState(cellValue);\n const [uploadingFiles, setUploadingFiles] = React.useState>(\n new Set(),\n );\n const [deletingFiles, setDeletingFiles] = React.useState>(\n new Set(),\n );\n const [isDraggingOver, setIsDraggingOver] = React.useState(false);\n const [isDragging, setIsDragging] = React.useState(false);\n const [error, setError] = React.useState(null);\n\n const isUploading = uploadingFiles.size > 0;\n const isDeleting = deletingFiles.size > 0;\n const isPending = isUploading || isDeleting;\n const containerRef = React.useRef(null);\n const fileInputRef = React.useRef(null);\n const dropzoneRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const fileCellOpts = cellOpts?.variant === \"file\" ? cellOpts : null;\n const maxFileSize = fileCellOpts?.maxFileSize ?? 10 * 1024 * 1024;\n const maxFiles = fileCellOpts?.maxFiles ?? 10;\n const accept = fileCellOpts?.accept;\n const multiple = fileCellOpts?.multiple ?? false;\n\n const acceptedTypes = React.useMemo(\n () => (accept ? accept.split(\",\").map((t) => t.trim()) : null),\n [accept],\n );\n\n const prevCellValueRef = React.useRef(cellValue);\n if (cellValue !== prevCellValueRef.current) {\n prevCellValueRef.current = cellValue;\n for (const file of files) {\n if (file.url) {\n URL.revokeObjectURL(file.url);\n }\n }\n setFiles(cellValue);\n setError(null);\n }\n\n if (prevCellKeyRef.current !== cellKey) {\n prevCellKeyRef.current = cellKey;\n setError(null);\n }\n\n const validateFile = React.useCallback(\n (file: File): string | null => {\n if (maxFileSize && file.size > maxFileSize) {\n return `File size exceeds ${formatFileSize(maxFileSize)}`;\n }\n if (acceptedTypes) {\n const fileExtension = `.${file.name.split(\".\").pop()}`;\n const isAccepted = acceptedTypes.some((type) => {\n if (type.endsWith(\"/*\")) {\n const baseType = type.slice(0, -2);\n return file.type.startsWith(`${baseType}/`);\n }\n if (type.startsWith(\".\")) {\n return fileExtension.toLowerCase() === type.toLowerCase();\n }\n return file.type === type;\n });\n if (!isAccepted) {\n return \"File type not accepted\";\n }\n }\n return null;\n },\n [maxFileSize, acceptedTypes],\n );\n\n const addFiles = React.useCallback(\n async (newFiles: File[], skipUpload = false) => {\n if (readOnly || isPending) return;\n setError(null);\n\n if (maxFiles && files.length + newFiles.length > maxFiles) {\n const errorMessage = `Maximum ${maxFiles} files allowed`;\n setError(errorMessage);\n toast(errorMessage);\n setTimeout(() => {\n setError(null);\n }, 2000);\n return;\n }\n\n const rejectedFiles: Array<{ name: string; reason: string }> = [];\n const filesToValidate: File[] = [];\n\n for (const file of newFiles) {\n const validationError = validateFile(file);\n if (validationError) {\n rejectedFiles.push({ name: file.name, reason: validationError });\n continue;\n }\n filesToValidate.push(file);\n }\n\n if (rejectedFiles.length > 0) {\n const firstError = rejectedFiles[0];\n if (firstError) {\n setError(firstError.reason);\n\n const truncatedName =\n firstError.name.length > 20\n ? `${firstError.name.slice(0, 20)}...`\n : firstError.name;\n\n if (rejectedFiles.length === 1) {\n toast(firstError.reason, {\n description: `\"${truncatedName}\" has been rejected`,\n });\n } else {\n toast(firstError.reason, {\n description: `\"${truncatedName}\" and ${rejectedFiles.length - 1} more rejected`,\n });\n }\n\n setTimeout(() => {\n setError(null);\n }, 2000);\n }\n }\n\n if (filesToValidate.length > 0) {\n if (!skipUpload) {\n const tempFiles = filesToValidate.map((f) => ({\n id: crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: undefined,\n }));\n const filesWithTemp = [...files, ...tempFiles];\n setFiles(filesWithTemp);\n\n const uploadingIds = new Set(tempFiles.map((f) => f.id));\n setUploadingFiles(uploadingIds);\n\n let uploadedFiles: FileCellData[] = [];\n\n if (tableMeta?.onFilesUpload) {\n try {\n uploadedFiles = await tableMeta.onFilesUpload({\n files: filesToValidate,\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error\n ? error.message\n : `Failed to upload ${filesToValidate.length} file${filesToValidate.length !== 1 ? \"s\" : \"\"}`,\n );\n setFiles((prev) => prev.filter((f) => !uploadingIds.has(f.id)));\n setUploadingFiles(new Set());\n return;\n }\n } else {\n uploadedFiles = filesToValidate.map((f, i) => ({\n id: tempFiles[i]?.id ?? crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: URL.createObjectURL(f),\n }));\n }\n\n const finalFiles = filesWithTemp\n .map((f) => {\n if (uploadingIds.has(f.id)) {\n return uploadedFiles.find((uf) => uf.name === f.name) ?? f;\n }\n return f;\n })\n .filter((f) => f.url !== undefined);\n\n setFiles(finalFiles);\n setUploadingFiles(new Set());\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: finalFiles });\n } else {\n const newFilesData: FileCellData[] = filesToValidate.map((f) => ({\n id: crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: URL.createObjectURL(f),\n }));\n const updatedFiles = [...files, ...newFilesData];\n setFiles(updatedFiles);\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: updatedFiles,\n });\n }\n }\n },\n [\n files,\n maxFiles,\n validateFile,\n tableMeta,\n rowIndex,\n columnId,\n readOnly,\n isPending,\n ],\n );\n\n const removeFile = React.useCallback(\n async (fileId: string) => {\n if (readOnly || isPending) return;\n setError(null);\n\n const fileToRemove = files.find((f) => f.id === fileId);\n if (!fileToRemove) return;\n\n setDeletingFiles((prev) => new Set(prev).add(fileId));\n\n if (tableMeta?.onFilesDelete) {\n try {\n await tableMeta.onFilesDelete({\n fileIds: [fileId],\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error\n ? error.message\n : `Failed to delete ${fileToRemove.name}`,\n );\n setDeletingFiles((prev) => {\n const next = new Set(prev);\n next.delete(fileId);\n return next;\n });\n return;\n }\n }\n\n if (fileToRemove.url?.startsWith(\"blob:\")) {\n URL.revokeObjectURL(fileToRemove.url);\n }\n\n const updatedFiles = files.filter((f) => f.id !== fileId);\n setFiles(updatedFiles);\n setDeletingFiles((prev) => {\n const next = new Set(prev);\n next.delete(fileId);\n return next;\n });\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: updatedFiles });\n },\n [files, tableMeta, rowIndex, columnId, readOnly, isPending],\n );\n\n const clearAll = React.useCallback(async () => {\n if (readOnly || isPending) return;\n setError(null);\n\n const fileIds = files.map((f) => f.id);\n setDeletingFiles(new Set(fileIds));\n\n if (tableMeta?.onFilesDelete && files.length > 0) {\n try {\n await tableMeta.onFilesDelete({\n fileIds,\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error ? error.message : \"Failed to delete files\",\n );\n setDeletingFiles(new Set());\n return;\n }\n }\n\n for (const file of files) {\n if (file.url?.startsWith(\"blob:\")) {\n URL.revokeObjectURL(file.url);\n }\n }\n setFiles([]);\n setDeletingFiles(new Set());\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: [] });\n }, [files, tableMeta, rowIndex, columnId, readOnly, isPending]);\n\n const onCellDragEnter = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n if (event.dataTransfer.types.includes(\"Files\")) {\n setIsDraggingOver(true);\n }\n }, []);\n\n const onCellDragLeave = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const rect = event.currentTarget.getBoundingClientRect();\n const x = event.clientX;\n const y = event.clientY;\n\n if (\n x <= rect.left ||\n x >= rect.right ||\n y <= rect.top ||\n y >= rect.bottom\n ) {\n setIsDraggingOver(false);\n }\n }, []);\n\n const onCellDragOver = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n }, []);\n\n const onCellDrop = React.useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDraggingOver(false);\n\n const droppedFiles = Array.from(event.dataTransfer.files);\n if (droppedFiles.length > 0) {\n addFiles(droppedFiles, false);\n }\n },\n [addFiles],\n );\n\n const onDropzoneDragEnter = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDragging(true);\n }, []);\n\n const onDropzoneDragLeave = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const rect = event.currentTarget.getBoundingClientRect();\n const x = event.clientX;\n const y = event.clientY;\n\n if (\n x <= rect.left ||\n x >= rect.right ||\n y <= rect.top ||\n y >= rect.bottom\n ) {\n setIsDragging(false);\n }\n }, []);\n\n const onDropzoneDragOver = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n }, []);\n\n const onDropzoneDrop = React.useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDragging(false);\n\n const droppedFiles = Array.from(event.dataTransfer.files);\n addFiles(droppedFiles, false);\n },\n [addFiles],\n );\n\n const onDropzoneClick = React.useCallback(() => {\n fileInputRef.current?.click();\n }, []);\n\n const onDropzoneKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onDropzoneClick();\n }\n },\n [onDropzoneClick],\n );\n\n const onFileInputChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const selectedFiles = Array.from(event.target.files ?? []);\n addFiles(selectedFiles, false);\n event.target.value = \"\";\n },\n [addFiles],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n setError(null);\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n setError(null);\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onEscapeKeyDown: NonNullable<\n React.ComponentProps[\"onEscapeKeyDown\"]\n > = React.useCallback((event) => {\n // Prevent the escape key from propagating to the data grid's keyboard handler\n // which would call blurCell() and remove focus from the cell\n event.stopPropagation();\n }, []);\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n queueMicrotask(() => {\n dropzoneRef.current?.focus();\n });\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Escape\") {\n event.preventDefault();\n setFiles(cellValue);\n setError(null);\n tableMeta?.onCellEditingStop?.();\n } else if (event.key === \" \") {\n event.preventDefault();\n onDropzoneClick();\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n } else if (isFocused && event.key === \"Enter\") {\n event.preventDefault();\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [\n isEditing,\n isFocused,\n cellValue,\n tableMeta,\n onDropzoneClick,\n rowIndex,\n columnId,\n ],\n );\n\n React.useEffect(() => {\n return () => {\n for (const file of files) {\n if (file.url) {\n URL.revokeObjectURL(file.url);\n }\n }\n };\n }, [files]);\n\n const lineCount = getLineCount(rowHeight);\n\n const { visibleItems: visibleFiles, hiddenCount: hiddenFileCount } =\n useBadgeOverflow({\n items: files,\n getLabel: (file) => file.name,\n containerRef,\n lineCount,\n cacheKeyPrefix: \"file\",\n iconSize: 12,\n maxWidth: 100,\n });\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n className={cn({\n \"ring-1 ring-primary/80 ring-inset\": isDraggingOver,\n })}\n onDragEnter={onCellDragEnter}\n onDragLeave={onCellDragLeave}\n onDragOver={onCellDragOver}\n onDrop={onCellDrop}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n \n
\n \n \n
\n \n File upload\n \n \n \n
\n

\n {isDragging ? \"Drop files here\" : \"Drag files here\"}\n

\n

\n or click to browse\n

\n
\n

\n {maxFileSize\n ? `Max size: ${formatFileSize(maxFileSize)}${maxFiles ? ` • Max ${maxFiles} files` : \"\"}`\n : maxFiles\n ? `Max ${maxFiles} files`\n : \"Select files to upload\"}\n

\n
\n \n {files.length > 0 && (\n
\n
\n

\n {files.length} {files.length === 1 ? \"file\" : \"files\"}\n

\n \n Clear all\n \n
\n
\n {files.map((file) => {\n const FileIcon = getFileIcon(file.type);\n const isFileUploading = uploadingFiles.has(file.id);\n const isFileDeleting = deletingFiles.has(file.id);\n const isFilePending = isFileUploading || isFileDeleting;\n\n return (\n \n {FileIcon && (\n \n )}\n
\n

{file.name}

\n

\n {isFileUploading\n ? \"Uploading...\"\n : isFileDeleting\n ? \"Deleting...\"\n : formatFileSize(file.size)}\n

\n
\n removeFile(file.id)}\n disabled={isPending}\n >\n \n \n
\n );\n })}\n
\n
\n )}\n \n \n
\n ) : null}\n {isDraggingOver ? (\n
\n \n Drop files here\n
\n ) : files.length > 0 ? (\n
\n {visibleFiles.map((file) => {\n const isUploading = uploadingFiles.has(file.id);\n\n if (isUploading) {\n return (\n \n );\n }\n\n const FileIcon = getFileIcon(file.type);\n\n return (\n \n {FileIcon && }\n {file.name}\n \n );\n })}\n {hiddenFileCount > 0 && (\n \n +{hiddenFileCount}\n \n )}\n
\n ) : null}\n \n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-cell-variants.tsx" }, diff --git a/src/components/data-grid/data-grid-cell-variants.tsx b/src/components/data-grid/data-grid-cell-variants.tsx index 8d2b90499..e0838e529 100644 --- a/src/components/data-grid/data-grid-cell-variants.tsx +++ b/src/components/data-grid/data-grid-cell-variants.tsx @@ -873,7 +873,10 @@ export function SelectCell({ const [value, setValue] = React.useState(initialValue); const containerRef = React.useRef(null); const cellOpts = cell.column.columnDef.meta?.cell; - const options = cellOpts?.variant === "select" ? cellOpts.options : []; + const options = React.useMemo( + () => (cellOpts?.variant === "select" ? cellOpts.options : []), + [cellOpts], + ); const optionByValue = React.useMemo( () => new Map(options.map((option) => [option.value, option])), [options], @@ -1017,7 +1020,10 @@ export function MultiSelectCell({ const containerRef = React.useRef(null); const inputRef = React.useRef(null); const cellOpts = cell.column.columnDef.meta?.cell; - const options = cellOpts?.variant === "multi-select" ? cellOpts.options : []; + const options = React.useMemo( + () => (cellOpts?.variant === "multi-select" ? cellOpts.options : []), + [cellOpts], + ); const optionByValue = React.useMemo( () => new Map(options.map((option) => [option.value, option])), [options],