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.
177 lines
5.8 KiB
TypeScript
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)
|
|
}
|