'use client' import { useEffect, useMemo, useRef, useState } from 'react' import { cn } from '@/lib/utils' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { useLanguage } from '@/lib/i18n' import type { PropertyType, SchemaProperty } from '@/lib/structured-views/types' import { openNotePeek } from '@/lib/note-peek-sync' type PropertyValueEditorProps = { property: SchemaProperty value: unknown onChange: (value: unknown) => void compact?: boolean className?: string } export function PropertyValueEditor({ property, value, onChange, compact, className, }: PropertyValueEditorProps) { const base = cn( 'w-full rounded-md border border-border/60 bg-background text-sm', compact ? 'px-2 py-1 text-[12px]' : 'px-3 py-2', className, ) switch (property.type) { case 'checkbox': return ( onChange(e.target.checked)} className="h-4 w-4 accent-brand-accent" /> ) case 'number': return ( onChange(e.target.value === '' ? null : Number(e.target.value))} className={base} /> ) case 'date': return ( onChange(e.target.value || null)} className={base} /> ) case 'select': return ( onChange(e.target.value || null)} className={base} > — {property.options.map((opt) => ( {opt} ))} ) case 'multiselect': return ( typeof v === 'string') : []} onChange={onChange} className={base} compact={compact} /> ) case 'relation': return default: return ( onChange(e.target.value || null)} className={base} /> ) } } function MultiSelectEditor({ options, value, onChange, className, compact, }: { options: string[] value: string[] onChange: (v: string[]) => void className?: string compact?: boolean }) { const { t } = useLanguage() const [open, setOpen] = useState(false) const toggle = (opt: string) => { if (value.includes(opt)) onChange(value.filter((v) => v !== opt)) else onChange([...value, opt]) } if (compact) { return ( {value.length === 0 ? ( {t('structuredViews.cellEmpty')} ) : ( {value.map((opt) => ( {opt} ))} )} {t('structuredViews.multiselectPick')} {options.map((opt) => { const active = value.includes(opt) return ( toggle(opt)} className={cn( 'w-full text-left px-2 py-1.5 rounded-md text-[12px] transition-colors', active ? 'bg-foreground text-background font-medium' : 'hover:bg-foreground/5 text-foreground', )} > {opt} ) })} ) } return ( {options.map((opt) => { const active = value.includes(opt) return ( toggle(opt)} className={cn( 'px-2 py-0.5 rounded-full text-[10px] font-bold border transition-colors', active ? 'bg-foreground text-background border-foreground' : 'border-border text-muted-foreground hover:border-foreground/30', )} > {opt} ) })} ) } export function useDebouncedPropertySave( noteId: string, onSaved?: (values: Record) => void, delayMs = 500, ) { const timerRef = useRef | null>(null) const pendingRef = useRef>({}) useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current) }, []) const queueSave = (propertyId: string, value: unknown) => { pendingRef.current[propertyId] = value if (timerRef.current) clearTimeout(timerRef.current) timerRef.current = setTimeout(async () => { const payload = { ...pendingRef.current } pendingRef.current = {} try { const res = await fetch(`/api/notes/${noteId}/properties`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ properties: payload }), }) const json = await res.json() if (json.success && json.data?.values) onSaved?.(json.data.values) } catch (e) { console.error(e) } }, delayMs) } return queueSave } export function formatPropertyTypeLabel(type: PropertyType, t: (k: string) => string) { return t(`structuredViews.propertyTypes.${type}`) } function RelationEditor({ value, onChange, compact, className, }: { value: string | { id: string; title: string } | null onChange: (v: unknown) => void compact?: boolean className?: string }) { const { t } = useLanguage() const [open, setOpen] = useState(false) const [search, setSearch] = useState('') const [results, setResults] = useState>([]) const [loading, setLoading] = useState(false) // Parse value into { id, title } const parsed = useMemo(() => { if (!value) return null if (typeof value === 'object' && value.id) return value as { id: string; title: string } if (typeof value === 'string') { try { const obj = JSON.parse(value) if (obj.id && obj.title) return obj } catch {} return { id: value, title: value } } return null }, [value]) // Fetch real title if we only have an ID (old data) const [resolvedTitle, setResolvedTitle] = useState(null) useEffect(() => { if (!parsed || parsed.title !== parsed.id) { setResolvedTitle(null); return } fetch(`/api/notes?limit=100`) .then(r => r.json()) .then(data => { const note = data.data?.find((n: any) => n.id === parsed.id) if (note) setResolvedTitle(note.title || t('notes.untitled')) }) .catch(() => {}) }, [parsed, t]) const displayTitle = resolvedTitle || parsed?.title || null useEffect(() => { if (!open || !search.trim()) { setResults([]); return } setLoading(true) const timer = setTimeout(() => { fetch(`/api/notes?search=${encodeURIComponent(search.trim())}&limit=10`) .then(r => r.json()) .then(data => setResults(data.data ?? [])) .catch(() => setResults([])) .finally(() => setLoading(false)) }, 250) return () => clearTimeout(timer) }, [open, search]) return ( {displayTitle ? ( { e.stopPropagation(); if (parsed) openNotePeek({ noteId: parsed.id }) }} className="truncate text-brand-accent hover:underline cursor-pointer" > {displayTitle} ) : parsed ? ( ... ) : ( {t('structuredViews.relationEmpty') || 'Lier une note...'} )} setSearch(e.target.value)} placeholder={t('structuredViews.relationSearch') || 'Rechercher une note...'} autoFocus className="w-full border-b border-border/40 px-3 py-2 text-sm bg-transparent outline-none" /> {loading && ...} {!loading && results.length === 0 && search.trim() && ( {t('structuredViews.relationNoResults') || 'Aucune note trouvée'} )} {results.map((note) => ( { onChange({ id: note.id, title: note.title || t('notes.untitled') }) setOpen(false) setSearch('') }} className="w-full text-left px-3 py-1.5 text-sm hover:bg-muted transition-colors truncate" > {note.title || t('notes.untitled')} ))} ) }
{t('structuredViews.multiselectPick')}