fix: importer bugs and editor bugs
This commit is contained in:
@@ -637,6 +637,10 @@ export class ImportExecutionEngine extends EventEmitter {
|
||||
// Unescape double-bracket macros that TurndownService escaped
|
||||
// \[\[ becomes [[ and \]\] becomes ]]
|
||||
markdown = markdown.replace(/\\\[\\\[/g, '[[').replace(/\\\]\\\]/g, ']]');
|
||||
// Remove backslash escapes inside [[macro]] blocks (e.g. photo\_archive → photo_archive)
|
||||
markdown = markdown.replace(/\[\[([^\]]*?)\]\]/g, (_match, inner: string) => {
|
||||
return '[[' + inner.replace(/\\(.)/g, '$1') + ']]';
|
||||
});
|
||||
return markdown;
|
||||
}
|
||||
|
||||
|
||||
@@ -839,6 +839,18 @@ export class MediaEngine extends EventEmitter {
|
||||
return path.join(this.getMediaDir(), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relative path for a media item (e.g. media/2025/01/uuid.jpg).
|
||||
* This is the path format used in markdown content for image references.
|
||||
*/
|
||||
async getRelativePath(id: string): Promise<string | null> {
|
||||
const db = getDatabase().getLocal();
|
||||
const dbMedia = await db.select().from(media).where(eq(media.id, id)).get();
|
||||
if (!dbMedia?.filePath) return null;
|
||||
const dataDir = this.getDataDir();
|
||||
return path.relative(dataDir, dbMedia.filePath);
|
||||
}
|
||||
|
||||
async rebuildDatabaseFromFiles(): Promise<void> {
|
||||
const mediaBaseDir = this.getMediaBaseDir();
|
||||
console.log(`[MediaEngine] rebuildDatabaseFromFiles: scanning mediaBaseDir=${mediaBaseDir}`);
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import matter from 'gray-matter';
|
||||
import { eq, and, desc, gte, lte, like, inArray } from 'drizzle-orm';
|
||||
import { eq, and, desc, gte, lte, like, inArray, ne } from 'drizzle-orm';
|
||||
import { app } from 'electron';
|
||||
import { getDatabase } from '../database';
|
||||
import { posts, Post, NewPost, postLinks } from '../database/schema';
|
||||
@@ -532,19 +532,74 @@ export class PostEngine extends EventEmitter {
|
||||
.all();
|
||||
const total = countResult.length;
|
||||
|
||||
// Drafts must ALWAYS be included regardless of pagination.
|
||||
// On the first page (offset=0), fetch all drafts and fill remaining slots with non-drafts.
|
||||
// On subsequent pages, only paginate non-draft posts (drafts were already returned).
|
||||
if (offset === 0) {
|
||||
// Fetch ALL drafts (typically few)
|
||||
const draftPosts = await db
|
||||
.select()
|
||||
.from(posts)
|
||||
.where(and(
|
||||
eq(posts.projectId, this.currentProjectId),
|
||||
eq(posts.status, 'draft')
|
||||
))
|
||||
.orderBy(desc(posts.createdAt))
|
||||
.all();
|
||||
|
||||
// Fill remaining slots with non-draft posts
|
||||
const remainingSlots = Math.max(0, limit - draftPosts.length);
|
||||
const nonDraftPosts = remainingSlots > 0 ? await db
|
||||
.select()
|
||||
.from(posts)
|
||||
.where(and(
|
||||
eq(posts.projectId, this.currentProjectId),
|
||||
ne(posts.status, 'draft')
|
||||
))
|
||||
.orderBy(desc(posts.createdAt))
|
||||
.limit(remainingSlots)
|
||||
.all() : [];
|
||||
|
||||
const allDbPosts = [...draftPosts, ...nonDraftPosts];
|
||||
const items: PostData[] = allDbPosts.map(dbPost =>
|
||||
this.dbRowToPostData(dbPost, dbPost.content || '')
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
hasMore: allDbPosts.length < total,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
// Subsequent pages: only paginate non-draft posts
|
||||
// Count drafts to calculate correct offset into non-draft posts
|
||||
const draftCount = await db
|
||||
.select({ count: posts.id })
|
||||
.from(posts)
|
||||
.where(and(
|
||||
eq(posts.projectId, this.currentProjectId),
|
||||
eq(posts.status, 'draft')
|
||||
))
|
||||
.all();
|
||||
const numDrafts = draftCount.length;
|
||||
|
||||
// Adjust offset: the first page returned numDrafts + (limit - numDrafts) non-draft posts
|
||||
// So for page 2+, offset into non-draft posts = offset - numDrafts
|
||||
const nonDraftOffset = offset - numDrafts;
|
||||
const dbPosts = await db
|
||||
.select()
|
||||
.from(posts)
|
||||
.where(eq(posts.projectId, this.currentProjectId))
|
||||
.where(and(
|
||||
eq(posts.projectId, this.currentProjectId),
|
||||
ne(posts.status, 'draft')
|
||||
))
|
||||
.orderBy(desc(posts.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.offset(nonDraftOffset)
|
||||
.all();
|
||||
|
||||
// For listing, we don't need to load content from filesystem.
|
||||
// Use DB content for drafts, empty string for published posts.
|
||||
// This avoids expensive filesystem reads for each post.
|
||||
const items: PostData[] = dbPosts.map(dbPost =>
|
||||
|
||||
const items: PostData[] = dbPosts.map(dbPost =>
|
||||
this.dbRowToPostData(dbPost, dbPost.content || '')
|
||||
);
|
||||
|
||||
|
||||
@@ -336,8 +336,11 @@ export function registerIpcHandlers(): void {
|
||||
});
|
||||
|
||||
safeHandle('media:getUrl', async (_, id: string) => {
|
||||
// Returns the bds-media:// protocol URL for a media item
|
||||
return `bds-media://${id}`;
|
||||
// Returns the relative path for a media item (e.g. media/2025/01/uuid.jpg)
|
||||
// This is the format used in markdown content for image references
|
||||
const engine = getMediaEngine();
|
||||
const relativePath = await engine.getRelativePath(id);
|
||||
return relativePath ?? `media/${id}`;
|
||||
});
|
||||
|
||||
safeHandle('media:getFilePath', async (_, id: string) => {
|
||||
|
||||
@@ -42,6 +42,8 @@ const autoSaveManager = new AutoSaveManager({
|
||||
if (updated) {
|
||||
useAppStore.getState().updatePost(id, updated as Partial<PostData>);
|
||||
useAppStore.getState().markClean(id);
|
||||
// Emit event so PostEditor can update its local state
|
||||
window.dispatchEvent(new CustomEvent('bds:post-auto-saved', { detail: { id, updated } }));
|
||||
}
|
||||
},
|
||||
onSaveComplete: (id) => {
|
||||
@@ -670,6 +672,18 @@ const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
|
||||
const isDirty = checkIsDirty(postId);
|
||||
|
||||
// Listen for auto-save events to keep local post state in sync
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const { id, updated } = (e as CustomEvent).detail;
|
||||
if (id === postId && updated) {
|
||||
setPost(prev => prev ? { ...prev, ...updated as Partial<PostData> } : prev);
|
||||
}
|
||||
};
|
||||
window.addEventListener('bds:post-auto-saved', handler);
|
||||
return () => window.removeEventListener('bds:post-auto-saved', handler);
|
||||
}, [postId]);
|
||||
|
||||
// Check if post has a published version for discard functionality
|
||||
useEffect(() => {
|
||||
window.electronAPI?.posts.hasPublishedVersion(postId).then(setHasPublishedVersion);
|
||||
@@ -764,6 +778,7 @@ const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
if (updated) {
|
||||
useAppStore.getState().updatePost(pending.postId, updated as Partial<PostData>);
|
||||
useAppStore.getState().markClean(pending.postId);
|
||||
window.dispatchEvent(new CustomEvent('bds:post-auto-saved', { detail: { id: pending.postId, updated } }));
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Auto-save failed:', error);
|
||||
@@ -835,6 +850,7 @@ const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
|
||||
if (updated) {
|
||||
updatePost(postId, updated as Partial<PostData>);
|
||||
setPost(prev => prev ? { ...prev, ...updated as Partial<PostData> } : prev);
|
||||
markClean(postId);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -856,6 +872,7 @@ const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
const updated = await window.electronAPI?.posts.publish(postId);
|
||||
if (updated) {
|
||||
updatePost(postId, updated as Partial<PostData>);
|
||||
setPost(prev => prev ? { ...prev, ...updated as Partial<PostData> } : prev);
|
||||
showToast.success('Post published');
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user