feat(UI-Overhaul-Phase3): Wiki WYSIWYG Editor mit Tiptap

- WikiEditor.tsx: Complete rebuild with Tiptap WYSIWYG editor
  - Notion-style floating toolbar with bold/italic/underline/strike
  - Headings (H1/H2/H3), bullet/ordered lists, blockquote, code blocks
  - Link and image insertion
  - Text alignment (left/center/right)
  - Undo/redo support
  - HTML-to-Markdown conversion for backend storage
  - Markdown-to-HTML conversion for editor initialization
- Wiki.tsx: View/Edit mode toggle
  - View mode: Rendered Markdown (ReactMarkdown)
  - Edit mode: WYSIWYG editor (Tiptap)
  - Inline save button in edit mode
  - wikiMode resets to view when selecting new article
  - handleInlineSave saves article content directly

tsc clean
This commit is contained in:
Agent Zero
2026-08-21 13:51:56 +02:00
parent 7f9a2bca50
commit 6c197e1fc5
2 changed files with 242 additions and 59 deletions
+52 -6
View File
@@ -81,6 +81,7 @@ export function WikiPage() {
const [deleting, setDeleting] = useState(false);
const [restoring, setRestoring] = useState(false);
const [browserRefreshKey, setBrowserRefreshKey] = useState(0);
const [wikiMode, setWikiMode] = useState<'view' | 'edit'>('view');
const loadCategories = useCallback(async () => {
try {
@@ -112,6 +113,7 @@ export function WikiPage() {
const handleSelectArticle = useCallback(
(a: WikiArticle) => {
void loadArticle(a.id);
setWikiMode('view');
},
[loadArticle]
);
@@ -172,6 +174,30 @@ export function WikiPage() {
}
}, [form, editingId, t, toast]);
// Inline save from View/Edit mode toggle
const handleInlineSave = useCallback(async () => {
if (!article) return;
setSaving(true);
try {
const updated = await updateWikiArticle(article.id, {
title: article.title,
content: article.content,
summary: article.summary,
category_id: article.category_id,
tags: article.tags,
status: article.status,
});
setArticle(updated);
toast.success(t('knowledge.editor.updated'));
setWikiMode('view');
setBrowserRefreshKey((k) => k + 1);
} catch (err) {
toast.error(err instanceof Error ? err.message : t('knowledge.editor.saveError'));
} finally {
setSaving(false);
}
}, [article, t, toast]);
const handleDelete = useCallback(async () => {
if (!deleteTarget) return;
setDeleting(true);
@@ -255,12 +281,22 @@ export function WikiPage() {
actions={
<div className="flex items-center gap-2">
{statusBadge}
<Button
variant="ghost"
size="sm"
onClick={() => setWikiMode(wikiMode === 'view' ? 'edit' : 'view')}
icon={wikiMode === 'view' ? <Pencil className="h-4 w-4" aria-hidden="true" /> : <History className="h-4 w-4" aria-hidden="true" />}
>
{wikiMode === 'view' ? t('common.edit') : t('common.preview', 'Vorschau')}
</Button>
{wikiMode === 'edit' && (
<Button variant="ghost" size="sm" onClick={() => void handleInlineSave()} isLoading={saving}>
{t('common.save')}
</Button>
)}
<Button variant="ghost" size="sm" onClick={() => void loadVersions()} icon={<History className="h-4 w-4" aria-hidden="true" />}>
{t('knowledge.versions.title')}
</Button>
<Button variant="ghost" size="sm" onClick={openEdit} icon={<Pencil className="h-4 w-4" aria-hidden="true" />}>
{t('common.edit')}
</Button>
<Button variant="danger" size="sm" onClick={() => setDeleteTarget(article)} icon={<Trash2 className="h-4 w-4" aria-hidden="true" />}>
{t('common.delete')}
</Button>
@@ -308,9 +344,19 @@ export function WikiPage() {
</div>
)}
<div className="prose prose-sm max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{article.content || t('wiki.detail.emptyContent')}</ReactMarkdown>
</div>
{wikiMode === 'edit' ? (
<div>
<WikiEditor
value={article.content}
onChange={(value) => setArticle({ ...article, content: value })}
placeholder={t('wiki.editor.contentPlaceholder')}
/>
</div>
) : (
<div className="prose prose-sm max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{article.content || t('wiki.detail.emptyContent')}</ReactMarkdown>
</div>
)}
</Card>
) : (
<EmptyState