From ee6972113ad7766daa6f966aec8a7fbc14156f16 Mon Sep 17 00:00:00 2001 From: Luca Rossi Date: Mon, 2 Mar 2026 02:01:21 +0100 Subject: [PATCH] feat: allow custom sidebar label for type sections (#165) * feat: allow custom sidebar label for type sections - Adds sidebarLabel field to vault type config (Rust + frontend) - Overrides auto-pluralization with user-defined label in sidebar - Tests updated to cover custom label display * fix: add missing sidebarLabel field to buildNewEntry and bulk mock generator --------- Co-authored-by: Test Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src-tauri/src/vault/mod.rs | 33 +++++++++++++++++ src/components/Sidebar.test.tsx | 65 +++++++++++++++++++++++++++++++-- src/components/Sidebar.tsx | 5 ++- src/hooks/useNoteActions.ts | 2 +- src/mock-tauri/mock-entries.ts | 39 +++++++++++++++++++- src/types.ts | 2 + 6 files changed, 138 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/vault/mod.rs b/src-tauri/src/vault/mod.rs index 2b3357a8..d61bc5f2 100644 --- a/src-tauri/src/vault/mod.rs +++ b/src-tauri/src/vault/mod.rs @@ -62,6 +62,9 @@ pub struct VaultEntry { pub color: Option, /// Display order for Type entries in sidebar (lower = higher). None = use default order. pub order: Option, + /// Custom sidebar section label for Type entries, overriding auto-pluralization. + #[serde(rename = "sidebarLabel")] + pub sidebar_label: Option, /// Word count of the note body (excludes frontmatter and H1 title). #[serde(rename = "wordCount")] pub word_count: u32, @@ -104,6 +107,8 @@ struct Frontmatter { color: Option, #[serde(default)] order: Option, + #[serde(rename = "sidebar label", default)] + sidebar_label: Option, } /// Handles YAML fields that can be either a single string or a list of strings. @@ -147,6 +152,7 @@ const SKIP_KEYS: &[&str] = &[ "icon", "color", "order", + "sidebar label", ]; /// Extract all wikilink-containing fields from raw YAML frontmatter. @@ -325,6 +331,7 @@ pub fn parse_md_file(path: &Path) -> Result { icon: frontmatter.icon, color: frontmatter.color, order: frontmatter.order, + sidebar_label: frontmatter.sidebar_label, word_count, outgoing_links, }) @@ -1055,6 +1062,32 @@ References: assert_eq!(fs::read_to_string(&path).unwrap(), content); } + // --- sidebar_label tests --- + + #[test] + fn test_parse_sidebar_label_from_type_entry() { + let dir = TempDir::new().unwrap(); + let content = "---\ntype: Type\nsidebar label: News\n---\n# News\n"; + let entry = parse_test_entry(&dir, "type/news.md", content); + assert_eq!(entry.sidebar_label, Some("News".to_string())); + } + + #[test] + fn test_parse_sidebar_label_missing_defaults_to_none() { + let dir = TempDir::new().unwrap(); + let content = "---\ntype: Type\n---\n# Project\n"; + let entry = parse_test_entry(&dir, "type/project.md", content); + assert_eq!(entry.sidebar_label, None); + } + + #[test] + fn test_sidebar_label_not_in_relationships() { + let dir = TempDir::new().unwrap(); + let content = "---\ntype: Type\nsidebar label: My Series\n---\n# Series\n"; + let entry = parse_test_entry(&dir, "type/series.md", content); + assert!(entry.relationships.get("sidebar label").is_none()); + } + // Frontmatter update/delete tests are in frontmatter.rs // save_image tests are in vault/image.rs // purge_trash tests are in vault/trash.rs diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 389b6a29..9679cc47 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -39,6 +39,7 @@ const mockEntries: VaultEntry[] = [ icon: null, color: null, order: null, + sidebarLabel: null, outgoingLinks: [], }, { @@ -64,6 +65,7 @@ const mockEntries: VaultEntry[] = [ icon: null, color: null, order: null, + sidebarLabel: null, outgoingLinks: [], }, { @@ -89,6 +91,7 @@ const mockEntries: VaultEntry[] = [ icon: null, color: null, order: null, + sidebarLabel: null, outgoingLinks: [], }, { @@ -114,6 +117,7 @@ const mockEntries: VaultEntry[] = [ icon: null, color: null, order: null, + sidebarLabel: null, outgoingLinks: [], }, { @@ -139,6 +143,7 @@ const mockEntries: VaultEntry[] = [ icon: null, color: null, order: null, + sidebarLabel: null, outgoingLinks: [], }, { @@ -164,6 +169,7 @@ const mockEntries: VaultEntry[] = [ icon: null, color: null, order: null, + sidebarLabel: null, outgoingLinks: [], }, { @@ -189,6 +195,7 @@ const mockEntries: VaultEntry[] = [ icon: null, color: null, order: null, + sidebarLabel: null, outgoingLinks: [], }, { @@ -214,6 +221,7 @@ const mockEntries: VaultEntry[] = [ icon: null, color: null, order: null, + sidebarLabel: null, outgoingLinks: [], }, ] @@ -432,6 +440,7 @@ describe('Sidebar', () => { icon: null, color: null, order: null, + sidebarLabel: null, outgoingLinks: [], }, { @@ -457,6 +466,7 @@ describe('Sidebar', () => { icon: null, color: null, order: null, + sidebarLabel: null, outgoingLinks: [], }, { @@ -553,7 +563,7 @@ describe('Sidebar', () => { isA: 'Event', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, archived: false, trashed: true, trashedAt: 1700000000, modifiedAt: 1700000000, createdAt: null, fileSize: 100, snippet: '', wordCount: 0, - relationships: {}, icon: null, color: null, order: null, outgoingLinks: [], + relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, outgoingLinks: [], }, ] render( {}} />) @@ -591,6 +601,7 @@ describe('Sidebar', () => { icon: null, color: null, order: null, + sidebarLabel: null, outgoingLinks: [], } render( {}} />) @@ -598,6 +609,52 @@ describe('Sidebar', () => { const projectLabels = screen.getAllByText('Projects') expect(projectLabels.length).toBe(1) }) + + it('uses sidebarLabel from Type entry instead of auto-pluralization', () => { + const entriesWithLabel: VaultEntry[] = [ + ...mockEntries, + { + path: '/vault/type/news.md', filename: 'news.md', title: 'News', isA: 'Type', + aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, + archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, + fileSize: 200, snippet: '', wordCount: 0, relationships: {}, + icon: null, color: null, order: null, sidebarLabel: 'News', outgoingLinks: [], + }, + { + path: '/vault/news/breaking.md', filename: 'breaking.md', title: 'Breaking Story', isA: 'News', + aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, + archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, + fileSize: 300, snippet: '', wordCount: 0, relationships: {}, + icon: null, color: null, order: null, sidebarLabel: null, outgoingLinks: [], + }, + ] + render( {}} />) + // Should show "News" (custom label), not "Newses" (auto-pluralized) + expect(screen.getByText('News')).toBeInTheDocument() + expect(screen.queryByText('Newses')).not.toBeInTheDocument() + }) + + it('uses sidebarLabel to override built-in type label', () => { + const entriesWithBuiltInOverride: VaultEntry[] = [ + ...mockEntries, + { + path: '/vault/type/person.md', filename: 'person.md', title: 'Person', isA: 'Type', + aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, + archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, + fileSize: 200, snippet: '', wordCount: 0, relationships: {}, + icon: null, color: null, order: null, sidebarLabel: 'Contacts', outgoingLinks: [], + }, + ] + render( {}} />) + expect(screen.getByText('Contacts')).toBeInTheDocument() + expect(screen.queryByText('People')).not.toBeInTheDocument() + }) + + it('falls back to auto-pluralization when sidebarLabel is null', () => { + render( {}} />) + // Recipe has no sidebarLabel → should auto-pluralize to "Recipes" + expect(screen.getByText('Recipes')).toBeInTheDocument() + }) }) describe('customize section visibility', () => { @@ -705,21 +762,21 @@ describe('Sidebar', () => { aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '', wordCount: 0, - relationships: {}, icon: null, color: null, order: 5, outgoingLinks: [], + relationships: {}, icon: null, color: null, order: 5, sidebarLabel: null, outgoingLinks: [], }, { path: '/vault/type/topic.md', filename: 'topic.md', title: 'Topic', isA: 'Type', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '', wordCount: 0, - relationships: {}, icon: null, color: null, order: 0, outgoingLinks: [], + relationships: {}, icon: null, color: null, order: 0, sidebarLabel: null, outgoingLinks: [], }, { path: '/vault/type/person.md', filename: 'person.md', title: 'Person', isA: 'Type', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '', wordCount: 0, - relationships: {}, icon: null, color: null, order: 1, outgoingLinks: [], + relationships: {}, icon: null, color: null, order: 1, sidebarLabel: null, outgoingLinks: [], }, ] diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 5227dccf..4e7ce6ef 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -79,11 +79,12 @@ function buildSectionGroup(type: string, typeEntryMap: Record `note/link-target-${(i + j) % 50}`), + outgoingLinks: Array.from({ length: i % 8 }, (_j, j) => `note/link-target-${(i + j) % 50}`), + sidebarLabel: null, }) } return entries diff --git a/src/types.ts b/src/types.ts index 41a9a020..3c271492 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,6 +25,8 @@ export interface VaultEntry { color: string | null /** Display order for Type entries in sidebar (lower = higher). null = use default order. */ order: number | null + /** Custom sidebar section label for Type entries, overriding auto-pluralization. */ + sidebarLabel: string | null /** All wikilink targets found in the note content. Extracted from [[target]] patterns. */ outgoingLinks: string[] }