Files
Momento/memento-note/components/structured-views/structured-views-wizard.tsx
Antigravity 83110200d5
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m31s
CI / Deploy production (on server) (push) Has been skipped
feat: calculs tableaux Structured Views + Link Preview + fixes
- Calculs en pied de tableau : Somme/Moyenne/Min/Max/Compte, cliquable pour changer
- Link Preview : métadonnées persistées + texte indexable pour recherche/embeddings
- Fix: bg-memento-paper → bg-card (dark mode) sur dialogs Structured Views
- Fix: bouton Ajouter un champ → brand-accent au lieu de primary
- Calendar view retiré du sélecteur (non pertinent)
2026-06-14 17:56:54 +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-card 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>
)
}