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.
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import type { PropertyType, SchemaProperty } from '@/lib/structured-views/types'
|
import type { PropertyType, SchemaProperty } from '@/lib/structured-views/types'
|
||||||
|
import { openNotePeek } from '@/lib/note-peek-sync'
|
||||||
|
|
||||||
type PropertyValueEditorProps = {
|
type PropertyValueEditorProps = {
|
||||||
property: SchemaProperty
|
property: SchemaProperty
|
||||||
@@ -78,6 +79,8 @@ export function PropertyValueEditor({
|
|||||||
compact={compact}
|
compact={compact}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case 'relation':
|
||||||
|
return <RelationEditor value={value as string | null} onChange={onChange} compact={compact} className={className} />
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
@@ -227,3 +230,123 @@ export function useDebouncedPropertySave(
|
|||||||
export function formatPropertyTypeLabel(type: PropertyType, t: (k: string) => string) {
|
export function formatPropertyTypeLabel(type: PropertyType, t: (k: string) => string) {
|
||||||
return t(`structuredViews.propertyTypes.${type}`)
|
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<Array<{ id: string; title: string | null }>>([])
|
||||||
|
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<string | null>(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 (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
'w-full min-h-[28px] rounded-md border border-transparent hover:border-border/60 px-2 text-left transition-colors flex items-center gap-1',
|
||||||
|
compact && 'text-[12px] py-0.5',
|
||||||
|
!compact && 'py-1.5',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{displayTitle ? (
|
||||||
|
<span
|
||||||
|
role="link"
|
||||||
|
onClick={(e) => { e.stopPropagation(); if (parsed) openNotePeek({ noteId: parsed.id }) }}
|
||||||
|
className="truncate text-brand-accent hover:underline cursor-pointer"
|
||||||
|
>
|
||||||
|
{displayTitle}
|
||||||
|
</span>
|
||||||
|
) : parsed ? (
|
||||||
|
<span className="truncate text-muted-foreground text-[11px]">...</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground/50 text-[11px]">{t('structuredViews.relationEmpty') || 'Lier une note...'}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-64 p-0" align="start">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<div className="max-h-48 overflow-y-auto">
|
||||||
|
{loading && <div className="px-3 py-2 text-xs text-muted-foreground">...</div>}
|
||||||
|
{!loading && results.length === 0 && search.trim() && (
|
||||||
|
<div className="px-3 py-2 text-xs text-muted-foreground">{t('structuredViews.relationNoResults') || 'Aucune note trouvée'}</div>
|
||||||
|
)}
|
||||||
|
{results.map((note) => (
|
||||||
|
<button
|
||||||
|
key={note.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
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')}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
PropertyType,
|
PropertyType,
|
||||||
SchemaProperty,
|
SchemaProperty,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
import { PROPERTY_TYPES } from './types'
|
||||||
|
|
||||||
export function parsePropertyOptions(raw: string | null | undefined): string[] {
|
export function parsePropertyOptions(raw: string | null | undefined): string[] {
|
||||||
if (!raw) return []
|
if (!raw) return []
|
||||||
@@ -31,6 +32,12 @@ export function serializePropertyValue(type: PropertyType, value: unknown): stri
|
|||||||
const arr = Array.isArray(value) ? value : []
|
const arr = Array.isArray(value) ? value : []
|
||||||
return JSON.stringify(arr.filter((v) => typeof v === 'string'))
|
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))
|
return JSON.stringify(String(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +45,7 @@ export function parseStoredPropertyValue(type: PropertyType, raw: string | null
|
|||||||
if (raw == null || raw === '') {
|
if (raw == null || raw === '') {
|
||||||
if (type === 'checkbox') return false
|
if (type === 'checkbox') return false
|
||||||
if (type === 'multiselect') return []
|
if (type === 'multiselect') return []
|
||||||
|
if (type === 'relation') return null
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -45,6 +53,7 @@ export function parseStoredPropertyValue(type: PropertyType, raw: string | null
|
|||||||
if (type === 'checkbox') return Boolean(parsed)
|
if (type === 'checkbox') return Boolean(parsed)
|
||||||
if (type === 'number') return typeof parsed === 'number' ? parsed : Number(parsed)
|
if (type === 'number') return typeof parsed === 'number' ? parsed : Number(parsed)
|
||||||
if (type === 'multiselect') return Array.isArray(parsed) ? 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
|
return parsed
|
||||||
} catch {
|
} catch {
|
||||||
return raw
|
return raw
|
||||||
|
|||||||
Reference in New Issue
Block a user