diff --git a/src-tauri/src/commands/vault.rs b/src-tauri/src/commands/vault.rs index 23d858c7..e82eea1f 100644 --- a/src-tauri/src/commands/vault.rs +++ b/src-tauri/src/commands/vault.rs @@ -207,7 +207,7 @@ pub fn batch_archive_notes(paths: Vec) -> Result { let mut count = 0; for path in &paths { let path = expand_tilde(path); - frontmatter::update_frontmatter(&path, "Archived", FrontmatterValue::Bool(true))?; + frontmatter::update_frontmatter(&path, "_archived", FrontmatterValue::Bool(true))?; count += 1; } Ok(count) @@ -219,10 +219,10 @@ pub fn batch_trash_notes(paths: Vec) -> Result { let mut count = 0; for path in &paths { let path = expand_tilde(path); - frontmatter::update_frontmatter(&path, "Trashed", FrontmatterValue::Bool(true))?; + frontmatter::update_frontmatter(&path, "_trashed", FrontmatterValue::Bool(true))?; frontmatter::update_frontmatter( &path, - "Trashed at", + "_trashed_at", FrontmatterValue::String(now.clone()), )?; count += 1; @@ -276,7 +276,7 @@ mod tests { 1 ); let content = std::fs::read_to_string(¬e).unwrap(); - assert!(content.contains("Archived: true")); + assert!(content.contains("_archived: true")); assert!(content.contains("Status: Active")); } @@ -288,8 +288,8 @@ mod tests { 1 ); let content = std::fs::read_to_string(¬e).unwrap(); - assert!(content.contains("Trashed: true")); - assert!(content.contains("Trashed at")); + assert!(content.contains("_trashed: true")); + assert!(content.contains("_trashed_at")); } #[test] diff --git a/src-tauri/src/vault/frontmatter.rs b/src-tauri/src/vault/frontmatter.rs index 93a3ad8e..bbf98a39 100644 --- a/src-tauri/src/vault/frontmatter.rs +++ b/src-tauri/src/vault/frontmatter.rs @@ -12,14 +12,16 @@ pub(crate) struct Frontmatter { #[serde(default)] pub aliases: Option, #[serde( - rename = "Archived", + rename = "_archived", + alias = "Archived", alias = "archived", default, deserialize_with = "deserialize_bool_or_string" )] pub archived: Option, #[serde( - rename = "Trashed", + rename = "_trashed", + alias = "Trashed", alias = "trashed", default, deserialize_with = "deserialize_bool_or_string" @@ -27,7 +29,11 @@ pub(crate) struct Frontmatter { pub trashed: Option, #[serde(rename = "Status", alias = "status", default)] pub status: Option, - #[serde(rename = "Trashed at", alias = "trashed_at")] + #[serde( + rename = "_trashed_at", + alias = "Trashed at", + alias = "trashed_at" + )] pub trashed_at: Option, #[serde(default)] pub icon: Option, @@ -145,10 +151,13 @@ fn parse_frontmatter(data: &HashMap) -> Frontmatter { "Is A", "is_a", "aliases", + "_archived", "Archived", "archived", + "_trashed", "Trashed", "trashed", + "_trashed_at", "Trashed at", "trashed_at", "icon", @@ -182,9 +191,13 @@ const SKIP_KEYS: &[&str] = &[ "is a", "type", "aliases", + "_archived", "archived", + "_trashed", "trashed", + "_trashed_at", "trashed at", + "trashed_at", "icon", "color", "order", diff --git a/src-tauri/src/vault/mod_tests.rs b/src-tauri/src/vault/mod_tests.rs index d31e2f51..12c136ee 100644 --- a/src-tauri/src/vault/mod_tests.rs +++ b/src-tauri/src/vault/mod_tests.rs @@ -1092,6 +1092,31 @@ fn test_parse_trashed_no() { assert!(!entry.trashed, "'Trashed: No' must be parsed as false"); } +// --- new canonical underscore-prefixed keys --- + +#[test] +fn test_parse_underscore_trashed_canonical() { + let dir = TempDir::new().unwrap(); + let content = "---\n_trashed: true\n_trashed_at: \"2026-03-15\"\n---\n# Gone\n"; + let entry = parse_test_entry(&dir, "gone-new.md", content); + assert!(entry.trashed, "'_trashed: true' must be parsed as trashed"); + assert!( + entry.trashed_at.is_some(), + "'_trashed_at' must be parsed as trashed_at" + ); +} + +#[test] +fn test_parse_underscore_archived_canonical() { + let dir = TempDir::new().unwrap(); + let content = "---\n_archived: true\n---\n# Old\n"; + let entry = parse_test_entry(&dir, "old-new.md", content); + assert!( + entry.archived, + "'_archived: true' must be parsed as archived" + ); +} + // --- visible field tests --- #[test] diff --git a/src-tauri/src/vault/trash.rs b/src-tauri/src/vault/trash.rs index 11b72d84..9fff068c 100644 --- a/src-tauri/src/vault/trash.rs +++ b/src-tauri/src/vault/trash.rs @@ -14,7 +14,10 @@ fn extract_trashed_at_string(data: &Option) -> Option let gray_matter::Pod::Hash(ref map) = data.as_ref()? else { return None; }; - let pod = map.get("Trashed at").or_else(|| map.get("trashed_at"))?; + let pod = map + .get("_trashed_at") + .or_else(|| map.get("Trashed at")) + .or_else(|| map.get("trashed_at"))?; match pod { gray_matter::Pod::String(s) => Some(s.clone()), _ => None, @@ -74,7 +77,11 @@ pub fn is_file_trashed(path: &Path) -> bool { // Check for "Trashed: true" if let Some(gray_matter::Pod::Hash(ref map)) = parsed.data { - if let Some(pod) = map.get("Trashed").or_else(|| map.get("trashed")) { + if let Some(pod) = map + .get("_trashed") + .or_else(|| map.get("Trashed")) + .or_else(|| map.get("trashed")) + { return match pod { gray_matter::Pod::Boolean(b) => *b, gray_matter::Pod::String(s) => { @@ -373,6 +380,28 @@ mod tests { assert!(!is_file_trashed(Path::new("/nonexistent/path.md"))); } + #[test] + fn test_is_file_trashed_with_underscore_trashed() { + let dir = TempDir::new().unwrap(); + create_test_file( + dir.path(), + "trashed.md", + "---\n_trashed: true\n---\n# Gone\n", + ); + assert!(is_file_trashed(&dir.path().join("trashed.md"))); + } + + #[test] + fn test_is_file_trashed_with_underscore_trashed_at() { + let dir = TempDir::new().unwrap(); + create_test_file( + dir.path(), + "trashed.md", + "---\n_trashed_at: \"2026-03-15\"\n---\n# Gone\n", + ); + assert!(is_file_trashed(&dir.path().join("trashed.md"))); + } + #[test] fn test_is_file_trashed_with_trashed_false() { let dir = TempDir::new().unwrap(); diff --git a/src/hooks/frontmatterOps.ts b/src/hooks/frontmatterOps.ts index f50a7120..5ad720e5 100644 --- a/src/hooks/frontmatterOps.ts +++ b/src/hooks/frontmatterOps.ts @@ -10,7 +10,9 @@ const ENTRY_DELETE_MAP: Record> = { type: { isA: null }, is_a: { isA: null }, status: { status: null }, color: { color: null }, icon: { icon: null }, aliases: { aliases: [] }, belongs_to: { belongsTo: [] }, related_to: { relatedTo: [] }, - archived: { archived: false }, trashed: { trashed: false }, order: { order: null }, + _archived: { archived: false }, archived: { archived: false }, + _trashed: { trashed: false }, trashed: { trashed: false }, + order: { order: null }, template: { template: null }, sort: { sort: null }, visible: { visible: null }, _favorite: { favorite: false }, _favorite_index: { favoriteIndex: null }, } @@ -53,7 +55,8 @@ export function frontmatterToEntryPatch( type: { isA: str }, is_a: { isA: str }, status: { status: str }, color: { color: str }, icon: { icon: str }, aliases: { aliases: arr }, belongs_to: { belongsTo: arr }, related_to: { relatedTo: arr }, - archived: { archived: Boolean(value) }, trashed: { trashed: Boolean(value) }, + _archived: { archived: Boolean(value) }, archived: { archived: Boolean(value) }, + _trashed: { trashed: Boolean(value) }, trashed: { trashed: Boolean(value) }, order: { order: typeof value === 'number' ? value : null }, template: { template: str }, sort: { sort: str }, diff --git a/src/hooks/useEntryActions.test.ts b/src/hooks/useEntryActions.test.ts index 604c2785..ff980768 100644 --- a/src/hooks/useEntryActions.test.ts +++ b/src/hooks/useEntryActions.test.ts @@ -67,8 +67,8 @@ describe('useEntryActions', () => { await result.current.handleTrashNote('/vault/note/test.md') }) - expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed', true, { silent: true }) - expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed at', expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/), { silent: true }) + expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', '_trashed', true, { silent: true }) + expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', '_trashed_at', expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/), { silent: true }) expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', { trashed: true, trashedAt: expect.any(Number), @@ -99,8 +99,8 @@ describe('useEntryActions', () => { await result.current.handleRestoreNote('/vault/note/test.md') }) - expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed', { silent: true }) - expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed at', { silent: true }) + expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/note/test.md', '_trashed', { silent: true }) + expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/note/test.md', '_trashed_at', { silent: true }) expect(handleUpdateFrontmatter).not.toHaveBeenCalled() expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', { trashed: false, @@ -119,7 +119,7 @@ describe('useEntryActions', () => { await result.current.handleArchiveNote('/vault/note/test.md') }) - expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'archived', true, { silent: true }) + expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', '_archived', true, { silent: true }) expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', { archived: true }) expect(setToastMessage).toHaveBeenCalledWith('Note archived') expect(onFrontmatterPersisted).toHaveBeenCalledTimes(1) @@ -146,7 +146,7 @@ describe('useEntryActions', () => { await result.current.handleUnarchiveNote('/vault/note/test.md') }) - expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/note/test.md', 'archived', { silent: true }) + expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/note/test.md', '_archived', { silent: true }) expect(handleUpdateFrontmatter).not.toHaveBeenCalled() expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', { archived: false }) expect(setToastMessage).toHaveBeenCalledWith('Note unarchived') diff --git a/src/hooks/useEntryActions.ts b/src/hooks/useEntryActions.ts index 4ecdd46b..172de314 100644 --- a/src/hooks/useEntryActions.ts +++ b/src/hooks/useEntryActions.ts @@ -35,8 +35,8 @@ export function useEntryActions({ setToastMessage('Note moved to trash') const now = new Date().toISOString().slice(0, 10) try { - await handleUpdateFrontmatter(path, 'Trashed', true, { silent: true }) - await handleUpdateFrontmatter(path, 'Trashed at', now, { silent: true }) + await handleUpdateFrontmatter(path, '_trashed', true, { silent: true }) + await handleUpdateFrontmatter(path, '_trashed_at', now, { silent: true }) onFrontmatterPersisted?.() } catch (err) { updateEntry(path, { trashed: false, trashedAt: null }) @@ -50,8 +50,8 @@ export function useEntryActions({ updateEntry(path, { trashed: false, trashedAt: null }) setToastMessage('Note restored from trash') try { - await handleDeleteProperty(path, 'Trashed', { silent: true }) - await handleDeleteProperty(path, 'Trashed at', { silent: true }) + await handleDeleteProperty(path, '_trashed', { silent: true }) + await handleDeleteProperty(path, '_trashed_at', { silent: true }) onFrontmatterPersisted?.() } catch (err) { updateEntry(path, { trashed: true, trashedAt: Date.now() / 1000 }) @@ -66,7 +66,7 @@ export function useEntryActions({ updateEntry(path, { archived: true }) setToastMessage('Note archived') try { - await handleUpdateFrontmatter(path, 'archived', true, { silent: true }) + await handleUpdateFrontmatter(path, '_archived', true, { silent: true }) onFrontmatterPersisted?.() } catch (err) { updateEntry(path, { archived: false }) @@ -80,7 +80,7 @@ export function useEntryActions({ updateEntry(path, { archived: false }) setToastMessage('Note unarchived') try { - await handleDeleteProperty(path, 'archived', { silent: true }) + await handleDeleteProperty(path, '_archived', { silent: true }) onFrontmatterPersisted?.() } catch (err) { updateEntry(path, { archived: true }) diff --git a/src/hooks/usePropertyPanelState.ts b/src/hooks/usePropertyPanelState.ts index 5aef19e3..3420db58 100644 --- a/src/hooks/usePropertyPanelState.ts +++ b/src/hooks/usePropertyPanelState.ts @@ -12,7 +12,7 @@ import { containsWikilinks } from '../components/DynamicPropertiesPanel' // Keys to skip showing in Properties (handled by dedicated UI or internal) // Compared case-insensitively via isVisibleProperty() -const SKIP_KEYS = new Set(['aliases', 'workspace', 'title', 'type', 'is_a', 'is a', 'trashed', 'trashed_at', 'archived', 'archived_at', 'icon']) +const SKIP_KEYS = new Set(['aliases', 'workspace', 'title', 'type', 'is_a', 'is a', '_trashed', 'trashed', '_trashed_at', 'trashed_at', 'trashed at', '_archived', 'archived', 'archived_at', 'icon']) function coerceValue(raw: string): FrontmatterValue { if (raw.toLowerCase() === 'true') return true