fix: editor-preview looks at draft again
This commit is contained in:
@@ -179,6 +179,8 @@ export class PreviewServer {
|
||||
const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
|
||||
const requestTheme = sanitizePicoTheme(requestUrl.searchParams.get('theme'));
|
||||
const previewThemeMode = sanitizePicoThemeMode(requestUrl.searchParams.get('mode'));
|
||||
const useDraftContent = requestUrl.searchParams.get('draft') === 'true';
|
||||
const draftPostId = requestUrl.searchParams.get('postId') || undefined;
|
||||
const appliedTheme = requestTheme ?? sanitizePicoTheme((metadata as { picoTheme?: unknown } | null)?.picoTheme);
|
||||
const picoStylesheetHref = getPicoStylesheetHref(appliedTheme);
|
||||
const htmlRewriteContext = await this.buildHtmlRewriteContext();
|
||||
@@ -218,7 +220,10 @@ export class PreviewServer {
|
||||
language,
|
||||
picoStylesheetHref,
|
||||
htmlThemeAttribute: undefined,
|
||||
}, categorySettings, listExcludedCategories);
|
||||
}, categorySettings, listExcludedCategories, {
|
||||
useDraftContent,
|
||||
draftPostId,
|
||||
});
|
||||
if (!result) {
|
||||
const notFoundHtml = await this.pageRenderer.renderNotFound({
|
||||
page_title: '404 Not Found',
|
||||
@@ -244,6 +249,7 @@ export class PreviewServer {
|
||||
pageContext: { pageTitle: string; language: string; picoStylesheetHref: string; htmlThemeAttribute?: string },
|
||||
categorySettings: Record<string, CategoryRenderSettings>,
|
||||
listExcludedCategories: string[],
|
||||
singlePostOptions?: { useDraftContent?: boolean; draftPostId?: string },
|
||||
): Promise<string | null> {
|
||||
const routePagination = parseRoutePagination(pathname);
|
||||
if (!routePagination) {
|
||||
@@ -263,7 +269,7 @@ export class PreviewServer {
|
||||
const month = Number(postsYearMonthSlugMatch[2]);
|
||||
const slug = postsYearMonthSlugMatch[3].replace(/\.html?$/i, '');
|
||||
if (month < 1 || month > 12) return null;
|
||||
const post = await this.findPublishedPostBySlug(slug, { year, month: month - 1 });
|
||||
const post = await this.findSinglePostBySlug(slug, singlePostOptions, { year, month: month - 1 });
|
||||
if (!post) return null;
|
||||
return this.pageRenderer.renderSinglePost(post, rewriteContext, {
|
||||
page_title: pageContext.pageTitle,
|
||||
@@ -276,7 +282,7 @@ export class PreviewServer {
|
||||
const postsSlugMatch = pagedPathname.match(/^\/posts\/([^/]+)$/);
|
||||
if (postsSlugMatch) {
|
||||
const slug = postsSlugMatch[1].replace(/\.html?$/i, '');
|
||||
const post = await this.findPublishedPostBySlug(slug);
|
||||
const post = await this.findSinglePostBySlug(slug, singlePostOptions);
|
||||
if (!post) return null;
|
||||
return this.pageRenderer.renderSinglePost(post, rewriteContext, {
|
||||
page_title: pageContext.pageTitle,
|
||||
@@ -292,7 +298,7 @@ export class PreviewServer {
|
||||
const month = Number(legacyPostsYearMonthSlugMatch[2]);
|
||||
const slug = legacyPostsYearMonthSlugMatch[3].replace(/\.html?$/i, '');
|
||||
if (month < 1 || month > 12) return null;
|
||||
const post = await this.findPublishedPostBySlug(slug, { year, month: month - 1 });
|
||||
const post = await this.findSinglePostBySlug(slug, singlePostOptions, { year, month: month - 1 });
|
||||
if (!post) return null;
|
||||
return this.pageRenderer.renderSinglePost(post, rewriteContext, {
|
||||
page_title: pageContext.pageTitle,
|
||||
@@ -305,7 +311,7 @@ export class PreviewServer {
|
||||
const legacyPostsSlugMatch = pagedPathname.match(/^\/post\/([^/]+)$/);
|
||||
if (legacyPostsSlugMatch) {
|
||||
const slug = legacyPostsSlugMatch[1].replace(/\.html?$/i, '');
|
||||
const post = await this.findPublishedPostBySlug(slug);
|
||||
const post = await this.findSinglePostBySlug(slug, singlePostOptions);
|
||||
if (!post) return null;
|
||||
return this.pageRenderer.renderSinglePost(post, rewriteContext, {
|
||||
page_title: pageContext.pageTitle,
|
||||
@@ -373,8 +379,7 @@ export class PreviewServer {
|
||||
const month = Number(daySlugMatch[2]);
|
||||
const day = Number(daySlugMatch[3]);
|
||||
const slug = daySlugMatch[4];
|
||||
const posts = await this.loadPostsForDay(year, month, day);
|
||||
const post = posts.find((candidate) => candidate.slug === slug) || null;
|
||||
const post = await this.findSinglePostBySlug(slug, singlePostOptions, { year, month: month - 1, day });
|
||||
if (!post) return null;
|
||||
return this.pageRenderer.renderSinglePost(post, rewriteContext, {
|
||||
page_title: pageContext.pageTitle,
|
||||
@@ -510,6 +515,31 @@ export class PreviewServer {
|
||||
return match;
|
||||
}
|
||||
|
||||
private async findSinglePostBySlug(
|
||||
slug: string,
|
||||
singlePostOptions?: { useDraftContent?: boolean; draftPostId?: string },
|
||||
dateFilter?: { year: number; month: number; day?: number },
|
||||
): Promise<PostData | null> {
|
||||
if (singlePostOptions?.useDraftContent && singlePostOptions.draftPostId) {
|
||||
const draftCandidate = await this.postEngine.getPost(singlePostOptions.draftPostId);
|
||||
if (draftCandidate && draftCandidate.status === 'draft' && draftCandidate.slug === slug) {
|
||||
if (!dateFilter) {
|
||||
return draftCandidate;
|
||||
}
|
||||
|
||||
const sameYear = draftCandidate.createdAt.getFullYear() === dateFilter.year;
|
||||
const sameMonth = draftCandidate.createdAt.getMonth() === dateFilter.month;
|
||||
const sameDay = dateFilter.day === undefined || draftCandidate.createdAt.getDate() === dateFilter.day;
|
||||
if (sameYear && sameMonth && sameDay) {
|
||||
return draftCandidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackDateFilter = dateFilter ? { year: dateFilter.year, month: dateFilter.month } : undefined;
|
||||
return this.findPublishedPostBySlug(slug, fallbackDateFilter);
|
||||
}
|
||||
|
||||
private async loadPostsForDay(
|
||||
year: number,
|
||||
month: number,
|
||||
|
||||
@@ -346,7 +346,7 @@ export function registerIpcHandlers(): void {
|
||||
return engine.getPost(id);
|
||||
});
|
||||
|
||||
safeHandle('posts:getPreviewUrl', async (_, id: string) => {
|
||||
safeHandle('posts:getPreviewUrl', async (_, id: string, options?: { draft?: boolean }) => {
|
||||
const engine = getPostEngine();
|
||||
const post = await engine.getPost(id);
|
||||
|
||||
@@ -356,6 +356,10 @@ export function registerIpcHandlers(): void {
|
||||
|
||||
const createdAt = resolvePostCreatedAt(post);
|
||||
const canonicalPath = buildCanonicalPreviewPath(createdAt, post.slug);
|
||||
if (options?.draft) {
|
||||
return `http://127.0.0.1:4123${canonicalPath}?draft=true&postId=${encodeURIComponent(id)}`;
|
||||
}
|
||||
|
||||
return `http://127.0.0.1:4123${canonicalPath}`;
|
||||
});
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export const electronAPI: ElectronAPI = {
|
||||
update: (id: string, data: unknown) => ipcRenderer.invoke('posts:update', id, data),
|
||||
delete: (id: string) => ipcRenderer.invoke('posts:delete', id),
|
||||
get: (id: string) => ipcRenderer.invoke('posts:get', id),
|
||||
getPreviewUrl: (id: string) => ipcRenderer.invoke('posts:getPreviewUrl', id),
|
||||
getPreviewUrl: (id: string, options?: { draft?: boolean }) => ipcRenderer.invoke('posts:getPreviewUrl', id, options),
|
||||
getAll: (options?: { limit?: number; offset?: number }) => ipcRenderer.invoke('posts:getAll', options),
|
||||
getByStatus: (status: string) => ipcRenderer.invoke('posts:getByStatus', status),
|
||||
publish: (id: string) => ipcRenderer.invoke('posts:publish', id),
|
||||
|
||||
@@ -442,7 +442,7 @@ export interface ElectronAPI {
|
||||
update: (id: string, data: Partial<PostData>) => Promise<PostData | null>;
|
||||
delete: (id: string) => Promise<boolean>;
|
||||
get: (id: string) => Promise<PostData | null>;
|
||||
getPreviewUrl: (id: string) => Promise<string | null>;
|
||||
getPreviewUrl: (id: string, options?: { draft?: boolean }) => Promise<string | null>;
|
||||
getAll: (options?: { limit?: number; offset?: number }) => Promise<PaginatedPostsResult>;
|
||||
getByStatus: (status: string) => Promise<PostData[]>;
|
||||
publish: (id: string) => Promise<PostData | null>;
|
||||
|
||||
@@ -205,7 +205,7 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
let cancelled = false;
|
||||
setPreviewUrl(null);
|
||||
|
||||
window.electronAPI?.posts.getPreviewUrl(postId)
|
||||
window.electronAPI?.posts.getPreviewUrl(postId, { draft: true })
|
||||
.then((url) => {
|
||||
if (!cancelled) {
|
||||
setPreviewUrl(url);
|
||||
|
||||
@@ -299,6 +299,47 @@ describe('PreviewServer', () => {
|
||||
expect(diagonalHtml).toContain('data-orientation="mixed-diagonal"');
|
||||
});
|
||||
|
||||
it('serves draft content for single post route when draft query flag and postId are provided', async () => {
|
||||
const publishedPost = makePost({
|
||||
id: 'post-1',
|
||||
slug: 'shared-slug',
|
||||
title: 'Published Title',
|
||||
content: 'Published body',
|
||||
status: 'published',
|
||||
createdAt: new Date('2025-01-03T10:00:00.000Z'),
|
||||
});
|
||||
const draftPost = makePost({
|
||||
id: 'post-2',
|
||||
slug: 'shared-slug',
|
||||
title: 'Draft Title',
|
||||
content: 'Draft-only body',
|
||||
status: 'draft',
|
||||
createdAt: new Date('2025-01-03T10:00:00.000Z'),
|
||||
});
|
||||
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([publishedPost, draftPost]),
|
||||
settingsEngine: makeSettings(50),
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
await server.start(0);
|
||||
|
||||
const publishedResponse = await fetch(`${server.getBaseUrl()}/posts/shared-slug`);
|
||||
expect(publishedResponse.status).toBe(200);
|
||||
const publishedHtml = await publishedResponse.text();
|
||||
expect(publishedHtml).toContain('Published Title');
|
||||
expect(publishedHtml).toContain('Published body');
|
||||
expect(publishedHtml).not.toContain('Draft-only body');
|
||||
|
||||
const draftResponse = await fetch(`${server.getBaseUrl()}/posts/shared-slug?draft=true&postId=post-2`);
|
||||
expect(draftResponse.status).toBe(200);
|
||||
const draftHtml = await draftResponse.text();
|
||||
expect(draftHtml).toContain('Draft Title');
|
||||
expect(draftHtml).toContain('Draft-only body');
|
||||
expect(draftHtml).not.toContain('Published body');
|
||||
});
|
||||
|
||||
it('uses selected pico theme stylesheet from project metadata', async () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([makePost()]),
|
||||
|
||||
@@ -765,6 +765,19 @@ describe('IPC Handlers', () => {
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return draft preview URL when draft option is enabled', async () => {
|
||||
mockPostEngine.getPost.mockResolvedValue(createMockPost({
|
||||
id: 'post-1',
|
||||
slug: 'my-post',
|
||||
createdAt: new Date('2026-02-16T12:00:00.000Z'),
|
||||
}));
|
||||
|
||||
const result = await invokeHandler('posts:getPreviewUrl', 'post-1', { draft: true });
|
||||
|
||||
expect(mockPostEngine.getPost).toHaveBeenCalledWith('post-1');
|
||||
expect(result).toBe('http://127.0.0.1:4123/2026/02/16/my-post?draft=true&postId=post-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('posts:getAll', () => {
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('Editor visual mode persistence', () => {
|
||||
(window as any).electronAPI.posts.get = vi.fn().mockResolvedValue(createPost());
|
||||
(window as any).electronAPI.posts.hasPublishedVersion = vi.fn().mockReturnValue(neverSettles);
|
||||
(window as any).electronAPI.posts.update = vi.fn().mockResolvedValue(null);
|
||||
(window as any).electronAPI.posts.getPreviewUrl = vi.fn().mockResolvedValue('http://127.0.0.1:4123/2026/02/16/test-post');
|
||||
(window as any).electronAPI.posts.getPreviewUrl = vi.fn().mockResolvedValue('http://127.0.0.1:4123/2026/02/16/test-post?draft=true&postId=post-1');
|
||||
(window as any).electronAPI.meta.getCategories = vi.fn().mockReturnValue(neverSettles);
|
||||
|
||||
useAppStore.setState({
|
||||
@@ -202,7 +202,7 @@ describe('Editor visual mode persistence', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uses canonical preview server URL in preview mode iframe', async () => {
|
||||
it('uses editor preview HTML in preview mode iframe', async () => {
|
||||
const { getByTitle, container } = render(<PostEditor postId="post-1" />);
|
||||
|
||||
await act(async () => {
|
||||
@@ -216,11 +216,12 @@ describe('Editor visual mode persistence', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect((window as any).electronAPI.posts.getPreviewUrl).toHaveBeenCalledWith('post-1');
|
||||
expect((window as any).electronAPI.posts.getPreviewUrl).toHaveBeenCalledWith('post-1', { draft: true });
|
||||
|
||||
const frame = container.querySelector('.editor-preview-frame') as HTMLIFrameElement | null;
|
||||
expect(frame).not.toBeNull();
|
||||
expect(frame?.getAttribute('src')).toBe('http://127.0.0.1:4123/2026/02/16/test-post');
|
||||
expect(frame?.getAttribute('src')).toBe('http://127.0.0.1:4123/2026/02/16/test-post?draft=true&postId=post-1');
|
||||
expect(frame?.getAttribute('srcdoc')).toBeNull();
|
||||
|
||||
expect(container.querySelector('.preview-content')).toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user