import type { ColumnFilter, ColumnSort, NotePropertyValues, PropertyType, SchemaProperty, } from './types' export function parsePropertyOptions(raw: string | null | undefined): string[] { if (!raw) return [] try { const parsed = JSON.parse(raw) return Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === 'string') : [] } catch { return [] } } export function serializePropertyValue(type: PropertyType, value: unknown): string | null { if (value === null || value === undefined || value === '') { return type === 'checkbox' ? JSON.stringify(false) : null } if (type === 'checkbox') { return JSON.stringify(Boolean(value)) } if (type === 'number') { const n = typeof value === 'number' ? value : Number(value) return Number.isFinite(n) ? JSON.stringify(n) : null } if (type === 'multiselect') { const arr = Array.isArray(value) ? value : [] return JSON.stringify(arr.filter((v) => typeof v === 'string')) } return JSON.stringify(String(value)) } export function parseStoredPropertyValue(type: PropertyType, raw: string | null | undefined): unknown { if (raw == null || raw === '') { if (type === 'checkbox') return false if (type === 'multiselect') return [] return null } try { const parsed = JSON.parse(raw) if (type === 'checkbox') return Boolean(parsed) if (type === 'number') return typeof parsed === 'number' ? parsed : Number(parsed) if (type === 'multiselect') return Array.isArray(parsed) ? parsed : [] return parsed } catch { return raw } } export function formatPropertyDisplay(type: PropertyType, value: unknown): string { if (value == null || value === '') return '—' if (type === 'checkbox') return value ? '✓' : '—' if (type === 'multiselect') { return Array.isArray(value) ? value.join(', ') : String(value) } if (type === 'date' && typeof value === 'string') { const d = new Date(value) return Number.isNaN(d.getTime()) ? String(value) : d.toLocaleDateString() } return String(value) } export function comparePropertyValues( type: PropertyType, a: unknown, b: unknown, direction: 'asc' | 'desc', ): number { const emptyA = a == null || a === '' || (Array.isArray(a) && a.length === 0) const emptyB = b == null || b === '' || (Array.isArray(b) && b.length === 0) if (emptyA && emptyB) return 0 if (emptyA) return direction === 'asc' ? 1 : -1 if (emptyB) return direction === 'asc' ? -1 : 1 let cmp = 0 if (type === 'number') { cmp = Number(a) - Number(b) } else if (type === 'date') { cmp = new Date(String(a)).getTime() - new Date(String(b)).getTime() } else if (type === 'checkbox') { cmp = Number(Boolean(a)) - Number(Boolean(b)) } else if (type === 'multiselect') { cmp = formatPropertyDisplay(type, a).localeCompare(formatPropertyDisplay(type, b)) } else { cmp = String(a).localeCompare(String(b), undefined, { sensitivity: 'base' }) } return direction === 'asc' ? cmp : -cmp } export function matchesFilter( type: PropertyType, value: unknown, filter: ColumnFilter, ): boolean { const { operator, value: filterValue } = filter const empty = value == null || value === '' || (Array.isArray(value) && value.length === 0) if (operator === 'empty') return empty if (empty) return false const haystack = formatPropertyDisplay(type, value).toLowerCase() const needle = (filterValue ?? '').toLowerCase() if (operator === 'equals') { if (type === 'multiselect' && Array.isArray(value)) { return value.some((v) => String(v).toLowerCase() === needle) } return haystack === needle } return haystack.includes(needle) } export function sortNotesWithProperties( notes: T[], valuesByNote: Record, sort: ColumnSort, properties: SchemaProperty[], ): T[] { const prop = properties.find((p) => p.id === sort.propertyId) const copy = [...notes] copy.sort((a, b) => { if (sort.propertyId === 'title') { const ta = (a.title ?? '').toLowerCase() const tb = (b.title ?? '').toLowerCase() const cmp = ta.localeCompare(tb) return sort.direction === 'asc' ? cmp : -cmp } if (sort.propertyId === 'updatedAt') { const cmp = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime() return sort.direction === 'asc' ? cmp : -cmp } if (!prop) return 0 const va = valuesByNote[a.id]?.[prop.id] const vb = valuesByNote[b.id]?.[prop.id] return comparePropertyValues(prop.type, va, vb, sort.direction) }) return copy } export function filterNotesWithProperties( notes: T[], valuesByNote: Record, filters: ColumnFilter[], properties: SchemaProperty[], ): T[] { if (filters.length === 0) return notes return notes.filter((note) => { const vals = valuesByNote[note.id] ?? {} return filters.every((filter) => { if (filter.propertyId === 'title') { const title = (note as { title?: string | null }).title ?? '' return matchesFilter('text', title, filter) } const prop = properties.find((p) => p.id === filter.propertyId) if (!prop) return true return matchesFilter(prop.type, vals[prop.id], filter) }) }) } export function isValidPropertyType(value: string): value is PropertyType { return ['text', 'number', 'date', 'select', 'multiselect', 'checkbox'].includes(value) }