## Bug Fixes ### Note Card Actions - Fix broken size change functionality (missing state declaration) - Implement React 19 useOptimistic for instant UI feedback - Add startTransition for non-blocking updates - Ensure smooth animations without page refresh - All note actions now work: pin, archive, color, size, checklist ### Markdown LaTeX Rendering - Add remark-math and rehype-katex plugins - Support inline equations with dollar sign syntax - Support block equations with double dollar sign syntax - Import KaTeX CSS for proper styling - Equations now render correctly instead of showing raw LaTeX ## Technical Details - Replace undefined currentNote references with optimistic state - Add optimistic updates before server actions for instant feedback - Use router.refresh() in transitions for smart cache invalidation - Install remark-math, rehype-katex, and katex packages ## Testing - Build passes successfully with no TypeScript errors - Dev server hot-reloads changes correctly
246 lines
9.7 KiB
TypeScript
246 lines
9.7 KiB
TypeScript
'use client'
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Checkbox } from '@/components/ui/checkbox'
|
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { updateSystemConfig, testSMTP } from '@/app/actions/admin-settings'
|
|
import { toast } from 'sonner'
|
|
import { useState, useEffect } from 'react'
|
|
|
|
export function AdminSettingsForm({ config }: { config: Record<string, string> }) {
|
|
const [isSaving, setIsSaving] = useState(false)
|
|
const [isTesting, setIsTesting] = useState(false)
|
|
|
|
// Local state for Checkbox
|
|
const [allowRegister, setAllowRegister] = useState(config.ALLOW_REGISTRATION !== 'false')
|
|
const [smtpSecure, setSmtpSecure] = useState(config.SMTP_SECURE === 'true')
|
|
const [smtpIgnoreCert, setSmtpIgnoreCert] = useState(config.SMTP_IGNORE_CERT === 'true')
|
|
|
|
// Sync state with config when server revalidates
|
|
useEffect(() => {
|
|
setAllowRegister(config.ALLOW_REGISTRATION !== 'false')
|
|
setSmtpSecure(config.SMTP_SECURE === 'true')
|
|
setSmtpIgnoreCert(config.SMTP_IGNORE_CERT === 'true')
|
|
}, [config])
|
|
|
|
const handleSaveSecurity = async (formData: FormData) => {
|
|
setIsSaving(true)
|
|
// We override the formData get because the hidden input might be tricky
|
|
const data = {
|
|
ALLOW_REGISTRATION: allowRegister ? 'true' : 'false',
|
|
}
|
|
|
|
const result = await updateSystemConfig(data)
|
|
setIsSaving(false)
|
|
|
|
if (result.error) {
|
|
toast.error('Failed to update security settings')
|
|
} else {
|
|
toast.success('Security Settings updated')
|
|
}
|
|
}
|
|
|
|
const handleSaveAI = async (formData: FormData) => {
|
|
setIsSaving(true)
|
|
const data = {
|
|
AI_PROVIDER: formData.get('AI_PROVIDER') as string,
|
|
OLLAMA_BASE_URL: formData.get('OLLAMA_BASE_URL') as string,
|
|
AI_MODEL_EMBEDDING: formData.get('AI_MODEL_EMBEDDING') as string,
|
|
OPENAI_API_KEY: formData.get('OPENAI_API_KEY') as string,
|
|
}
|
|
|
|
const result = await updateSystemConfig(data)
|
|
setIsSaving(false)
|
|
|
|
if (result.error) {
|
|
toast.error('Failed to update AI settings')
|
|
} else {
|
|
toast.success('AI Settings updated')
|
|
}
|
|
}
|
|
|
|
const handleSaveSMTP = async (formData: FormData) => {
|
|
setIsSaving(true)
|
|
const data = {
|
|
SMTP_HOST: formData.get('SMTP_HOST') as string,
|
|
SMTP_PORT: formData.get('SMTP_PORT') as string,
|
|
SMTP_USER: formData.get('SMTP_USER') as string,
|
|
SMTP_PASS: formData.get('SMTP_PASS') as string,
|
|
SMTP_FROM: formData.get('SMTP_FROM') as string,
|
|
SMTP_IGNORE_CERT: smtpIgnoreCert ? 'true' : 'false',
|
|
SMTP_SECURE: smtpSecure ? 'true' : 'false',
|
|
}
|
|
|
|
const result = await updateSystemConfig(data)
|
|
setIsSaving(false)
|
|
|
|
if (result.error) {
|
|
toast.error('Failed to update SMTP settings')
|
|
} else {
|
|
toast.success('SMTP Settings updated')
|
|
}
|
|
}
|
|
|
|
const handleTestEmail = async () => {
|
|
setIsTesting(true)
|
|
try {
|
|
const result: any = await testSMTP()
|
|
if (result.success) {
|
|
toast.success('Test email sent successfully!')
|
|
} else {
|
|
toast.error(`Failed: ${result.error}`)
|
|
}
|
|
} catch (e: any) {
|
|
toast.error(`Error: ${e.message}`)
|
|
} finally {
|
|
setIsTesting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Security Settings</CardTitle>
|
|
<CardDescription>Manage access control and registration policies.</CardDescription>
|
|
</CardHeader>
|
|
<form action={handleSaveSecurity}>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="ALLOW_REGISTRATION"
|
|
checked={allowRegister}
|
|
onCheckedChange={(c) => setAllowRegister(!!c)}
|
|
/>
|
|
<label
|
|
htmlFor="ALLOW_REGISTRATION"
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
>
|
|
Allow Public Registration
|
|
</label>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
If disabled, new users can only be added by an Administrator via the User Management page.
|
|
</p>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button type="submit" disabled={isSaving}>Save Security Settings</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>AI Configuration</CardTitle>
|
|
<CardDescription>Configure the AI provider for auto-tagging and semantic search.</CardDescription>
|
|
</CardHeader>
|
|
<form action={handleSaveAI}>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
<label htmlFor="AI_PROVIDER" className="text-sm font-medium">Provider</label>
|
|
<select
|
|
id="AI_PROVIDER"
|
|
name="AI_PROVIDER"
|
|
defaultValue={config.AI_PROVIDER || 'ollama'}
|
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
>
|
|
<option value="ollama">Ollama (Local)</option>
|
|
<option value="openai">OpenAI</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label htmlFor="OLLAMA_BASE_URL" className="text-sm font-medium">Ollama Base URL</label>
|
|
<Input id="OLLAMA_BASE_URL" name="OLLAMA_BASE_URL" defaultValue={config.OLLAMA_BASE_URL || 'http://localhost:11434'} placeholder="http://localhost:11434" />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label htmlFor="AI_MODEL_EMBEDDING" className="text-sm font-medium">Embedding Model</label>
|
|
<Input id="AI_MODEL_EMBEDDING" name="AI_MODEL_EMBEDDING" defaultValue={config.AI_MODEL_EMBEDDING || 'embeddinggemma:latest'} placeholder="embeddinggemma:latest" />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label htmlFor="OPENAI_API_KEY" className="text-sm font-medium">OpenAI API Key (if using OpenAI)</label>
|
|
<Input id="OPENAI_API_KEY" name="OPENAI_API_KEY" type="password" defaultValue={config.OPENAI_API_KEY || ''} placeholder="sk-..." />
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button type="submit" disabled={isSaving}>Save AI Settings</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>SMTP Configuration</CardTitle>
|
|
<CardDescription>Configure email server for password resets.</CardDescription>
|
|
</CardHeader>
|
|
<form action={handleSaveSMTP}>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<label htmlFor="SMTP_HOST" className="text-sm font-medium">Host</label>
|
|
<Input id="SMTP_HOST" name="SMTP_HOST" defaultValue={config.SMTP_HOST || ''} placeholder="smtp.example.com" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label htmlFor="SMTP_PORT" className="text-sm font-medium">Port</label>
|
|
<Input id="SMTP_PORT" name="SMTP_PORT" defaultValue={config.SMTP_PORT || '587'} placeholder="587" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label htmlFor="SMTP_USER" className="text-sm font-medium">Username</label>
|
|
<Input id="SMTP_USER" name="SMTP_USER" defaultValue={config.SMTP_USER || ''} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label htmlFor="SMTP_PASS" className="text-sm font-medium">Password</label>
|
|
<Input id="SMTP_PASS" name="SMTP_PASS" type="password" defaultValue={config.SMTP_PASS || ''} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label htmlFor="SMTP_FROM" className="text-sm font-medium">From Email</label>
|
|
<Input id="SMTP_FROM" name="SMTP_FROM" defaultValue={config.SMTP_FROM || 'noreply@memento.app'} />
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="SMTP_SECURE"
|
|
checked={smtpSecure}
|
|
onCheckedChange={(c) => setSmtpSecure(!!c)}
|
|
/>
|
|
<label
|
|
htmlFor="SMTP_SECURE"
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
>
|
|
Force SSL/TLS (usually for port 465)
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="SMTP_IGNORE_CERT"
|
|
checked={smtpIgnoreCert}
|
|
onCheckedChange={(c) => setSmtpIgnoreCert(!!c)}
|
|
/>
|
|
<label
|
|
htmlFor="SMTP_IGNORE_CERT"
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-yellow-600"
|
|
>
|
|
Ignore Certificate Errors (Self-hosted/Dev only)
|
|
</label>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter className="flex justify-between">
|
|
<Button type="submit" disabled={isSaving}>Save SMTP Settings</Button>
|
|
<Button type="button" variant="secondary" onClick={handleTestEmail} disabled={isTesting}>
|
|
{isTesting ? 'Sending...' : 'Test Email'}
|
|
</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|