Files
Momento/memento-note/lib/structured-views/property-utils.ts
Antigravity 0784c94242
Some checks failed
CI / Lint, Test & Build (push) Failing after 57s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): vues structurées tableau/kanban, flashcards et MCP robuste
Ajoute la base organisable par carnet (schéma, champs partagés, valeurs par note)
avec activation guidée, tableau éditable, kanban et suppression de colonnes.
Corrige le multiselect en vue tableau et enrichit sidebar, grille et i18n FR/EN.
Inclut aussi les améliorations flashcards SM-2, l'audit consentement IA et la
robustesse du serveur MCP (config, validation, rate-limit, métriques).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 23:03:16 +00:00

168 lines
5.4 KiB
TypeScript

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<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)
}