feat: MVP-5 P2 - Export/Import, Settings, Tests y Validaciones
- Ticket 10: Navegación completa de listas por teclado (↑↓ Enter E F P) - Ticket 13: Historial de navegación contextual con recent-context-list - Ticket 17: Exportación mejorada a Markdown con frontmatter - Ticket 18: Exportación HTML simple y legible - Ticket 19: Importador Markdown mejorado con frontmatter, tags, wiki links - Ticket 20: Importador Obsidian-compatible (wiki links, #tags inline) - Ticket 21: Centro de respaldo y portabilidad en Settings - Ticket 22: Configuración visible de feature flags - Ticket 24: Tests de command palette y captura externa - Ticket 25: Harden de validaciones y límites (50MB backup, 10K notas, etc)
This commit is contained in:
+108
-36
@@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef } from 'react'
|
||||
import { Download, Upload, History } from 'lucide-react'
|
||||
import { Download, Upload, History, FileText, Code, FolderOpen } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { toast } from 'sonner'
|
||||
import { BackupList } from '@/components/backup-list'
|
||||
import { PreferencesPanel } from '@/components/preferences-panel'
|
||||
|
||||
function parseMarkdownToNote(content: string, filename: string) {
|
||||
const lines = content.split('\n')
|
||||
@@ -28,31 +29,56 @@ function parseMarkdownToNote(content: string, filename: string) {
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [importing, setImporting] = useState(false)
|
||||
const [exporting, setExporting] = useState<string | null>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleExport = async () => {
|
||||
const handleExport = async (format: 'json' | 'markdown' | 'html') => {
|
||||
setExporting(format)
|
||||
try {
|
||||
const response = await fetch('/api/export-import')
|
||||
const response = await fetch(`/api/export-import?format=${format}`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al exportar')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
let blob: Blob
|
||||
let filename: string
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
|
||||
if (format === 'json') {
|
||||
blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
filename = `recall-backup-${date}.json`
|
||||
} else if (format === 'markdown') {
|
||||
if (data.files) {
|
||||
// Multiple files - in the future could be a zip
|
||||
blob = new Blob([data.files.map((f: { content: string }) => f.content).join('\n\n---\n\n')], { type: 'text/markdown' })
|
||||
} else {
|
||||
blob = new Blob([data.content], { type: 'text/markdown' })
|
||||
}
|
||||
filename = `recall-export-${date}.md`
|
||||
} else {
|
||||
if (data.files) {
|
||||
blob = new Blob([data.files.map((f: { content: string }) => f.content).join('\n\n')], { type: 'text/html' })
|
||||
} else {
|
||||
blob = new Blob([data.content], { type: 'text/html' })
|
||||
}
|
||||
filename = `recall-export-${date}.html`
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `recall-backup-${date}.json`
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
toast.success('Notas exportadas correctamente')
|
||||
toast.success(`Notas exportadas en formato ${format.toUpperCase()}`)
|
||||
} catch {
|
||||
toast.error('Error al exportar las notas')
|
||||
} finally {
|
||||
setExporting(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,15 +95,17 @@ export default function SettingsPage() {
|
||||
const isMarkdown = file.name.endsWith('.md')
|
||||
|
||||
let payload: object[]
|
||||
let endpoint = '/api/export-import'
|
||||
|
||||
if (isMarkdown) {
|
||||
const note = parseMarkdownToNote(text, file.name)
|
||||
payload = [note]
|
||||
payload = [{ markdown: text, filename: file.name }]
|
||||
endpoint = '/api/import-markdown'
|
||||
} else {
|
||||
payload = JSON.parse(text)
|
||||
}
|
||||
|
||||
const response = await fetch('/api/export-import', {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
@@ -89,7 +117,11 @@ export default function SettingsPage() {
|
||||
throw new Error(result.error || 'Error al importar')
|
||||
}
|
||||
|
||||
toast.success(`${result.count} nota${result.count !== 1 ? 's' : ''} importada${result.count !== 1 ? 's' : ''} correctamente`)
|
||||
const msg = result.warnings
|
||||
? `${result.count} nota${result.count !== 1 ? 's' : ''} importada${result.count !== 1 ? 's' : ''} correctamente (con advertencias)`
|
||||
: `${result.count} nota${result.count !== 1 ? 's' : ''} importada${result.count !== 1 ? 's' : ''} correctamente`
|
||||
|
||||
toast.success(msg)
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
@@ -104,27 +136,82 @@ export default function SettingsPage() {
|
||||
<main className="container mx-auto py-8 px-4">
|
||||
<h1 className="text-2xl font-bold mb-6">Configuración</h1>
|
||||
|
||||
<div className="grid gap-6 max-w-xl">
|
||||
<div className="grid gap-6 max-w-2xl">
|
||||
{/* Preferences Section */}
|
||||
<PreferencesPanel />
|
||||
|
||||
{/* Backups Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Exportar notas</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<History className="h-5 w-5" />
|
||||
Backups y Restauración
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Descarga todas tus notas en formato JSON. El archivo incluye títulos, contenido, tipos y tags.
|
||||
Los backups automáticos se guardan localmente. También puedes crear un backup manual antes de operaciones riesgosas.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={handleExport} className="gap-2">
|
||||
<Download className="h-4 w-4" />
|
||||
Exportar
|
||||
</Button>
|
||||
<BackupList />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Export Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Importar notas</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Download className="h-5 w-5" />
|
||||
Exportar Notas
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Importa notas desde archivos JSON o MD. En archivos MD, el primer heading (#) se usa como título.
|
||||
Descarga tus notas en diferentes formatos. Elige el que mejor se adapte a tus necesidades.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() => handleExport('json')}
|
||||
disabled={exporting !== null}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
{exporting === 'json' ? 'Exportando...' : 'JSON (Backup completo)'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleExport('markdown')}
|
||||
disabled={exporting !== null}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
{exporting === 'markdown' ? 'Exportando...' : 'Markdown'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleExport('html')}
|
||||
disabled={exporting !== null}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<Code className="h-4 w-4" />
|
||||
{exporting === 'html' ? 'Exportando...' : 'HTML'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Import Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Upload className="h-5 w-5" />
|
||||
Importar Notas
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Importa notas desde archivos JSON o Markdown. Soporta frontmatter, tags, y enlaces wiki.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
@@ -138,28 +225,13 @@ export default function SettingsPage() {
|
||||
onClick={handleImport}
|
||||
disabled={importing}
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
className="gap-2 self-start"
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
{importing ? 'Importando...' : 'Importar'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<History className="h-5 w-5" />
|
||||
Backups
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Restaura notas desde backups guardados localmente en tu navegador.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BackupList />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user