import { hasCellEditor } from '../editor-registry' export type CellEditorType = | 'text' | 'number' | 'date' | 'datetime' | 'time' // Opt-out of the rich date/time pickers: render the plain native inputs. | 'date-native' | 'datetime-native' | 'time-native' | 'password' | 'checkbox' | 'list' | 'chips' | 'select' | 'rich-select' | 'autocomplete' | 'textarea' | 'color' | 'rating' // Any other string names a CUSTOM editor registered via `registerCellEditor`. // `(string & {})` keeps the literals above autocompleting while allowing // arbitrary custom type names. | (string & {}) /** Normalized option used by list/chips editors. The optional `color` * paints the chip - both in the in-cell readonly chips render and in * the dropdown trigger when `renderChipsInTrigger` is on. Any CSS * color works (hex, rgb, hsl, oklch, named); the chip uses a soft * tint built via `color-mix` so it sits well on both themes. */ export type CellEditorOption = { value: string | number; label: string; color?: string } export type ParseEditorValueOptions = { /** When true, list/chips return an array; otherwise a scalar. */ multiple?: boolean } /** Normalize the loose `editorOptions` ColumnDef prop into a uniform shape. * Dynamic (function) editorOptions are resolved before reaching here. */ export function normalizeEditorOptions( options: | ReadonlyArray< string | number | { value: string | number; label?: string; color?: string } > | undefined, ): CellEditorOption[] { if (!options || typeof options === 'function') return [] return options.map((opt) => { if (typeof opt === 'string' || typeof opt === 'number') { return { value: opt, label: String(opt) } } return { value: opt.value, label: opt.label ?? String(opt.value), color: opt.color, } }) } export function parseEditorValue( rawType: CellEditorType, value: unknown, opts?: ParseEditorValueOptions, ) { // `date-native` etc. parse identically to their rich counterparts. const type = rawType.replace(/-native$/, '') as CellEditorType if (type === 'number') { const parsed = Number(value) return Number.isFinite(parsed) ? parsed : null } if (type === 'rating') { const parsed = Number(value) if (!Number.isFinite(parsed)) return 0 return Math.max(0, Math.min(5, Math.round(parsed))) } if (type === 'color') { const s = String(value ?? '').trim() // Native always returns "#rrggbb"; if anything // else comes in (CSS name, rgb()), pass it through unchanged so the // caller can keep whatever color string they had. return s || '#000000' } if (type === 'date') { const date = new Date(String(value)) return Number.isNaN(date.getTime()) ? null : date.toISOString() } if (type === 'datetime') { const date = new Date(String(value)) return Number.isNaN(date.getTime()) ? null : date.toISOString() } if (type === 'time') { // Native `` returns "HH:MM" (or "HH:MM:SS"). Accept // anything that parses to that shape; reject malformed strings as // `null` so consumers can distinguish "cleared" from "never set". const s = String(value ?? '').trim() if (!s) return null const m = s.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/) if (!m) return null const h = Math.max(0, Math.min(23, parseInt(m[1]!, 10))) const mm = Math.max(0, Math.min(59, parseInt(m[2]!, 10))) const ss = m[3] ? Math.max(0, Math.min(59, parseInt(m[3]!, 10))) : null const HH = String(h).padStart(2, '0') const MM = String(mm).padStart(2, '0') return ss === null ? `${HH}:${MM}` : `${HH}:${MM}:${String(ss).padStart(2, '0')}` } if (type === 'password') { // Password is just a string with masked rendering. Empty string is a // legitimate value (user cleared the field); keep it instead of nulling. return String(value ?? '') } if (type === 'checkbox') { if (typeof value === 'boolean') return value return String(value).toLowerCase() === 'true' } if (type === 'select' || type === 'rich-select') { // Single-select: trust the editor's reported value; coerce nullish // to `null` so consumers can disambiguate "user cleared" from "user // never picked". if (Array.isArray(value)) return value[0] ?? null return value ?? null } if (type === 'textarea') { return String(value ?? '') } // Custom registered editors own their value shape (like list/chips), so pass // whatever the editor committed through unchanged instead of stringifying it. if (hasCellEditor(type)) return value if (type === 'list' || type === 'chips') { // The editor manages the value shape: a scalar for single-select, an // array for multi-select. Trust what the editor handed us, just coerce // the shape so consumers can rely on the contract. if (opts?.multiple) { if (Array.isArray(value)) return value.slice() if (value == null || value === '') return [] return [value] } if (Array.isArray(value)) return value[0] ?? null return value ?? null } return String(value ?? '') }