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