fix: post content problems after refactoring

This commit is contained in:
2026-02-13 23:08:47 +01:00
parent 642c6f5294
commit 155e7a09d2

View File

@@ -525,10 +525,10 @@ function setupPhotoArchiveClickHandlers(
} }
interface PostEditorProps { interface PostEditorProps {
post: PostData; postId: string;
} }
const PostEditor: React.FC<PostEditorProps> = ({ post }) => { const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
const { const {
updatePost, updatePost,
markDirty, markDirty,
@@ -539,12 +539,38 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
showErrorModal, showErrorModal,
showConfirmDeleteModal, showConfirmDeleteModal,
media, media,
closeTab,
} = useAppStore(); } = useAppStore();
const [title, setTitle] = useState(post.title); // Fetch full post data from backend
const [content, setContent] = useState(post.content); const [post, setPost] = useState<PostData | null>(null);
const [tags, setTags] = useState<string[]>(post.tags); const [isLoadingPost, setIsLoadingPost] = useState(true);
const [category, setCategory] = useState(post.categories[0] || 'article'); // Track whether form state has been initialized from post data
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
let cancelled = false;
setIsLoadingPost(true);
setIsInitialized(false);
window.electronAPI?.posts.get(postId).then((fetchedPost) => {
if (cancelled) return;
if (fetchedPost) {
setPost(fetchedPost as PostData);
// Also update the store so other components have the full data
useAppStore.getState().updatePost(postId, fetchedPost as Partial<PostData>);
} else {
// Post doesn't exist, close the tab
closeTab(postId);
}
setIsLoadingPost(false);
});
return () => { cancelled = true; };
}, [postId, closeTab]);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [tags, setTags] = useState<string[]>([]);
const [category, setCategory] = useState('article');
const [availableCategories, setAvailableCategories] = useState<string[]>(['article', 'picture', 'aside', 'page']); const [availableCategories, setAvailableCategories] = useState<string[]>(['article', 'picture', 'aside', 'page']);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [hasPublishedVersion, setHasPublishedVersion] = useState(false); const [hasPublishedVersion, setHasPublishedVersion] = useState(false);
@@ -556,12 +582,12 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
const editorRef = useRef<unknown>(null); const editorRef = useRef<unknown>(null);
const previewRef = useRef<HTMLDivElement>(null); const previewRef = useRef<HTMLDivElement>(null);
const isDirty = checkIsDirty(post.id); const isDirty = checkIsDirty(postId);
// Check if post has a published version for discard functionality // Check if post has a published version for discard functionality
useEffect(() => { useEffect(() => {
window.electronAPI?.posts.hasPublishedVersion(post.id).then(setHasPublishedVersion); window.electronAPI?.posts.hasPublishedVersion(postId).then(setHasPublishedVersion);
}, [post.id]); }, [postId]);
// Load available categories from backend (project-scoped) // Load available categories from backend (project-scoped)
useEffect(() => { useEffect(() => {
@@ -603,13 +629,13 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
setLightboxOpen(true); setLightboxOpen(true);
}; };
hydrateGalleries(previewRef.current, post.id, lightboxHandler); hydrateGalleries(previewRef.current, postId, lightboxHandler);
hydratePhotoArchive(previewRef.current, post.id, lightboxHandler); hydratePhotoArchive(previewRef.current, postId, lightboxHandler);
} }
}, 100); }, 100);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [editorMode, post.id, resolvedContent]); }, [editorMode, postId, resolvedContent]);
// Track latest values for auto-save on unmount/switch // Track latest values for auto-save on unmount/switch
const pendingChangesRef = useRef<{ const pendingChangesRef = useRef<{
@@ -628,23 +654,21 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
content, content,
tags, tags,
category, category,
postId: post.id, postId,
isDirty, isDirty,
}; };
}, [title, content, tags, category, post.id, isDirty]); }, [title, content, tags, category, postId, isDirty]);
// Auto-save when switching away from a post or unmounting // Auto-save when switching away from a post or unmounting
useEffect(() => { useEffect(() => {
const prevPostId = post.id;
return () => { return () => {
// Cancel any pending auto-save timer - we'll save immediately // Cancel any pending auto-save timer - we'll save immediately
autoSaveManager.cancel(prevPostId); autoSaveManager.cancel(postId);
const pending = pendingChangesRef.current; const pending = pendingChangesRef.current;
// Only auto-save if the post still exists in the store (not deleted/discarded) // Only auto-save if the post still exists in the store (not deleted/discarded)
const postStillExists = useAppStore.getState().posts.some(p => p.id === prevPostId); const postStillExists = useAppStore.getState().posts.some(p => p.id === postId);
if (pending && pending.postId === prevPostId && pending.isDirty && postStillExists) { if (pending && pending.postId === postId && pending.isDirty && postStillExists) {
// Fire and forget auto-save // Fire and forget auto-save
window.electronAPI?.posts.update(pending.postId, { window.electronAPI?.posts.update(pending.postId, {
title: pending.title, title: pending.title,
@@ -661,19 +685,25 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
}); });
} }
}; };
}, [post.id]); }, [postId]);
// Reset when post changes (after auto-save cleanup runs) // Reset when post data is loaded or changes
useEffect(() => { useEffect(() => {
setTitle(post.title); if (post) {
setContent(post.content); setTitle(post.title);
setTags(post.tags); setContent(post.content);
setCategory(post.categories[0] || 'article'); setTags(post.tags);
markClean(post.id); setCategory(post.categories[0] || 'article');
}, [post.id, post.title, post.content, post.tags, post.categories, markClean]); markClean(postId);
// Mark as initialized AFTER setting local state
setIsInitialized(true);
}
}, [post, postId, markClean]);
// Track changes and notify auto-save manager // Track changes and notify auto-save manager
// Only run after form has been initialized from post data
useEffect(() => { useEffect(() => {
if (!post || !isInitialized) return;
const currentCategory = post.categories[0] || 'article'; const currentCategory = post.categories[0] || 'article';
const tagsChanged = JSON.stringify(tags.slice().sort()) !== JSON.stringify(post.tags.slice().sort()); const tagsChanged = JSON.stringify(tags.slice().sort()) !== JSON.stringify(post.tags.slice().sort());
const hasChanges = const hasChanges =
@@ -683,19 +713,19 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
category !== currentCategory; category !== currentCategory;
if (hasChanges) { if (hasChanges) {
markDirty(post.id); markDirty(postId);
// Notify auto-save manager with accumulated changes // Notify auto-save manager with accumulated changes
// Convert tags array to comma-separated string for auto-save compatibility // Convert tags array to comma-separated string for auto-save compatibility
autoSaveManager.notifyChange(post.id, { autoSaveManager.notifyChange(postId, {
title, title,
content, content,
tags: tags.join(', '), tags: tags.join(', '),
category, category,
}); });
} else { } else {
markClean(post.id); markClean(postId);
} }
}, [title, content, tags, category, post, markDirty, markClean]); }, [title, content, tags, category, post, postId, isInitialized, markDirty, markClean]);
// Handle editor mode change and persist preference // Handle editor mode change and persist preference
const handleEditorModeChange = (mode: EditorMode) => { const handleEditorModeChange = (mode: EditorMode) => {
@@ -707,11 +737,11 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
if (!isDirty || isSaving) return; if (!isDirty || isSaving) return;
// Cancel any pending auto-save since we're saving manually // Cancel any pending auto-save since we're saving manually
autoSaveManager.cancel(post.id); autoSaveManager.cancel(postId);
setIsSaving(true); setIsSaving(true);
try { try {
const updated = await window.electronAPI?.posts.update(post.id, { const updated = await window.electronAPI?.posts.update(postId, {
title, title,
content, content,
tags, tags,
@@ -719,8 +749,8 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
}); });
if (updated) { if (updated) {
updatePost(post.id, updated as Partial<PostData>); updatePost(postId, updated as Partial<PostData>);
markClean(post.id); markClean(postId);
} }
} catch (error) { } catch (error) {
console.error('Failed to save post:', error); console.error('Failed to save post:', error);
@@ -733,14 +763,14 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}, [post.id, title, content, tags, category, isDirty, isSaving, updatePost, markClean, showErrorModal]); }, [postId, title, content, tags, category, isDirty, isSaving, updatePost, markClean, showErrorModal]);
const handlePublish = async () => { const handlePublish = async () => {
await handleSave(); await handleSave();
try { try {
const updated = await window.electronAPI?.posts.publish(post.id); const updated = await window.electronAPI?.posts.publish(postId);
if (updated) { if (updated) {
updatePost(post.id, updated as Partial<PostData>); updatePost(postId, updated as Partial<PostData>);
showToast.success('Post published'); showToast.success('Post published');
} }
} catch (error) { } catch (error) {
@@ -768,22 +798,22 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
try { try {
if (hasPublishedVersion) { if (hasPublishedVersion) {
// Revert to published version // Revert to published version
const reverted = await window.electronAPI?.posts.discard(post.id); const reverted = await window.electronAPI?.posts.discard(postId);
if (reverted) { if (reverted) {
setTitle(reverted.title); setTitle(reverted.title);
setContent(reverted.content); setContent(reverted.content);
setTags(reverted.tags); setTags(reverted.tags);
setCategory(reverted.categories[0] || 'article'); setCategory(reverted.categories[0] || 'article');
updatePost(post.id, reverted as Partial<PostData>); updatePost(postId, reverted as Partial<PostData>);
markClean(post.id); markClean(postId);
showToast.success('Reverted to last published version'); showToast.success('Reverted to last published version');
} }
} else { } else {
// Never published - delete the post entirely // Never published - delete the post entirely
await window.electronAPI?.posts.delete(post.id); await window.electronAPI?.posts.delete(postId);
// Clear pending ref to prevent auto-save on unmount from resurrecting the post // Clear pending ref to prevent auto-save on unmount from resurrecting the post
pendingChangesRef.current = null; pendingChangesRef.current = null;
useAppStore.getState().removePost(post.id); useAppStore.getState().removePost(postId);
useAppStore.getState().setSelectedPost(null); useAppStore.getState().setSelectedPost(null);
showToast.success('Draft deleted'); showToast.success('Draft deleted');
} }
@@ -802,8 +832,8 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
try { try {
// Fetch references to this post // Fetch references to this post
const [linkedBy, linkedMedia] = await Promise.all([ const [linkedBy, linkedMedia] = await Promise.all([
window.electronAPI?.posts.getLinkedBy(post.id), window.electronAPI?.posts.getLinkedBy(postId),
window.electronAPI?.postMedia.getForPost(post.id), window.electronAPI?.postMedia.getForPost(postId),
]); ]);
// Build references array // Build references array
@@ -833,14 +863,14 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
// Show confirmation modal // Show confirmation modal
showConfirmDeleteModal({ showConfirmDeleteModal({
itemType: 'post', itemType: 'post',
itemTitle: post.title || 'Untitled', itemTitle: title || 'Untitled',
references, references,
onConfirm: async () => { onConfirm: async () => {
try { try {
await window.electronAPI?.posts.delete(post.id); await window.electronAPI?.posts.delete(postId);
// Clear pending ref to prevent auto-save on unmount from resurrecting the post // Clear pending ref to prevent auto-save on unmount from resurrecting the post
pendingChangesRef.current = null; pendingChangesRef.current = null;
useAppStore.getState().removePost(post.id); useAppStore.getState().removePost(postId);
useAppStore.getState().setSelectedPost(null); useAppStore.getState().setSelectedPost(null);
showToast.success('Post deleted'); showToast.success('Post deleted');
} catch (error) { } catch (error) {
@@ -994,6 +1024,19 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
}; };
}, [handleSave]); }, [handleSave]);
// Show loading state while fetching post data
if (isLoadingPost || !post) {
return (
<div className="editor">
<div className="editor-empty">
<div className="welcome-content">
<p className="text-muted">Loading post...</p>
</div>
</div>
</div>
);
}
return ( return (
<div className="editor"> <div className="editor">
<div className="editor-header"> <div className="editor-header">
@@ -1078,14 +1121,14 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
</div> </div>
<PostLinks <PostLinks
postId={post.id} postId={postId}
updatedAt={post.updatedAt} updatedAt={post.updatedAt}
onPostClick={(id) => useAppStore.getState().setSelectedPost(id)} onPostClick={(id) => useAppStore.getState().setSelectedPost(id)}
/> />
</div> </div>
<div className="editor-media-panel"> <div className="editor-media-panel">
<LinkedMediaPanel postId={post.id} /> <LinkedMediaPanel postId={postId} />
</div> </div>
</div> </div>
@@ -1174,7 +1217,7 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
<div className="editor-preview markdown-body" ref={previewRef}> <div className="editor-preview markdown-body" ref={previewRef}>
<div <div
className="preview-content" className="preview-content"
dangerouslySetInnerHTML={{ __html: markdownToHtml(resolvedContent, post.id) }} dangerouslySetInnerHTML={{ __html: markdownToHtml(resolvedContent, postId) }}
/> />
</div> </div>
)} )}
@@ -1927,22 +1970,10 @@ export const Editor: React.FC = () => {
} }
}, [activeView, selectedMediaId, media, isLoading, setSelectedMedia]); }, [activeView, selectedMediaId, media, isLoading, setSelectedMedia]);
// Close tab if the item doesn't exist anymore, or fetch it if not yet loaded // Close media tab if the media doesn't exist anymore
useEffect(() => { useEffect(() => {
if (activeTab && !isLoading) { if (activeTab && !isLoading) {
if (activeTab.type === 'post') { if (activeTab.type === 'media') {
const postExists = posts.some(p => p.id === activeTab.id);
if (!postExists) {
// Post might not be loaded yet (pagination / filtered view) — try fetching it
window.electronAPI?.posts.get(activeTab.id).then((post) => {
if (post) {
useAppStore.getState().addPost(post as PostData);
} else {
closeTab(activeTab.id);
}
});
}
} else if (activeTab.type === 'media') {
const mediaExists = media.some(m => m.id === activeTab.id); const mediaExists = media.some(m => m.id === activeTab.id);
if (!mediaExists) { if (!mediaExists) {
closeTab(activeTab.id); closeTab(activeTab.id);
@@ -2007,25 +2038,9 @@ export const Editor: React.FC = () => {
// Show post editor if a post tab is active // Show post editor if a post tab is active
if (showPost && activeTabId) { if (showPost && activeTabId) {
const post = posts.find(p => p.id === activeTabId);
if (post) {
return (
<div className="editor">
<PostEditor key={post.id} post={post} />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
// Post not found - show loading or empty state
return ( return (
<div className="editor"> <div className="editor">
<div className="editor-empty"> <PostEditor key={activeTabId} postId={activeTabId} />
<div className="welcome-content">
<p className="text-muted">{isLoading ? 'Loading post...' : ''}</p>
</div>
</div>
{renderErrorModal()} {renderErrorModal()}
{renderConfirmDeleteModal()} {renderConfirmDeleteModal()}
</div> </div>