fix: pages feature now works properly
This commit is contained in:
@@ -747,7 +747,7 @@ export class PostEngine extends EventEmitter {
|
||||
|
||||
// Search the stemmed content, filtered by project_id for project isolation
|
||||
const result = await client.execute({
|
||||
sql: `SELECT id FROM posts_fts WHERE project_id = ? AND posts_fts MATCH ? ORDER BY rank LIMIT 50`,
|
||||
sql: `SELECT id FROM posts_fts WHERE project_id = ? AND posts_fts MATCH ? ORDER BY rank LIMIT 500`,
|
||||
args: [this.currentProjectId, stemmedQuery],
|
||||
});
|
||||
|
||||
|
||||
@@ -483,12 +483,14 @@ type PostsListMode = 'posts' | 'pages';
|
||||
|
||||
interface PostsListProps {
|
||||
mode: PostsListMode;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const PostsList: React.FC<PostsListProps> = ({ mode }) => {
|
||||
const PostsList: React.FC<PostsListProps> = ({ mode, isActive }) => {
|
||||
const { posts, hasMorePosts, totalPosts, appendPosts, openTab, activeTabId } = useAppStore();
|
||||
const isPagesMode = mode === 'pages';
|
||||
const postSubset = useMemo(() => applyPageFilter(posts, isPagesMode), [posts, isPagesMode]);
|
||||
const [pageBasePosts, setPageBasePosts] = useState<PostData[] | null>(null);
|
||||
|
||||
// Filter state
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -534,6 +536,35 @@ const PostsList: React.FC<PostsListProps> = ({ mode }) => {
|
||||
loadFilters();
|
||||
}, [posts]);
|
||||
|
||||
// In pages mode, load the full pages subset from backend filtering,
|
||||
// independent of currently paged post list in the store.
|
||||
useEffect(() => {
|
||||
if (!isPagesMode || !isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isCancelled = false;
|
||||
|
||||
const loadPagesBase = async () => {
|
||||
try {
|
||||
const results = await window.electronAPI?.posts.filter({ categories: [PAGE_CATEGORY] });
|
||||
if (!isCancelled && results) {
|
||||
setPageBasePosts(results as PostData[]);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCancelled) {
|
||||
console.error('Failed to load pages subset:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadPagesBase();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [isPagesMode, isActive, posts]);
|
||||
|
||||
// Handle search
|
||||
const handleSearch = async (query: string) => {
|
||||
setSearchQuery(query);
|
||||
@@ -725,15 +756,17 @@ const PostsList: React.FC<PostsListProps> = ({ mode }) => {
|
||||
|
||||
// Determine which posts to display
|
||||
// Filters only apply to published/archived posts — drafts are always shown unfiltered
|
||||
const filteredDisplayPosts = searchResults ?? filteredPosts ?? null;
|
||||
const filteredDisplayPosts = searchResults ?? filteredPosts ?? (isPagesMode ? pageBasePosts : null);
|
||||
const isFiltered = filteredDisplayPosts !== null;
|
||||
const hasActiveFilters = searchQuery || selectedYear || selectedTags.length > 0 || selectedCategories.length > 0;
|
||||
|
||||
const baseDisplayPosts = isPagesMode ? (pageBasePosts ?? postSubset) : postSubset;
|
||||
|
||||
// Memoized grouping that freshens cached filter results with current store data
|
||||
// This ensures status changes are reflected even when filters are active
|
||||
const groupedPosts = useMemo(
|
||||
() => groupPostsByStatus(postSubset, filteredDisplayPosts),
|
||||
[postSubset, filteredDisplayPosts]
|
||||
() => groupPostsByStatus(baseDisplayPosts, filteredDisplayPosts),
|
||||
[baseDisplayPosts, filteredDisplayPosts]
|
||||
);
|
||||
|
||||
const clearAllFilters = () => {
|
||||
@@ -896,7 +929,7 @@ const PostsList: React.FC<PostsListProps> = ({ mode }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{postSubset.length === 0 && !isFiltered && (
|
||||
{baseDisplayPosts.length === 0 && !isFiltered && (
|
||||
<div className="sidebar-empty">
|
||||
<p>{isPagesMode ? 'No pages yet' : 'No posts yet'}</p>
|
||||
<button onClick={handleCreatePost}>Create your first post</button>
|
||||
@@ -1582,11 +1615,25 @@ export const Sidebar: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<div style={{ display: activeView === 'posts' ? 'block' : 'none' }}>
|
||||
<PostsList mode="posts" />
|
||||
<div
|
||||
style={{
|
||||
display: activeView === 'posts' ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
<PostsList mode="posts" isActive={activeView === 'posts'} />
|
||||
</div>
|
||||
<div style={{ display: activeView === 'pages' ? 'block' : 'none' }}>
|
||||
<PostsList mode="pages" />
|
||||
<div
|
||||
style={{
|
||||
display: activeView === 'pages' ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
<PostsList mode="pages" isActive={activeView === 'pages'} />
|
||||
</div>
|
||||
{activeView === 'media' && <MediaList />}
|
||||
{activeView === 'settings' && <SettingsNav />}
|
||||
|
||||
@@ -2175,6 +2175,19 @@ Published content`);
|
||||
const result = await postEngine.searchPosts('nonexistent');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should cap search results at 500', async () => {
|
||||
mockLocalClient.execute.mockResolvedValueOnce({ rows: [] });
|
||||
|
||||
await postEngine.searchPosts('term');
|
||||
|
||||
expect(mockLocalClient.execute).toHaveBeenCalled();
|
||||
const call = vi.mocked(mockLocalClient.execute).mock.calls[0]?.[0] as { sql?: string } | undefined;
|
||||
expect(call?.sql).toBeDefined();
|
||||
const sql = call?.sql?.toLowerCase() ?? '';
|
||||
expect(sql).toContain('limit 500');
|
||||
expect(sql).not.toMatch(/\blimit\s+50\b/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagsWithCounts', () => {
|
||||
|
||||
@@ -77,6 +77,9 @@ describe('Pages shortcut UI', () => {
|
||||
|
||||
it('shows only page-category posts when pages view is active', async () => {
|
||||
useAppStore.setState({ activeView: 'pages', sidebarVisible: true });
|
||||
window.electronAPI.posts.filter = vi.fn().mockResolvedValue([
|
||||
createMockPost({ id: 'post-page', title: 'About Page', categories: ['page'] }),
|
||||
]);
|
||||
|
||||
render(<Sidebar />);
|
||||
|
||||
@@ -87,4 +90,70 @@ describe('Pages shortcut UI', () => {
|
||||
expect(within(pagesPanel as HTMLElement).getByText('About Page')).toBeInTheDocument();
|
||||
expect(within(pagesPanel as HTMLElement).queryByText('Regular Article')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('loads pages subset from full table and does not require load-more pagination', async () => {
|
||||
useAppStore.setState({
|
||||
activeView: 'pages',
|
||||
sidebarVisible: true,
|
||||
posts: [
|
||||
createMockPost({
|
||||
id: 'post-article-only',
|
||||
title: 'Loaded Article',
|
||||
categories: ['article'],
|
||||
}),
|
||||
],
|
||||
hasMorePosts: true,
|
||||
totalPosts: 1200,
|
||||
});
|
||||
|
||||
window.electronAPI.posts.filter = vi.fn().mockResolvedValue([
|
||||
createMockPost({ id: 'post-page-remote', title: 'Remote Page', categories: ['page'] }),
|
||||
]);
|
||||
|
||||
render(<Sidebar />);
|
||||
|
||||
const pagesHeader = await screen.findByText('PAGES');
|
||||
const pagesPanel = pagesHeader.closest('.sidebar-content') as HTMLElement;
|
||||
|
||||
expect(within(pagesPanel).getByText('Remote Page')).toBeInTheDocument();
|
||||
expect(within(pagesPanel).queryByText('Loaded Article')).not.toBeInTheDocument();
|
||||
expect(within(pagesPanel).queryByText(/Load more/i)).not.toBeInTheDocument();
|
||||
expect(window.electronAPI.posts.filter).toHaveBeenCalledWith({ categories: ['page'] });
|
||||
});
|
||||
|
||||
it('does not prefetch pages subset while posts view is active', async () => {
|
||||
useAppStore.setState({
|
||||
activeView: 'posts',
|
||||
sidebarVisible: true,
|
||||
posts: [
|
||||
createMockPost({ id: 'post-1', title: 'Loaded Post', categories: ['article'] }),
|
||||
],
|
||||
hasMorePosts: true,
|
||||
totalPosts: 1200,
|
||||
});
|
||||
|
||||
window.electronAPI.posts.filter = vi.fn().mockResolvedValue([
|
||||
createMockPost({ id: 'post-page-remote', title: 'Remote Page', categories: ['page'] }),
|
||||
]);
|
||||
|
||||
render(<Sidebar />);
|
||||
|
||||
expect(await screen.findByText('POSTS')).toBeInTheDocument();
|
||||
expect(window.electronAPI.posts.filter).not.toHaveBeenCalledWith({ categories: ['page'] });
|
||||
});
|
||||
|
||||
it('uses a flex-height wrapper for active posts/pages sidebar view', async () => {
|
||||
useAppStore.setState({
|
||||
activeView: 'posts',
|
||||
sidebarVisible: true,
|
||||
posts: [createMockPost({ id: 'post-1', title: 'Loaded Post', categories: ['article'] })],
|
||||
});
|
||||
|
||||
const { container } = render(<Sidebar />);
|
||||
expect(await screen.findByText('POSTS')).toBeInTheDocument();
|
||||
|
||||
const wrappers = container.querySelectorAll('.sidebar > div');
|
||||
expect(wrappers.length).toBeGreaterThanOrEqual(2);
|
||||
expect((wrappers[0] as HTMLElement).style.display).toBe('flex');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user