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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user