From cbfcb9d03ab9677bf5f7e510399c52011b8777ed Mon Sep 17 00:00:00 2001 From: Luca Rossi Date: Sat, 28 Feb 2026 20:01:56 +0100 Subject: [PATCH] fix: show actual type icons in Properties panel TypeSelector (#147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: parse `type` frontmatter key after is_a→type migration The migration converts `Is A:` to `type:` in frontmatter, and the TypeSelector UI writes to key `type`. But the Rust Frontmatter struct only read `Is A`, so type selection was silently lost after migration or UI change, falling back to folder inference. Add serde alias so both `Is A` and `type` keys are parsed. Also add `type` to SKIP_KEYS so it's not treated as a generic relationship. Co-Authored-By: Claude Opus 4.6 * fix: show actual type icons in Properties panel TypeSelector Replace generic Circle icon with each type's real icon (from iconRegistry) in the Type dropdown. Icons resolve via getTypeIcon with custom icon support from Type documents. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Test Co-authored-by: Claude Opus 4.6 --- src-tauri/src/vault/mod.rs | 36 ++++++++++++++++++++++- src/components/DynamicPropertiesPanel.tsx | 36 ++++++++++++++++++----- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/vault/mod.rs b/src-tauri/src/vault/mod.rs index 01a391a7..f7ad1849 100644 --- a/src-tauri/src/vault/mod.rs +++ b/src-tauri/src/vault/mod.rs @@ -74,7 +74,7 @@ pub struct VaultEntry { /// Intermediate struct to capture YAML frontmatter fields. #[derive(Debug, Deserialize, Default)] struct Frontmatter { - #[serde(rename = "Is A")] + #[serde(rename = "Is A", alias = "type")] is_a: Option, #[serde(default)] aliases: Option, @@ -135,6 +135,7 @@ fn parse_frontmatter(data: &HashMap) -> Frontmatter { /// Only skip keys that can never contain wikilinks. const SKIP_KEYS: &[&str] = &[ "is a", + "type", "aliases", "status", "cadence", @@ -968,6 +969,39 @@ References: assert_eq!(entry.is_a, Some("Type".to_string())); } + // --- type key (post-migration) tests --- + + #[test] + fn test_parse_type_key_lowercase() { + let dir = TempDir::new().unwrap(); + let content = "---\ntype: Project\n---\n# My Project\n"; + let entry = parse_test_entry(&dir, "project/my-project.md", content); + assert_eq!(entry.is_a, Some("Project".to_string())); + } + + #[test] + fn test_type_key_generates_type_relationship() { + let dir = TempDir::new().unwrap(); + let content = "---\ntype: Person\n---\n# Alice\n"; + let entry = parse_test_entry(&dir, "person/alice.md", content); + assert_eq!( + entry.relationships.get("Type").unwrap(), + &vec!["[[type/person]]".to_string()] + ); + } + + #[test] + fn test_type_key_not_in_relationships_as_generic() { + let dir = TempDir::new().unwrap(); + let content = "---\ntype: Note\nHas:\n - \"[[task/foo]]\"\n---\n# Test\n"; + let entry = parse_test_entry(&dir, "note/test.md", content); + // "type" key itself should not appear as a relationship (it's in SKIP_KEYS) + // Only "Has" and the auto-generated "Type" should be relationships + assert_eq!(entry.relationships.len(), 2); + assert!(entry.relationships.get("Has").is_some()); + assert!(entry.relationships.get("Type").is_some()); + } + // --- outgoing_links tests --- #[test] diff --git a/src/components/DynamicPropertiesPanel.tsx b/src/components/DynamicPropertiesPanel.tsx index 4ff7bfe5..54c64d85 100644 --- a/src/components/DynamicPropertiesPanel.tsx +++ b/src/components/DynamicPropertiesPanel.tsx @@ -12,6 +12,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from '@/components/ui/select' import { CalendarIcon, XIcon, Check, X, Type, ToggleLeft, Circle, Link, Tag } from 'lucide-react' import { getTypeColor, getTypeLightColor } from '../utils/typeColors' +import { getTypeIcon } from './NoteItem' import { countWords } from '../utils/wikilinks' import { type PropertyDisplayMode, @@ -482,9 +483,24 @@ function ReadOnlyType({ isA, customColorKey, onNavigate }: { isA?: string | null ) } -function TypeSelector({ isA, customColorKey, availableTypes, typeColorKeys, onUpdateProperty, onNavigate }: { +function TypeSelectorItem({ type, typeColorKeys, typeIconKeys }: { + type: string; typeColorKeys: Record; typeIconKeys: Record +}) { + const Icon = getTypeIcon(type, typeIconKeys[type]) + const color = getTypeColor(type, typeColorKeys[type]) + return ( + <> + {/* eslint-disable-next-line react-hooks/static-components -- icon from static map lookup */} + + {type} + + ) +} + +function TypeSelector({ isA, customColorKey, availableTypes, typeColorKeys, typeIconKeys, onUpdateProperty, onNavigate }: { isA?: string | null; customColorKey?: string | null; availableTypes: string[] typeColorKeys: Record + typeIconKeys: Record onUpdateProperty?: (key: string, value: FrontmatterValue) => void onNavigate?: (target: string) => void }) { @@ -511,8 +527,7 @@ function TypeSelector({ isA, customColorKey, availableTypes, typeColorKeys, onUp {options.map(type => ( - - {type} + ))} @@ -642,11 +657,16 @@ function reconcileListUpdate( function deriveTypeInfo(entries: VaultEntry[] | undefined, entryIsA: string | null) { const typeEntries = (entries ?? []).filter(e => e.isA === 'Type') const typeColorKeys: Record = {} - for (const e of typeEntries) { typeColorKeys[e.title] = e.color ?? null } + const typeIconKeys: Record = {} + for (const e of typeEntries) { + typeColorKeys[e.title] = e.color ?? null + typeIconKeys[e.title] = e.icon ?? null + } return { availableTypes: typeEntries.map(e => e.title).sort((a, b) => a.localeCompare(b)), customColorKey: entryIsA ? (typeColorKeys[entryIsA] ?? null) : null, typeColorKeys, + typeIconKeys, } } @@ -714,7 +734,7 @@ function usePropertyPanelState(deps: PropertyPanelDeps) { const [showAddDialog, setShowAddDialog] = useState(false) const [displayOverrides, setDisplayOverrides] = useState(() => loadDisplayModeOverrides()) - const { availableTypes, customColorKey, typeColorKeys } = useMemo(() => deriveTypeInfo(entries, entryIsA), [entries, entryIsA]) + const { availableTypes, customColorKey, typeColorKeys, typeIconKeys } = useMemo(() => deriveTypeInfo(entries, entryIsA), [entries, entryIsA]) const vaultStatuses = useMemo(() => collectVaultStatuses(entries), [entries]) const vaultTagsByKey = useMemo(() => collectAllVaultTags(entries, allContent), [entries, allContent]) const propertyEntries = useMemo(() => Object.entries(frontmatter).filter(isVisibleProperty), [frontmatter]) @@ -746,7 +766,7 @@ function usePropertyPanelState(deps: PropertyPanelDeps) { return { editingKey, setEditingKey, showAddDialog, setShowAddDialog, displayOverrides, - availableTypes, customColorKey, typeColorKeys, vaultStatuses, vaultTagsByKey, propertyEntries, + availableTypes, customColorKey, typeColorKeys, typeIconKeys, vaultStatuses, vaultTagsByKey, propertyEntries, handleSaveValue, handleSaveList, handleAdd, handleDisplayModeChange, } } @@ -767,7 +787,7 @@ export function DynamicPropertiesPanel({ }) { const { editingKey, setEditingKey, showAddDialog, setShowAddDialog, displayOverrides, - availableTypes, customColorKey, typeColorKeys, vaultStatuses, vaultTagsByKey, propertyEntries, + availableTypes, customColorKey, typeColorKeys, typeIconKeys, vaultStatuses, vaultTagsByKey, propertyEntries, handleSaveValue, handleSaveList, handleAdd, handleDisplayModeChange, } = usePropertyPanelState({ entries, entryIsA: entry.isA, frontmatter, allContent, onUpdateProperty, onDeleteProperty, onAddProperty }) @@ -776,7 +796,7 @@ export function DynamicPropertiesPanel({ return (
- + {propertyEntries.map(([key, value]) => (