/** * Wiki page — knowledge articles with categories, Markdown editor, versioning. * * Combines WikiBrowser (category tree + article list + search), article detail * with Markdown preview, create/edit modal, delete, and version history with * restore. */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { CalendarDays, History, Loader2, Pencil, Plus, Trash2, Undo2 } from 'lucide-react'; import { createWikiArticle, deleteWikiArticle, fetchWikiArticle, fetchWikiCategories, fetchWikiVersions, restoreWikiVersion, updateWikiArticle, type WikiArticle, type WikiArticleStatus, type WikiCategory, type WikiVersion, } from '@/api/knowledge'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Select } from '@/components/ui/Select'; import { Modal } from '@/components/ui/Modal'; import { ConfirmDialog } from '@/components/ui/ConfirmDialog'; import { Badge } from '@/components/ui/Badge'; import { EmptyState } from '@/components/ui/EmptyState'; import { useToast } from '@/components/ui/Toast'; import { WikiBrowser } from '@/components/wiki/WikiBrowser'; import { WikiEditor } from '@/components/wiki/WikiEditor'; interface ArticleForm { title: string; slug: string; content: string; summary: string; category_id: string; tags: string; status: WikiArticleStatus; } const emptyForm: ArticleForm = { title: '', slug: '', content: '', summary: '', category_id: '', tags: '', status: 'draft', }; function formatDate(iso: string | null): string { if (!iso) return '—'; const d = new Date(iso); if (Number.isNaN(d.getTime())) return '—'; return d.toLocaleString(); } export function WikiPage() { const { t } = useTranslation(); const toast = useToast(); const [selectedArticleId, setSelectedArticleId] = useState(null); const [article, setArticle] = useState(null); const [loadingArticle, setLoadingArticle] = useState(false); const [categories, setCategories] = useState<{ id: string; name: string }[]>([]); const [versions, setVersions] = useState([]); const [showVersions, setShowVersions] = useState(false); const [editorOpen, setEditorOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(emptyForm); const [saving, setSaving] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); const [restoring, setRestoring] = useState(false); const [browserRefreshKey, setBrowserRefreshKey] = useState(0); const loadCategories = useCallback(async () => { try { const cats = await fetchWikiCategories(); setCategories(cats.map((c) => ({ id: c.id, name: c.name }))); } catch { // Non-fatal — categories are optional in the editor } }, []); useEffect(() => { void loadCategories(); }, [loadCategories]); const loadArticle = useCallback(async (id: string) => { setLoadingArticle(true); try { const data = await fetchWikiArticle(id); setArticle(data); setSelectedArticleId(data.id); setShowVersions(false); } catch (err) { toast.error(err instanceof Error ? err.message : t('wiki.detail.loadError')); } finally { setLoadingArticle(false); } }, [t, toast]); const handleSelectArticle = useCallback( (a: WikiArticle) => { void loadArticle(a.id); }, [loadArticle] ); const openCreate = useCallback(() => { setEditingId(null); setForm(emptyForm); setEditorOpen(true); }, []); const openEdit = useCallback(() => { if (!article) return; setEditingId(article.id); setForm({ title: article.title, slug: article.slug, content: article.content, summary: article.summary ?? '', category_id: article.category_id ?? '', tags: article.tags.join(', '), status: article.status, }); setEditorOpen(true); }, [article]); const handleSave = useCallback(async () => { if (!form.title.trim()) { toast.error(t('wiki.editor.titleRequired')); return; } setSaving(true); try { const payload = { title: form.title.trim(), slug: form.slug.trim() || undefined, content: form.content, summary: form.summary.trim() || null, category_id: form.category_id || null, tags: form.tags.split(',').map((s) => s.trim()).filter(Boolean), status: form.status, }; if (editingId) { const updated = await updateWikiArticle(editingId, payload); setArticle(updated); toast.success(t('knowledge.editor.updated')); } else { const created = await createWikiArticle(payload); setArticle(created); setSelectedArticleId(created.id); toast.success(t('knowledge.editor.created')); } setEditorOpen(false); setBrowserRefreshKey((k) => k + 1); } catch (err) { toast.error(err instanceof Error ? err.message : t('knowledge.editor.saveError')); } finally { setSaving(false); } }, [form, editingId, t, toast]); const handleDelete = useCallback(async () => { if (!deleteTarget) return; setDeleting(true); try { await deleteWikiArticle(deleteTarget.id); toast.success(t('knowledge.editor.deleted')); setArticle(null); setSelectedArticleId(null); setDeleteTarget(null); setBrowserRefreshKey((k) => k + 1); } catch (err) { toast.error(err instanceof Error ? err.message : t('knowledge.editor.deleteError')); } finally { setDeleting(false); } }, [deleteTarget, t, toast]); const loadVersions = useCallback(async () => { if (!article) return; try { const data = await fetchWikiVersions(article.id); setVersions(data); setShowVersions(true); } catch (err) { toast.error(err instanceof Error ? err.message : t('knowledge.versions.loadError')); } }, [article, t, toast]); const handleRestore = useCallback( async (version: number) => { if (!article) return; setRestoring(true); try { const restored = await restoreWikiVersion(article.id, version); setArticle(restored); setShowVersions(false); toast.success(t('knowledge.versions.restored')); } catch (err) { toast.error(err instanceof Error ? err.message : t('knowledge.versions.restoreError')); } finally { setRestoring(false); } }, [article, t, toast] ); const statusBadge = useMemo(() => { if (!article) return null; const variant = article.status === 'published' ? 'success' : article.status === 'archived' ? 'secondary' : 'warning'; return {t(`wiki.status.${article.status}`)}; }, [article, t]); return (

{t('wiki.title')}

{t('wiki.subtitle')}

{/* Browser sidebar */}
{/* Article detail / editor */}
{loadingArticle ? (
) : article ? ( {statusBadge}
} >
{t('knowledge.detail.version')}: {article.version} {article.tags.map((tag) => ( #{tag} ))}
{showVersions && (

{t('knowledge.versions.title')}

{versions.length === 0 ? (

{t('knowledge.versions.empty')}

) : (
    {versions.map((v) => (
  • v{v.version} {formatDate(v.created_at)} {v.edit_comment && — {v.edit_comment}}
  • ))}
)}
)}
{article.content || t('wiki.detail.emptyContent')}
) : (
{/* Create/Edit modal */} setEditorOpen(false)} title={editingId ? t('wiki.editor.editTitle') : t('wiki.editor.createTitle')} size="xl" >
setForm((f) => ({ ...f, title: e.target.value }))} required /> setForm((f) => ({ ...f, slug: e.target.value }))} placeholder={t('wiki.editor.slugPlaceholder')} />
setForm((f) => ({ ...f, status: e.target.value as WikiArticleStatus }))} options={[ { value: 'draft', label: t('wiki.status.draft') }, { value: 'published', label: t('wiki.status.published') }, { value: 'archived', label: t('wiki.status.archived') }, ]} /> setForm((f) => ({ ...f, tags: e.target.value }))} placeholder={t('wiki.editor.tagsPlaceholder')} />
setForm((f) => ({ ...f, summary: e.target.value }))} />
setForm((f) => ({ ...f, content: value }))} placeholder={t('wiki.editor.contentPlaceholder')} />
void handleDelete()} onCancel={() => setDeleteTarget(null)} /> ); }