typeof v === 'string') : []}
onChange={onChange}
className={base}
compact={compact}
/>
)
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 (
{t('structuredViews.multiselectPick')}
{options.map((opt) => {
const active = value.includes(opt)
return (
)
})}
)
}
return (
{options.map((opt) => {
const active = value.includes(opt)
return (
)
})}
)
}
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}`)
}