Files
Momento/memento-note/lib/structured-views/property-utils.ts
Antigravity c21cbf84a1
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m38s
CI / Deploy production (on server) (push) Successful in 1m5s
revert: champ Relation retiré — doublon avec wikilinks [[note]]
Le champ Relation reproduit ce que les wikilinks font déjà.
Momento est centré sur les notes, pas sur les bases de données.
Ajoute de la complexité pour un bénéfice nul.
2026-06-19 20:29:33 +00:00

177 lines
5.8 KiB
TypeScript

import type {
ColumnFilter,
ColumnSort,
NotePropertyValues,
PropertyType,
SchemaProperty,
} from './types'
import { PROPERTY_TYPES } 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'))
}
if (type === 'relation') {
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value)
}
return JSON.stringify(String(value))
}
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 []
if (type === 'relation') return null
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 : []
if (type === 'relation') return typeof parsed === 'object' ? parsed : { id: String(parsed), title: String(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<T extends { id: string; title?: string | null; updatedAt: string | Date }>(
notes: T[],
valuesByNote: Record<string, NotePropertyValues>,
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<T extends { id: string }>(
notes: T[],
valuesByNote: Record<string, NotePropertyValues>,
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)
}