Files
Momento/memento-note/components/structured-views/structured-views-wizard.tsx
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

274 lines
9.6 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { X, CheckSquare, BookOpen, GraduationCap, LayoutList } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useLanguage } from '@/lib/i18n'
import { toast } from 'sonner'
import type { NotesLayoutMode } from '@/components/notes-list-views'
import {
WIZARD_DEFAULT_VIEW,
WIZARD_FIELDS_BY_GOAL,
WIZARD_GOALS,
type WizardFieldDef,
type WizardFieldId,
type WizardGoal,
} from '@/lib/structured-views/wizard-templates'
import { cn } from '@/lib/utils'
type StructuredViewsWizardProps = {
open: boolean
onClose: () => void
onComplete: (view: NotesLayoutMode) => void
structuredModeActive: boolean
enableStructuredMode: () => Promise<unknown>
addProperty: (name: string, type: string, options?: string[]) => Promise<{ properties: { id: string; name: string; type: string }[] } | null | undefined>
setKanbanGroupProperty: (propertyId: string | null) => Promise<void>
initialGoal?: WizardGoal
}
const GOAL_ICONS: Record<WizardGoal, React.ElementType> = {
tasks: CheckSquare,
learning: GraduationCap,
reading: BookOpen,
simple: LayoutList,
}
const VIEW_LABEL_KEYS: Record<'list' | 'gallery' | 'table' | 'kanban', string> = {
list: 'structuredViews.viewList',
gallery: 'structuredViews.viewGallery',
table: 'structuredViews.viewTable',
kanban: 'structuredViews.viewKanban',
}
export function StructuredViewsWizard({
open,
onClose,
onComplete,
structuredModeActive,
enableStructuredMode,
addProperty,
setKanbanGroupProperty,
initialGoal,
}: StructuredViewsWizardProps) {
const { t } = useLanguage()
const [step, setStep] = useState(0)
const [goal, setGoal] = useState<WizardGoal>('tasks')
const [selectedFields, setSelectedFields] = useState<Set<WizardFieldId>>(new Set())
const [view, setView] = useState<NotesLayoutMode>('list')
const [saving, setSaving] = useState(false)
const fieldDefs = WIZARD_FIELDS_BY_GOAL[goal]
useEffect(() => {
if (!open) return
const g = initialGoal ?? 'tasks'
setStep(0)
setGoal(g)
setSelectedFields(new Set(WIZARD_FIELDS_BY_GOAL[g].map((f) => f.id)))
setView(WIZARD_DEFAULT_VIEW[g])
}, [open, initialGoal])
useEffect(() => {
setSelectedFields(new Set(fieldDefs.map((f) => f.id)))
setView(WIZARD_DEFAULT_VIEW[goal])
}, [goal, fieldDefs])
const kanbanAllowed = fieldDefs.some((f) => f.type === 'select' && selectedFields.has(f.id))
if (!open) return null
const toggleField = (id: WizardFieldId) => {
setSelectedFields((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const fieldLabel = (def: WizardFieldDef) => t(`structuredViews.wizard.fields.${def.id}.name`)
const parseOptions = (fieldId: WizardFieldId) =>
t(`structuredViews.wizard.fields.${fieldId}.options`)
.split('\n')
.map((l) => l.trim())
.filter(Boolean)
const handleFinish = async () => {
setSaving(true)
try {
if (!structuredModeActive) await enableStructuredMode()
let kanbanPropertyId: string | null = null
for (const def of fieldDefs) {
if (!selectedFields.has(def.id)) continue
const name = fieldLabel(def)
const options = def.hasOptions ? parseOptions(def.id) : []
const schema = await addProperty(name, def.type, options)
const created = schema?.properties.find((p) => p.name === name)
if (created && def.type === 'select' && !kanbanPropertyId) {
kanbanPropertyId = created.id
}
}
const finalView = view === 'kanban' && !kanbanAllowed ? 'list' : view
if (finalView === 'kanban' && kanbanPropertyId) {
await setKanbanGroupProperty(kanbanPropertyId)
}
onComplete(finalView)
onClose()
} catch {
toast.error(t('structuredViews.enableFailed'))
} finally {
setSaving(false)
}
}
const goNext = () => {
if (step === 0 && goal === 'simple') {
setStep(2)
return
}
setStep((s) => Math.min(s + 1, 2))
}
return (
<div className="fixed inset-0 z-[210] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm">
<div
role="dialog"
aria-modal
className="w-full max-w-lg rounded-2xl border border-border bg-memento-paper shadow-xl p-6 space-y-5"
>
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="font-memento-serif text-xl">{t('structuredViews.wizard.title')}</h2>
<p className="text-[13px] text-muted-foreground mt-1">{t('structuredViews.wizard.subtitle')}</p>
</div>
<button type="button" onClick={onClose} className="p-1 rounded-lg hover:bg-foreground/5 shrink-0">
<X size={18} />
</button>
</div>
{step === 0 && (
<div className="space-y-3">
<p className="text-[10px] uppercase tracking-widest font-bold text-muted-foreground">
{t('structuredViews.wizard.stepGoal')}
</p>
<div className="grid gap-2">
{WIZARD_GOALS.map((g) => {
const Icon = GOAL_ICONS[g]
return (
<button
key={g}
type="button"
onClick={() => setGoal(g)}
className={cn(
'flex items-start gap-3 rounded-xl border p-3 text-left transition-colors',
goal === g
? 'border-foreground bg-foreground/[0.04]'
: 'border-border hover:border-foreground/25',
)}
>
<Icon size={18} className="mt-0.5 shrink-0 text-muted-foreground" />
<div>
<div className="text-[13px] font-medium">{t(`structuredViews.wizard.goals.${g}.title`)}</div>
<div className="text-[12px] text-muted-foreground mt-0.5">
{t(`structuredViews.wizard.goals.${g}.desc`)}
</div>
</div>
</button>
)
})}
</div>
</div>
)}
{step === 1 && fieldDefs.length > 0 && (
<div className="space-y-3">
<p className="text-[10px] uppercase tracking-widest font-bold text-muted-foreground">
{t('structuredViews.wizard.stepFields')}
</p>
<p className="text-[12px] text-muted-foreground">{t('structuredViews.wizard.fieldsHint')}</p>
<div className="space-y-2">
{fieldDefs.map((def) => (
<label
key={def.id}
className="flex items-center gap-3 rounded-xl border border-border px-3 py-2.5 cursor-pointer hover:bg-foreground/[0.02]"
>
<input
type="checkbox"
checked={selectedFields.has(def.id)}
onChange={() => toggleField(def.id)}
className="rounded border-border"
/>
<span className="text-[13px] font-medium">{fieldLabel(def)}</span>
<span className="text-[10px] text-muted-foreground ms-auto uppercase">
{t(`structuredViews.propertyTypes.${def.type}`)}
</span>
</label>
))}
</div>
</div>
)}
{step === 2 && (
<div className="space-y-3">
<p className="text-[10px] uppercase tracking-widest font-bold text-muted-foreground">
{t('structuredViews.wizard.stepView')}
</p>
<div className="grid grid-cols-2 gap-2">
{(['list', 'gallery', 'table', 'kanban'] as const).map((v) => {
const disabled = v === 'kanban' && !kanbanAllowed
return (
<button
key={v}
type="button"
disabled={disabled}
onClick={() => setView(v)}
className={cn(
'rounded-xl border px-3 py-3 text-[12px] font-bold uppercase tracking-wider transition-colors',
view === v
? 'border-foreground bg-foreground text-background'
: 'border-border text-muted-foreground hover:border-foreground/30',
disabled && 'opacity-40 cursor-not-allowed',
)}
>
{t(VIEW_LABEL_KEYS[v])}
</button>
)
})}
</div>
{view === 'kanban' && !kanbanAllowed && (
<p className="text-[12px] text-muted-foreground">{t('structuredViews.wizard.kanbanNeedsStatus')}</p>
)}
<p className="text-[12px] text-muted-foreground">{t('structuredViews.wizard.doneHint')}</p>
</div>
)}
<div className="flex justify-between gap-2 pt-2">
<Button
type="button"
variant="ghost"
onClick={() => (step === 0 ? onClose() : setStep((s) => s - 1))}
disabled={saving}
>
{step === 0 ? t('general.cancel') : t('structuredViews.wizard.back')}
</Button>
{step < 2 ? (
<Button type="button" onClick={goNext}>
{t('structuredViews.wizard.next')}
</Button>
) : (
<Button type="button" onClick={() => void handleFinish()} disabled={saving}>
{saving ? t('general.loading') : t('structuredViews.wizard.finish')}
</Button>
)}
</div>
</div>
</div>
)
}