- 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)
274 lines
9.6 KiB
TypeScript
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>
|
|
)
|
|
}
|