fix: trashed notes reappear after restart due to frontmatter key casing mismatch
Frontend wrote `trashed` (lowercase) but Rust parser expected `Trashed` (title-case via serde rename). On restart, the lowercase key didn't match, defaulted to false, and trashed notes reappeared. - Frontend: use title-case keys (Trashed, Trashed at) matching vault convention - Rust: add serde aliases for lowercase keys (backward compat with already-written files) - Add regression tests for both title-case and lowercase parsing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -103,9 +103,9 @@ struct Frontmatter {
|
||||
cadence: Option<String>,
|
||||
#[serde(rename = "Archived")]
|
||||
archived: Option<bool>,
|
||||
#[serde(rename = "Trashed")]
|
||||
#[serde(rename = "Trashed", alias = "trashed")]
|
||||
trashed: Option<bool>,
|
||||
#[serde(rename = "Trashed at")]
|
||||
#[serde(rename = "Trashed at", alias = "trashed_at")]
|
||||
trashed_at: Option<String>,
|
||||
#[serde(rename = "Created at")]
|
||||
created_at: Option<String>,
|
||||
@@ -1312,6 +1312,33 @@ Company: Acme Corp
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trashed_title_case() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\nTrashed: true\nTrashed at: \"2025-02-01\"\n---\n# Gone\n";
|
||||
let entry = parse_test_entry(&dir, "gone.md", content);
|
||||
assert!(entry.trashed);
|
||||
assert!(entry.trashed_at.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trashed_lowercase_alias() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntrashed: true\ntrashed_at: \"2025-02-01\"\n---\n# Gone\n";
|
||||
let entry = parse_test_entry(&dir, "gone.md", content);
|
||||
assert!(entry.trashed, "lowercase 'trashed' must be parsed via alias");
|
||||
assert!(entry.trashed_at.is_some(), "lowercase 'trashed_at' must be parsed via alias");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trashed_false_when_absent() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\nIs A: Note\n---\n# Active\n";
|
||||
let entry = parse_test_entry(&dir, "active.md", content);
|
||||
assert!(!entry.trashed);
|
||||
assert!(entry.trashed_at.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
|
||||
|
||||
@@ -66,8 +66,8 @@ describe('useEntryActions', () => {
|
||||
await result.current.handleTrashNote('/vault/note/test.md')
|
||||
})
|
||||
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'trashed', true)
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'trashed_at', expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/))
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed', true)
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed at', expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/))
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', {
|
||||
trashed: true,
|
||||
trashedAt: expect.any(Number),
|
||||
@@ -84,8 +84,8 @@ describe('useEntryActions', () => {
|
||||
await result.current.handleRestoreNote('/vault/note/test.md')
|
||||
})
|
||||
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'trashed', false)
|
||||
expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/note/test.md', 'trashed_at')
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed', false)
|
||||
expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed at')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', {
|
||||
trashed: false,
|
||||
trashedAt: null,
|
||||
|
||||
@@ -19,15 +19,15 @@ export function useEntryActions({
|
||||
}: EntryActionsConfig) {
|
||||
const handleTrashNote = useCallback(async (path: string) => {
|
||||
const now = new Date().toISOString().slice(0, 10)
|
||||
await handleUpdateFrontmatter(path, 'trashed', true)
|
||||
await handleUpdateFrontmatter(path, 'trashed_at', now)
|
||||
await handleUpdateFrontmatter(path, 'Trashed', true)
|
||||
await handleUpdateFrontmatter(path, 'Trashed at', now)
|
||||
updateEntry(path, { trashed: true, trashedAt: Date.now() / 1000 })
|
||||
setToastMessage('Note moved to trash')
|
||||
}, [handleUpdateFrontmatter, updateEntry, setToastMessage])
|
||||
|
||||
const handleRestoreNote = useCallback(async (path: string) => {
|
||||
await handleUpdateFrontmatter(path, 'trashed', false)
|
||||
await handleDeleteProperty(path, 'trashed_at')
|
||||
await handleUpdateFrontmatter(path, 'Trashed', false)
|
||||
await handleDeleteProperty(path, 'Trashed at')
|
||||
updateEntry(path, { trashed: false, trashedAt: null })
|
||||
setToastMessage('Note restored from trash')
|
||||
}, [handleUpdateFrontmatter, handleDeleteProperty, updateEntry, setToastMessage])
|
||||
|
||||
Reference in New Issue
Block a user