feat: categories have settings for filtering and titles

This commit is contained in:
2026-02-20 21:10:15 +01:00
parent eeffa247bb
commit 63c4b148e1
15 changed files with 661 additions and 53 deletions

View File

@@ -423,10 +423,10 @@ describe('MetaEngine', () => {
});
const metadata = await metaEngine.getProjectMetadata();
expect(metadata).toEqual({
expect(metadata).toEqual(expect.objectContaining({
name: 'My Blog',
description: 'A personal blog about technology',
});
}));
});
it('should update project name only', async () => {
@@ -593,6 +593,70 @@ describe('MetaEngine', () => {
expect(parsed.picoTheme).toBe('slate');
});
it('should apply default category settings for standard categories', async () => {
await metaEngine.setProjectMetadata({
name: 'Category Defaults Project',
} as any);
const metadata = await metaEngine.getProjectMetadata() as any;
expect(metadata.categorySettings).toEqual(
expect.objectContaining({
article: { renderInLists: true, showTitle: true },
picture: { renderInLists: true, showTitle: true },
aside: { renderInLists: true, showTitle: false },
page: { renderInLists: false, showTitle: true },
})
);
});
it('should persist category settings to project.json', async () => {
await metaEngine.setProjectMetadata({
name: 'Persisted Category Settings',
categorySettings: {
article: { renderInLists: true, showTitle: true },
aside: { renderInLists: true, showTitle: false },
page: { renderInLists: false, showTitle: true },
custom: { renderInLists: false, showTitle: true },
},
} as any);
const metaDir = metaEngine.getMetaDir();
const projectPath = normalizePath(`${metaDir}/project.json`);
const content = mockFiles.get(projectPath);
const parsed = JSON.parse(content!);
expect(parsed.categorySettings).toEqual(
expect.objectContaining({
custom: { renderInLists: false, showTitle: true },
aside: { renderInLists: true, showTitle: false },
page: { renderInLists: false, showTitle: true },
})
);
});
it('should merge missing category settings with defaults when loading from filesystem', async () => {
const metaDir = metaEngine.getMetaDir();
const projectPath = normalizePath(`${metaDir}/project.json`);
mockFiles.set(projectPath, JSON.stringify({
name: 'Loaded Project',
categorySettings: {
custom: { renderInLists: false, showTitle: false },
},
}));
await metaEngine.loadProjectMetadata();
const metadata = await metaEngine.getProjectMetadata() as any;
expect(metadata.categorySettings).toEqual(
expect.objectContaining({
custom: { renderInLists: false, showTitle: false },
article: { renderInLists: true, showTitle: true },
aside: { renderInLists: true, showTitle: false },
page: { renderInLists: false, showTitle: true },
})
);
});
it('should load picoTheme from filesystem', async () => {
const metaDir = metaEngine.getMetaDir();
const projectPath = normalizePath(`${metaDir}/project.json`);
@@ -691,10 +755,10 @@ describe('MetaEngine', () => {
description: 'Testing events',
});
expect(handler).toHaveBeenCalledWith({
expect(handler).toHaveBeenCalledWith(expect.objectContaining({
name: 'Event Test',
description: 'Testing events',
});
}));
});
it('should clear project metadata when project context changes', () => {

View File

@@ -65,6 +65,10 @@ function makeEngine(posts: PostData[]): PostEngineLike {
result = result.filter((post) => filter.categories!.some((category) => post.categories.includes(category)));
}
if ((filter as any).excludeCategories && (filter as any).excludeCategories.length > 0) {
result = result.filter((post) => !(filter as any).excludeCategories.some((category: string) => post.categories.includes(category)));
}
if (filter.year !== undefined) {
result = result.filter((post) => post.createdAt.getUTCFullYear() === filter.year);
}
@@ -519,6 +523,108 @@ describe('PreviewServer', () => {
expect(secondPageHtml).toContain('<h1 class="archive-heading">news - 1.1.2020 - 2.1.2020</h1>');
});
it('filters out categories disabled for list rendering on list routes', async () => {
const posts = [
makePost({ id: 'list-1', slug: 'list-1', title: 'List Included', categories: ['article'], createdAt: new Date('2025-02-05T10:00:00.000Z') }),
makePost({ id: 'list-2', slug: 'list-2', title: 'List Excluded', categories: ['page'], createdAt: new Date('2025-02-04T10:00:00.000Z') }),
];
server = new PreviewServer({
postEngine: makeEngine(posts),
settingsEngine: {
setProjectContext: vi.fn(),
async getProjectMetadata() {
return {
maxPostsPerPage: 50,
categorySettings: {
article: { renderInLists: true, showTitle: true },
page: { renderInLists: false, showTitle: true },
},
};
},
} as any,
getActiveProjectContext: async () => ({ projectId: 'default' }),
});
await server.start(0);
const html = await (await fetch(`${server.getBaseUrl()}/`)).text();
expect(html).toContain('List Included');
expect(html).not.toContain('List Excluded');
});
it('suppresses all list category titles when any assigned category has showTitle disabled', async () => {
const posts = [
makePost({
id: 'ct-1',
slug: 'ct-1',
title: 'Category Title Test',
categories: ['aside', 'article'],
content: 'Body without markdown headings',
createdAt: new Date('2025-02-05T10:00:00.000Z'),
}),
];
server = new PreviewServer({
postEngine: makeEngine(posts),
settingsEngine: {
setProjectContext: vi.fn(),
async getProjectMetadata() {
return {
maxPostsPerPage: 50,
categorySettings: {
article: { renderInLists: true, showTitle: true },
aside: { renderInLists: true, showTitle: false },
},
};
},
} as any,
getActiveProjectContext: async () => ({ projectId: 'default' }),
});
await server.start(0);
const html = await (await fetch(`${server.getBaseUrl()}/`)).text();
expect(html).not.toContain('<h2 class="post-title">Category Title Test</h2>');
});
it('renders post title in list when category titles are enabled', async () => {
const posts = [
makePost({
id: 'pt-1',
slug: 'pt-1',
title: 'Article Title',
categories: ['article'],
content: 'Body without markdown headings',
createdAt: new Date('2025-02-06T10:00:00.000Z'),
}),
];
server = new PreviewServer({
postEngine: makeEngine(posts),
settingsEngine: {
setProjectContext: vi.fn(),
async getProjectMetadata() {
return {
maxPostsPerPage: 50,
categorySettings: {
article: { renderInLists: true, showTitle: true },
aside: { renderInLists: true, showTitle: false },
page: { renderInLists: false, showTitle: true },
},
};
},
} as any,
getActiveProjectContext: async () => ({ projectId: 'default' }),
});
await server.start(0);
const html = await (await fetch(`${server.getBaseUrl()}/`)).text();
expect(html).toContain('<h2 class="post-title">Article Title</h2>');
expect(html).not.toContain('<h2 class="post-category-title">article</h2>');
});
it('supports tag, category, and page-slug routes', async () => {
const tagged = makePost({ id: 'tag1', title: 'Tagged', slug: 'tagged', tags: ['dev'] });
const categorized = makePost({ id: 'cat1', title: 'Categorized', slug: 'categorized', categories: ['news'] });
@@ -982,7 +1088,7 @@ describe('PreviewServer', () => {
slug: 'published-slug',
content: '# Published content only',
tags: ['published-tag'],
categories: ['page'],
categories: ['article'],
createdAt: new Date('2025-02-14T10:00:00.000Z'),
});

View File

@@ -43,7 +43,16 @@ describe('SettingsView Diff Preferences', () => {
meta: {
...(window as any).electronAPI?.meta,
getCategories: vi.fn().mockResolvedValue(['article', 'picture', 'aside', 'page']),
getProjectMetadata: vi.fn().mockResolvedValue({ maxPostsPerPage: 75, publicUrl: 'https://example.com' }),
getProjectMetadata: vi.fn().mockResolvedValue({
maxPostsPerPage: 75,
publicUrl: 'https://example.com',
categorySettings: {
article: { renderInLists: true, showTitle: true },
picture: { renderInLists: true, showTitle: true },
aside: { renderInLists: true, showTitle: false },
page: { renderInLists: false, showTitle: true },
},
}),
updateProjectMetadata: vi.fn().mockResolvedValue({ maxPostsPerPage: 12, publicUrl: 'https://example.com' }),
},
chat: {
@@ -107,4 +116,35 @@ describe('SettingsView Diff Preferences', () => {
expect.objectContaining({ publicUrl: 'https://example.com' })
);
});
it('renders category settings checkboxes with required defaults', async () => {
render(<SettingsView />);
const asideShowTitle = await screen.findByLabelText(/aside show titles/i);
const asideRenderInLists = screen.getByLabelText(/aside render in lists/i);
const pageRenderInLists = screen.getByLabelText(/page render in lists/i);
const articleShowTitle = screen.getByLabelText(/article show titles/i);
expect((asideShowTitle as HTMLInputElement).checked).toBe(false);
expect((asideRenderInLists as HTMLInputElement).checked).toBe(true);
expect((pageRenderInLists as HTMLInputElement).checked).toBe(false);
expect((articleShowTitle as HTMLInputElement).checked).toBe(true);
});
it('persists category settings changes via project metadata update', async () => {
render(<SettingsView />);
const pageRenderInLists = await screen.findByLabelText(/page render in lists/i);
fireEvent.click(pageRenderInLists);
await new Promise((resolve) => setTimeout(resolve, 0));
expect((window as any).electronAPI.meta.updateProjectMetadata).toHaveBeenCalledWith(
expect.objectContaining({
categorySettings: expect.objectContaining({
page: expect.objectContaining({ renderInLists: true, showTitle: true }),
}),
})
);
});
});