fix: post content problems after refactoring
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user