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