fix: better handling of post metadata (from db instead of arrays)
This commit is contained in:
@@ -29,10 +29,8 @@ interface SearchResult {
|
||||
const autoSaveManager = new AutoSaveManager({
|
||||
idleTimeMs: 3000, // Save after 3 seconds of idle time
|
||||
onSave: async (id, changes) => {
|
||||
const state = useAppStore.getState();
|
||||
// Only save if post still exists in store
|
||||
const postExists = state.posts.some(p => p.id === id);
|
||||
if (!postExists) return;
|
||||
// Note: We don't check if post exists in store's posts array since that's limited to 500.
|
||||
// If the post was deleted, the update will fail gracefully.
|
||||
|
||||
// Build update payload from changes
|
||||
const update: Parameters<typeof window.electronAPI.posts.update>[1] = {};
|
||||
@@ -666,9 +664,8 @@ const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
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 === postId);
|
||||
if (pending && pending.postId === postId && pending.isDirty && postStillExists) {
|
||||
// Auto-save if we have pending changes (the update will fail gracefully if post was deleted)
|
||||
if (pending && pending.postId === postId && pending.isDirty) {
|
||||
// Fire and forget auto-save
|
||||
window.electronAPI?.posts.update(pending.postId, {
|
||||
title: pending.title,
|
||||
@@ -1257,15 +1254,17 @@ const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
};
|
||||
|
||||
const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
const { media, posts, updateMedia, showErrorModal, showConfirmDeleteModal, openTab } = useAppStore();
|
||||
const { media, updateMedia, showErrorModal, showConfirmDeleteModal, openTab } = useAppStore();
|
||||
const item = media.find(m => m.id === mediaId);
|
||||
|
||||
const [alt, setAlt] = useState(item?.alt || '');
|
||||
const [caption, setCaption] = useState(item?.caption || '');
|
||||
const [tags, setTags] = useState(item?.tags.join(', ') || '');
|
||||
const [linkedPosts, setLinkedPosts] = useState<{ postId: string; sortOrder: number }[]>([]);
|
||||
const [postTitles, setPostTitles] = useState<Map<string, string>>(new Map());
|
||||
const [showPostPicker, setShowPostPicker] = useState(false);
|
||||
const [postSearchQuery, setPostSearchQuery] = useState('');
|
||||
const [pickerPosts, setPickerPosts] = useState<{ id: string; title: string }[]>([]);
|
||||
|
||||
// Quick action menu state
|
||||
const [showQuickActions, setShowQuickActions] = useState(false);
|
||||
@@ -1326,7 +1325,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Load linked posts for this media
|
||||
// Load linked posts for this media and fetch their titles
|
||||
useEffect(() => {
|
||||
const loadLinkedPosts = async () => {
|
||||
if (!mediaId) return;
|
||||
@@ -1334,6 +1333,15 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
const links = await window.electronAPI?.postMedia.getForMedia(mediaId);
|
||||
if (links) {
|
||||
setLinkedPosts(links.map(l => ({ postId: l.postId, sortOrder: l.sortOrder })));
|
||||
// Fetch titles for linked posts
|
||||
const titles = new Map<string, string>();
|
||||
for (const link of links) {
|
||||
const post = await window.electronAPI?.posts.get(link.postId);
|
||||
if (post) {
|
||||
titles.set(link.postId, post.title || 'Untitled');
|
||||
}
|
||||
}
|
||||
setPostTitles(titles);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load linked posts:', error);
|
||||
@@ -1342,17 +1350,33 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
loadLinkedPosts();
|
||||
}, [mediaId]);
|
||||
|
||||
// Fetch posts for the picker when it opens
|
||||
useEffect(() => {
|
||||
if (!showPostPicker) return;
|
||||
const loadPickerPosts = async () => {
|
||||
try {
|
||||
const result = await window.electronAPI?.posts.getAll({ limit: 100, offset: 0 });
|
||||
if (result?.items) {
|
||||
setPickerPosts(result.items.map(p => ({ id: p.id, title: p.title || 'Untitled' })));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load posts for picker:', error);
|
||||
}
|
||||
};
|
||||
loadPickerPosts();
|
||||
}, [showPostPicker]);
|
||||
|
||||
// Get post titles for display
|
||||
const getPostTitle = (postId: string): string => {
|
||||
const post = posts.find(p => p.id === postId);
|
||||
return post?.title || 'Untitled';
|
||||
return postTitles.get(postId) || 'Loading...';
|
||||
};
|
||||
|
||||
// Handle linking to a new post
|
||||
const handleLinkToPost = async (postId: string) => {
|
||||
const handleLinkToPost = async (postId: string, postTitle: string) => {
|
||||
try {
|
||||
await window.electronAPI?.postMedia.link(postId, mediaId);
|
||||
setLinkedPosts([...linkedPosts, { postId, sortOrder: linkedPosts.length }]);
|
||||
setPostTitles(prev => new Map(prev).set(postId, postTitle));
|
||||
setShowPostPicker(false);
|
||||
setPostSearchQuery('');
|
||||
showToast.success('Linked to post');
|
||||
@@ -1380,10 +1404,10 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
};
|
||||
|
||||
// Get unlinked posts for picker, filtered by search
|
||||
const unlinkedPosts = posts.filter(
|
||||
const unlinkedPosts = pickerPosts.filter(
|
||||
p => !linkedPosts.find(l => l.postId === p.id)
|
||||
).filter(
|
||||
p => !postSearchQuery || (p.title || 'Untitled').toLowerCase().includes(postSearchQuery.toLowerCase())
|
||||
p => !postSearchQuery || p.title.toLowerCase().includes(postSearchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1428,10 +1452,10 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
// Build references array
|
||||
const references: Array<{ id: string; title: string; type: 'post' | 'media' | 'link' }> = [];
|
||||
|
||||
// Add posts that use this media
|
||||
// Add posts that use this media - fetch titles from database
|
||||
if (linkedPostsList && linkedPostsList.length > 0) {
|
||||
linkedPostsList.forEach((link: { postId: string }) => {
|
||||
const post = posts.find(p => p.id === link.postId);
|
||||
for (const link of linkedPostsList) {
|
||||
const post = await window.electronAPI?.posts.get(link.postId);
|
||||
if (post) {
|
||||
references.push({
|
||||
id: post.id,
|
||||
@@ -1439,7 +1463,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
type: 'post',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Show confirmation modal
|
||||
@@ -1622,9 +1646,9 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
<div
|
||||
key={post.id}
|
||||
className="post-picker-item"
|
||||
onClick={() => handleLinkToPost(post.id)}
|
||||
onClick={() => handleLinkToPost(post.id, post.title)}
|
||||
>
|
||||
{post.title || 'Untitled'}
|
||||
{post.title}
|
||||
</div>
|
||||
))}
|
||||
{unlinkedPosts.length > 10 && (
|
||||
@@ -1955,12 +1979,13 @@ export const Editor: React.FC = () => {
|
||||
// Clear selectedPostId if the post doesn't exist (e.g., after project switch)
|
||||
useEffect(() => {
|
||||
if (activeView === 'posts' && selectedPostId && !isLoading) {
|
||||
const postExists = posts.some(p => p.id === selectedPostId);
|
||||
if (!postExists) {
|
||||
setSelectedPost(null);
|
||||
}
|
||||
window.electronAPI?.posts.get(selectedPostId).then(post => {
|
||||
if (!post) {
|
||||
setSelectedPost(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [activeView, selectedPostId, posts, isLoading, setSelectedPost]);
|
||||
}, [activeView, selectedPostId, isLoading, setSelectedPost]);
|
||||
|
||||
// Clear selectedMediaId if the media doesn't exist (e.g., after project switch)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAppStore } from '../../store';
|
||||
import { ProjectSelector } from '../ProjectSelector';
|
||||
import './StatusBar.css';
|
||||
@@ -8,16 +8,27 @@ export const StatusBar: React.FC = () => {
|
||||
syncStatus,
|
||||
syncConfigured,
|
||||
pendingChanges,
|
||||
posts,
|
||||
media,
|
||||
tasks,
|
||||
selectedPostId,
|
||||
totalPosts,
|
||||
} = useAppStore();
|
||||
|
||||
const [selectedPostStatus, setSelectedPostStatus] = useState<string | null>(null);
|
||||
|
||||
// Fetch selected post status from database
|
||||
useEffect(() => {
|
||||
if (!selectedPostId) {
|
||||
setSelectedPostStatus(null);
|
||||
return;
|
||||
}
|
||||
window.electronAPI?.posts.get(selectedPostId).then(post => {
|
||||
setSelectedPostStatus(post?.status || null);
|
||||
});
|
||||
}, [selectedPostId]);
|
||||
|
||||
const runningTasks = tasks.filter(t => t.status === 'running');
|
||||
const totalPending = pendingChanges.posts + pendingChanges.media;
|
||||
const selectedPost = posts.find(p => p.id === selectedPostId);
|
||||
|
||||
return (
|
||||
<div className="status-bar">
|
||||
@@ -53,16 +64,16 @@ export const StatusBar: React.FC = () => {
|
||||
|
||||
<div className="status-bar-right">
|
||||
{/* Current Post Info */}
|
||||
{selectedPost && (
|
||||
{selectedPostStatus && (
|
||||
<div className="status-bar-item">
|
||||
<span className={`status-dot status-${selectedPost.status}`} />
|
||||
<span>{selectedPost.status}</span>
|
||||
<span className={`status-dot status-${selectedPostStatus}`} />
|
||||
<span>{selectedPostStatus}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="status-bar-item">
|
||||
<span>{totalPosts || posts.length} posts</span>
|
||||
<span>{totalPosts} posts</span>
|
||||
</div>
|
||||
<div className="status-bar-item">
|
||||
<span>{media.length} media</span>
|
||||
|
||||
@@ -6,7 +6,7 @@ const MAX_CHAT_TITLE_LENGTH = 18;
|
||||
|
||||
const getTabTitle = (
|
||||
tab: Tab,
|
||||
posts: { id: string; title: string }[],
|
||||
postTitles: Map<string, string>,
|
||||
media: { id: string; originalName: string }[],
|
||||
chatTitles: Map<string, string>,
|
||||
importDefTitles: Map<string, string>
|
||||
@@ -20,8 +20,7 @@ const getTabTitle = (
|
||||
}
|
||||
|
||||
if (tab.type === 'post') {
|
||||
const post = posts.find(p => p.id === tab.id);
|
||||
return post?.title || 'Untitled';
|
||||
return postTitles.get(tab.id) || 'Loading...';
|
||||
}
|
||||
|
||||
if (tab.type === 'media') {
|
||||
@@ -116,7 +115,6 @@ export const TabBar: React.FC = () => {
|
||||
const {
|
||||
tabs,
|
||||
activeTabId,
|
||||
posts,
|
||||
media,
|
||||
dirtyPosts,
|
||||
sidebarVisible,
|
||||
@@ -129,9 +127,59 @@ export const TabBar: React.FC = () => {
|
||||
const tabsContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
||||
const [showRightArrow, setShowRightArrow] = useState(false);
|
||||
const [postTitles, setPostTitles] = useState<Map<string, string>>(new Map());
|
||||
const [chatTitles, setChatTitles] = useState<Map<string, string>>(new Map());
|
||||
const [importDefTitles, setImportDefTitles] = useState<Map<string, string>>(new Map());
|
||||
|
||||
// Fetch post titles from database for post tabs
|
||||
useEffect(() => {
|
||||
const postTabs = tabs.filter(t => t.type === 'post');
|
||||
if (postTabs.length === 0) return;
|
||||
|
||||
const fetchTitles = async () => {
|
||||
const newTitles = new Map(postTitles);
|
||||
let changed = false;
|
||||
|
||||
for (const tab of postTabs) {
|
||||
if (!postTitles.has(tab.id)) {
|
||||
try {
|
||||
const post = await window.electronAPI?.posts.get(tab.id);
|
||||
if (post) {
|
||||
newTitles.set(tab.id, post.title || 'Untitled');
|
||||
changed = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch post title:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
setPostTitles(newTitles);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTitles();
|
||||
}, [tabs]); // Note: intentionally not including postTitles to avoid infinite loops
|
||||
|
||||
// Listen for post updates to refresh titles
|
||||
useEffect(() => {
|
||||
const unsub = window.electronAPI?.on('post-updated', (...args: unknown[]) => {
|
||||
const post = args[0] as { id: string; title: string } | undefined;
|
||||
if (post) {
|
||||
setPostTitles(prev => {
|
||||
const newTitles = new Map(prev);
|
||||
newTitles.set(post.id, post.title || 'Untitled');
|
||||
return newTitles;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsub?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch chat titles for chat tabs
|
||||
useEffect(() => {
|
||||
const chatTabs = tabs.filter(t => t.type === 'chat');
|
||||
@@ -349,7 +397,7 @@ export const TabBar: React.FC = () => {
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.id === activeTabId;
|
||||
const isDirty = tab.type === 'post' && dirtyPosts.has(tab.id);
|
||||
const title = getTabTitle(tab, posts, media, chatTitles, importDefTitles);
|
||||
const title = getTabTitle(tab, postTitles, media, chatTitles, importDefTitles);
|
||||
const icon = getTabIcon(tab);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user