diff --git a/demo-vault-v2/career-tracks-depend-on-company-shape.md b/demo-vault-v2/career-tracks-depend-on-company-shape.md index ad288116..82cc845b 100644 --- a/demo-vault-v2/career-tracks-depend-on-company-shape.md +++ b/demo-vault-v2/career-tracks-depend-on-company-shape.md @@ -1,5 +1,5 @@ --- -title: Untitled note 343 +title: Untitled note 345 type: Note status: Active --- diff --git a/demo-vault-v2/final-title.md b/demo-vault-v2/final-title.md index 90482ef3..1a74b9c2 100644 --- a/demo-vault-v2/final-title.md +++ b/demo-vault-v2/final-title.md @@ -1,5 +1,5 @@ --- -title: Untitled note 342 +title: Untitled note 352 type: Note status: Active --- diff --git a/demo-vault-v2/responsive-rename-test.md b/demo-vault-v2/renamed-title-xyz.md similarity index 57% rename from demo-vault-v2/responsive-rename-test.md rename to demo-vault-v2/renamed-title-xyz.md index 82cc845b..ad288116 100644 --- a/demo-vault-v2/responsive-rename-test.md +++ b/demo-vault-v2/renamed-title-xyz.md @@ -1,5 +1,5 @@ --- -title: Untitled note 345 +title: Untitled note 343 type: Note status: Active --- diff --git a/demo-vault-v2/untitled-note-342.md b/demo-vault-v2/untitled-note-342.md new file mode 100644 index 00000000..b1d3210b --- /dev/null +++ b/demo-vault-v2/untitled-note-342.md @@ -0,0 +1,6 @@ +--- +title: Untitled note 342 +type: Note +status: Active +--- +My Custom H1 Heading diff --git a/demo-vault-v2/untitled-note-346.md b/demo-vault-v2/untitled-note-346.md new file mode 100644 index 00000000..0aa864b6 --- /dev/null +++ b/demo-vault-v2/untitled-note-346.md @@ -0,0 +1,5 @@ +--- +title: Untitled note 346 +type: Note +status: Active +--- diff --git a/demo-vault-v2/untitled-note-347.md b/demo-vault-v2/untitled-note-347.md new file mode 100644 index 00000000..00a3f030 --- /dev/null +++ b/demo-vault-v2/untitled-note-347.md @@ -0,0 +1,5 @@ +--- +title: Untitled note 347 +type: Note +status: Active +--- diff --git a/demo-vault-v2/untitled-note-348.md b/demo-vault-v2/untitled-note-348.md new file mode 100644 index 00000000..3369b558 --- /dev/null +++ b/demo-vault-v2/untitled-note-348.md @@ -0,0 +1,5 @@ +--- +title: Untitled note 348 +type: Note +status: Active +--- diff --git a/demo-vault-v2/untitled-note-349.md b/demo-vault-v2/untitled-note-349.md new file mode 100644 index 00000000..40a4b968 --- /dev/null +++ b/demo-vault-v2/untitled-note-349.md @@ -0,0 +1,6 @@ +--- +title: Untitled note 349 +type: Note +status: Active +--- +My Custom H1 Heading diff --git a/demo-vault-v2/untitled-note-350.md b/demo-vault-v2/untitled-note-350.md new file mode 100644 index 00000000..e570736d --- /dev/null +++ b/demo-vault-v2/untitled-note-350.md @@ -0,0 +1,5 @@ +--- +title: Untitled note 350 +type: Note +status: Active +--- diff --git a/demo-vault-v2/untitled-note-351.md b/demo-vault-v2/untitled-note-351.md new file mode 100644 index 00000000..b98824d6 --- /dev/null +++ b/demo-vault-v2/untitled-note-351.md @@ -0,0 +1,5 @@ +--- +title: Untitled note 351 +type: Note +status: Active +--- diff --git a/demo-vault-v2/untitled-note-353-renamed.md b/demo-vault-v2/untitled-note-353-renamed.md new file mode 100644 index 00000000..d5d02519 --- /dev/null +++ b/demo-vault-v2/untitled-note-353-renamed.md @@ -0,0 +1,8 @@ +--- +title: Untitled note 353 +type: Project +status: Active +--- + + +Appended by raw editor test \ No newline at end of file diff --git a/demo-vault-v2/untitled-note-354.md b/demo-vault-v2/untitled-note-354.md new file mode 100644 index 00000000..e41b182f --- /dev/null +++ b/demo-vault-v2/untitled-note-354.md @@ -0,0 +1,8 @@ +--- +title: Untitled note 354 +type: Note +status: Active +--- +[[2024-03|March 2024]] + +[[2024-03|March 2024]] diff --git a/demo-vault-v2/untitled-project-57.md b/demo-vault-v2/untitled-project-57.md new file mode 100644 index 00000000..7493a1a4 --- /dev/null +++ b/demo-vault-v2/untitled-project-57.md @@ -0,0 +1,16 @@ +--- +title: Untitled project 57 +type: Project +status: Active +--- + +## Objective + + + +## Key Results + + + +## Notes + diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index 369ac876..d9a43f6d 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -77,8 +77,8 @@ classDiagram +Number wordCount +String? snippet +Boolean archived - +Boolean trashed - +Number? trashedAt + +Boolean trashed ⚠ legacy + +Number? trashedAt ⚠ legacy +Record~string,string~ properties } @@ -127,8 +127,8 @@ interface VaultEntry { wordCount: number | null // Body word count (excludes frontmatter) snippet: string | null // First 200 chars of body archived: boolean // Archived flag - trashed: boolean // Trashed flag - trashedAt: number | null // When trashed (for auto-purge) + trashed: boolean // Kept for backward compatibility (Trash system removed — delete is permanent) + trashedAt: number | null // Kept for backward compatibility (Trash system removed) properties: Record // Scalar frontmatter fields (custom properties) } ``` @@ -248,7 +248,7 @@ The editor displays a dedicated `TitleField` component above the BlockNote edito Navigation state is modeled as a discriminated union: ```typescript -type SidebarFilter = 'all' | 'archived' | 'trash' | 'changes' | 'pulse' +type SidebarFilter = 'all' | 'archived' | 'changes' | 'pulse' type SidebarSelection = | { kind: 'filter'; filter: SidebarFilter } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 472ea01e..ec6ba09b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -361,7 +361,7 @@ Search is keyword-based, using `walkdir` to scan all `.md` files in the vault di - Matches query against file titles and content (case-insensitive) - Scores results: title matches ranked higher than content-only matches - Extracts contextual snippets around the first match -- Skips trashed and hidden files +- Skips hidden files The `search_vault` Tauri command runs the scan in a blocking Tokio task and returns results sorted by relevance score. @@ -466,7 +466,7 @@ sequenceDiagram participant MCP as MCP Server participant U as User - T->>T: run_startup_tasks()
(purge trash, register MCP) + T->>T: run_startup_tasks()
(register MCP) T->>MCP: spawn_ws_bridge() — ports 9710 + 9711 T->>A: App mounts @@ -548,7 +548,6 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules: | `parsing.rs` | Text processing: snippet extraction, markdown stripping, ISO date parsing, `extract_title`, `slug_to_title` | | `title_sync.rs` | `sync_title_on_open` — ensures `title` frontmatter matches filename on note open | | `cache.rs` | Git-based incremental vault caching (`scan_vault_cached`), git helpers | -| `trash.rs` | `purge_trash` — deletes trashed notes older than 30 days | | `rename.rs` | `rename_note` — renames files, updates `title` frontmatter, and updates wikilinks across the vault | | `image.rs` | `save_image` — saves base64-encoded attachments with sanitized filenames | | `migration.rs` | `flatten_vault`, `vault_health_check`, `migrate_is_a_to_type` | @@ -559,7 +558,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules: | Module | Purpose | |--------|---------| -| `vault/` | Vault scanning, caching, parsing, trash, rename, image, migration | +| `vault/` | Vault scanning, caching, parsing, rename, image, migration | | `frontmatter/` | YAML frontmatter read/write (`mod.rs`, `yaml.rs`, `ops.rs`) | | `git/` | Git operations (`commit.rs`, `status.rs`, `history.rs`, `conflict.rs`, `remote.rs`, `pulse.rs`) | | `github/` | GitHub OAuth + API (`auth.rs`, `api.rs`, `clone.rs`) | @@ -581,14 +580,11 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules: | `list_vault` | Scan vault (cached) → `Vec` | | `get_note_content` | Read note file content | | `save_note_content` | Write note content to disk | -| `delete_note` | Move note to trash | +| `delete_note` | Permanently delete note from disk (with confirm dialog) | | `rename_note` | Rename note + update `title` frontmatter + cross-vault wikilinks | | `sync_note_title` | Sync `title` frontmatter with filename on note open → `bool` (modified) | | `batch_archive_notes` | Archive multiple notes | -| `batch_trash_notes` | Trash multiple notes | | `batch_delete_notes` | Permanently delete notes from disk | -| `empty_trash` | Permanently delete all trashed notes from disk | -| `purge_trash` | Delete notes trashed >30 days ago | | `reload_vault` | Invalidate cache and full rescan from filesystem → `Vec` | | `reload_vault_entry` | Re-read a single file from disk → `VaultEntry` | | `check_vault_exists` | Check if vault path exists | @@ -857,7 +853,7 @@ Desktop-only features gated at the function level in `commands/`: - Menu state updates Features that work on both platforms without changes: -- Vault scan, note read/write, rename, delete, trash, archive +- Vault scan, note read/write, rename, delete, archive - Frontmatter read/write/delete - AI chat (Anthropic API via `reqwest`) - Search (pure Rust in-memory) diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index c0bdb214..aa7b2e67 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -138,7 +138,6 @@ laputa-app/ │ │ │ ├── mod.rs # Core types, parse_md_file, scan_vault │ │ │ ├── cache.rs # Git-based incremental caching │ │ │ ├── parsing.rs # Text processing + title extraction -│ │ │ ├── trash.rs # Trash auto-purge │ │ │ ├── rename.rs # Rename + cross-vault wikilink update │ │ │ ├── image.rs # Image attachment saving │ │ │ ├── migration.rs # Frontmatter migration diff --git a/docs/PROJECT-SPEC.md b/docs/PROJECT-SPEC.md index 64ec50fd..38d33fa4 100644 --- a/docs/PROJECT-SPEC.md +++ b/docs/PROJECT-SPEC.md @@ -180,7 +180,6 @@ Minimize custom UI behavior per entity type. Everything is a file, everything ge - Favorites - People - Events -- Trash **Section Groups** (expandable, show entity list): - PROJECTS + @@ -280,7 +279,7 @@ bc75647 Remove unused Vite scaffold files ### M2: Sidebar & Note List **Goal:** Navigate the vault via sidebar, see filtered note lists. -- [ ] Sidebar: Filters section (All Notes, Favorites, Trash) +- [ ] Sidebar: Filters section (All Notes, Favorites) - [ ] Sidebar: Section Groups (Projects, Experiments, Responsibilities, Procedures) — populated from frontmatter `type:` - [ ] Sidebar: Topics — flat list, populated from `Related to` topic links - [ ] Note list: show title, preview snippet, date, type indicator @@ -315,7 +314,7 @@ bc75647 Remove unused Vite scaffold files **Goal:** Create, rename, delete files. Polish for daily-driver use. - [ ] Create new note (with type selector → sets `type:` in frontmatter, created at vault root) - [ ] Rename file (updates filename + title) -- [ ] Delete → move to trash +- [ ] Delete → permanent delete with confirm dialog - [ ] Keyboard shortcuts (Cmd+N new, Cmd+S save, Cmd+P quick open/search) - [ ] Quick open palette (Cmd+P) — fuzzy search across all files - [ ] People and Events filters in sidebar diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d0c1fd7b..0ef3d905 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2282,7 +2282,6 @@ dependencies = [ "tauri-plugin-updater", "tempfile", "tokio", - "trash", "uuid", "walkdir", ] @@ -4632,7 +4631,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -4721,7 +4720,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows 0.61.3", + "windows", ] [[package]] @@ -4884,7 +4883,7 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.18", "url", - "windows 0.61.3", + "windows", "zbus", ] @@ -4953,7 +4952,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.3", + "windows", ] [[package]] @@ -4979,7 +4978,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.3", + "windows", "wry", ] @@ -5405,24 +5404,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "trash" -version = "5.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9b93a14fcf658568eb11b3ac4cb406822e916e2c55cdebc421beeb0bd7c94d8" -dependencies = [ - "chrono", - "libc", - "log", - "objc2", - "objc2-foundation", - "once_cell", - "percent-encoding", - "scopeguard", - "urlencoding", - "windows 0.56.0", -] - [[package]] name = "tray-icon" version = "0.21.3" @@ -5580,12 +5561,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "urlpattern" version = "0.3.0" @@ -5917,10 +5892,10 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.61.3", + "windows", "windows-core 0.61.2", - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", ] [[package]] @@ -5941,7 +5916,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows 0.61.3", + "windows", "windows-core 0.61.2", ] @@ -5991,16 +5966,6 @@ dependencies = [ "windows-version", ] -[[package]] -name = "windows" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" -dependencies = [ - "windows-core 0.56.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.61.3" @@ -6023,26 +5988,14 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "windows-core" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" -dependencies = [ - "windows-implement 0.56.0", - "windows-interface 0.56.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -6054,8 +6007,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -6072,17 +6025,6 @@ dependencies = [ "windows-threading", ] -[[package]] -name = "windows-implement" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.115", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -6094,17 +6036,6 @@ dependencies = [ "syn 2.0.115", ] -[[package]] -name = "windows-interface" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.115", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -6149,15 +6080,6 @@ dependencies = [ "windows-strings 0.5.1", ] -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -6670,7 +6592,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-version", "x11-dl", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 414578c1..0a2d79da 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -37,7 +37,6 @@ tauri-plugin-process = "2.3.1" tauri-plugin-opener = "2" sentry = "0.37" uuid = { version = "1", features = ["v4"] } -trash = "5" [dev-dependencies] tempfile = "3" diff --git a/src-tauri/src/commands/vault.rs b/src-tauri/src/commands/vault.rs index 24416d5c..50d516cc 100644 --- a/src-tauri/src/commands/vault.rs +++ b/src-tauri/src/commands/vault.rs @@ -60,12 +60,6 @@ pub fn update_wikilinks_for_renames( vault::update_wikilinks_for_renames(&vault_path, &renames) } -#[tauri::command] -pub fn purge_trash(vault_path: String) -> Result, String> { - let vault_path = expand_tilde(&vault_path); - vault::purge_trash(&vault_path) -} - #[tauri::command] pub fn delete_note(path: String) -> Result { let path = expand_tilde(&path); @@ -78,12 +72,6 @@ pub fn batch_delete_notes(paths: Vec) -> Result, String> { vault::batch_delete_notes(&expanded) } -#[tauri::command] -pub fn empty_trash(vault_path: String) -> Result, String> { - let vault_path = expand_tilde(&vault_path); - vault::empty_trash(&vault_path) -} - #[tauri::command] pub fn migrate_is_a_to_type(vault_path: String) -> Result { let vault_path = expand_tilde(&vault_path); @@ -230,23 +218,6 @@ pub fn batch_archive_notes(paths: Vec) -> Result { Ok(count) } -#[tauri::command] -pub fn batch_trash_notes(paths: Vec) -> Result { - let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); - 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_at", - FrontmatterValue::String(now.clone()), - )?; - count += 1; - } - Ok(count) -} - // ── Search commands ───────────────────────────────────────────────────────── #[tauri::command] @@ -297,18 +268,6 @@ mod tests { assert!(content.contains("Status: Active")); } - #[test] - fn test_batch_trash_notes() { - let (_dir, note) = temp_note("---\nStatus: Active\n---\n# Note\n"); - assert_eq!( - batch_trash_notes(vec![note.to_str().unwrap().to_string()]).unwrap(), - 1 - ); - let content = std::fs::read_to_string(¬e).unwrap(); - assert!(content.contains("_trashed: true")); - assert!(content.contains("_trashed_at")); - } - #[test] fn test_reload_vault_entry_reads_from_disk() { let dir = tempfile::TempDir::new().unwrap(); @@ -359,7 +318,7 @@ mod tests { std::fs::write( vault_path.join("note.md"), - "---\nTrashed: false\n---\n# Note\n", + "---\n_archived: false\n---\n# Note\n", ) .unwrap(); std::process::Command::new("git") @@ -374,11 +333,11 @@ mod tests { .unwrap(); let entries = list_vault(vault_path.to_str().unwrap().to_string()).unwrap(); - assert!(!entries[0].trashed); + assert!(!entries[0].archived); std::fs::write( vault_path.join("note.md"), - "---\nTrashed: true\n---\n# Note\n", + "---\n_archived: true\n---\n# Note\n", ) .unwrap(); @@ -386,8 +345,8 @@ mod tests { crate::vault::invalidate_cache(std::path::Path::new(vp_str)); let fresh = crate::vault::scan_vault_cached(std::path::Path::new(vp_str)).unwrap(); assert!( - fresh[0].trashed, - "reload_vault must reflect disk state after trashing" + fresh[0].archived, + "reload_vault must reflect disk state after archiving" ); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 17c3b01f..924ee724 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,15 +16,10 @@ pub mod vault_list; use std::process::Child; #[cfg(desktop)] use std::sync::Mutex; -#[cfg(desktop)] -use std::time::Instant; #[cfg(desktop)] struct WsBridgeChild(Mutex>); -#[cfg(desktop)] -struct LastPurgeTime(Mutex); - #[cfg(desktop)] fn log_startup_result(label: &str, result: Result) { match result { @@ -34,7 +29,7 @@ fn log_startup_result(label: &str, result: Result) { } } -/// Run startup housekeeping on the default vault (purge old trash, migrate legacy frontmatter). +/// Run startup housekeeping on the default vault (migrate legacy frontmatter, seed configs). #[cfg(desktop)] fn run_startup_tasks() { let vault_path = dirs::home_dir() @@ -44,10 +39,6 @@ fn run_startup_tasks() { return; } let vp_str = vault_path.to_str().unwrap_or_default(); - log_startup_result( - "Purged trashed files on startup", - vault::purge_trash(vp_str).map(|d| d.len()), - ); log_startup_result( "Migrated is_a to type on startup", vault::migrate_is_a_to_type(vp_str), @@ -86,8 +77,6 @@ pub fn run() { #[cfg(desktop)] let builder = builder.manage(WsBridgeChild(Mutex::new(None))); - #[cfg(desktop)] - let builder = builder.manage(LastPurgeTime(Mutex::new(Instant::now()))); builder .setup(|app| { @@ -158,14 +147,11 @@ pub fn run() { commands::sync_note_title, commands::save_image, commands::copy_image_to_vault, - commands::purge_trash, commands::delete_note, commands::batch_delete_notes, - commands::empty_trash, commands::migrate_is_a_to_type, commands::create_vault_folder, commands::batch_archive_notes, - commands::batch_trash_notes, commands::get_settings, commands::update_menu_state, commands::save_settings, @@ -196,39 +182,13 @@ pub fn run() { #[cfg(desktop)] { use tauri::Manager; - match _event { - tauri::RunEvent::Exit => { - let state: tauri::State<'_, WsBridgeChild> = _app_handle.state(); - let mut guard = state.0.lock().unwrap(); - if let Some(ref mut child) = *guard { - let _ = child.kill(); - log::info!("ws-bridge child process killed on exit"); - } + if let tauri::RunEvent::Exit = _event { + let state: tauri::State<'_, WsBridgeChild> = _app_handle.state(); + let mut guard = state.0.lock().unwrap(); + if let Some(ref mut child) = *guard { + let _ = child.kill(); + log::info!("ws-bridge child process killed on exit"); } - tauri::RunEvent::WindowEvent { - event: tauri::WindowEvent::Focused(true), - .. - } => { - let state: tauri::State<'_, LastPurgeTime> = _app_handle.state(); - let mut last = state.0.lock().unwrap(); - if last.elapsed() >= std::time::Duration::from_secs(3600) { - *last = Instant::now(); - drop(last); - std::thread::spawn(|| { - let vault_path = dirs::home_dir() - .map(|h| h.join("Laputa")) - .unwrap_or_default(); - if vault_path.is_dir() { - log_startup_result( - "Purged trashed files on focus", - vault::purge_trash(vault_path.to_str().unwrap_or_default()) - .map(|d| d.len()), - ); - } - }); - } - } - _ => {} } } }); diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index c222ae7f..37f93ed6 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -32,13 +32,11 @@ const VIEW_GO_FORWARD: &str = "view-go-forward"; const GO_ALL_NOTES: &str = "go-all-notes"; const GO_ARCHIVED: &str = "go-archived"; -const GO_TRASH: &str = "go-trash"; const GO_CHANGES: &str = "go-changes"; const GO_INBOX: &str = "go-inbox"; const NOTE_ARCHIVE: &str = "note-archive"; -const NOTE_TRASH: &str = "note-trash"; -const NOTE_EMPTY_TRASH: &str = "note-empty-trash"; +const NOTE_DELETE: &str = "note-delete"; const NOTE_OPEN_IN_NEW_WINDOW: &str = "note-open-in-new-window"; const VAULT_OPEN: &str = "vault-open"; @@ -77,11 +75,9 @@ const CUSTOM_IDS: &[&str] = &[ VIEW_GO_FORWARD, GO_ALL_NOTES, GO_ARCHIVED, - GO_TRASH, GO_CHANGES, NOTE_ARCHIVE, - NOTE_TRASH, - NOTE_EMPTY_TRASH, + NOTE_DELETE, NOTE_OPEN_IN_NEW_WINDOW, VAULT_OPEN, VAULT_REMOVE, @@ -99,7 +95,7 @@ const CUSTOM_IDS: &[&str] = &[ const NOTE_DEPENDENT_IDS: &[&str] = &[ FILE_SAVE, NOTE_ARCHIVE, - NOTE_TRASH, + NOTE_DELETE, EDIT_TOGGLE_RAW_EDITOR, EDIT_TOGGLE_DIFF, VIEW_TOGGLE_BACKLINKS, @@ -249,7 +245,6 @@ fn build_go_menu(app: &App) -> MenuResult { let archived = MenuItemBuilder::new("Archived") .id(GO_ARCHIVED) .build(app)?; - let trash = MenuItemBuilder::new("Trash").id(GO_TRASH).build(app)?; let changes = MenuItemBuilder::new("Changes").id(GO_CHANGES).build(app)?; let inbox = MenuItemBuilder::new("Inbox").id(GO_INBOX).build(app)?; let go_back = MenuItemBuilder::new("Go Back") @@ -264,7 +259,6 @@ fn build_go_menu(app: &App) -> MenuResult { Ok(SubmenuBuilder::new(app, "Go") .item(&all_notes) .item(&archived) - .item(&trash) .item(&changes) .item(&inbox) .separator() @@ -278,13 +272,10 @@ fn build_note_menu(app: &App) -> MenuResult { .id(NOTE_ARCHIVE) .accelerator("CmdOrCtrl+E") .build(app)?; - let trash_note = MenuItemBuilder::new("Trash Note") - .id(NOTE_TRASH) + let delete_note = MenuItemBuilder::new("Delete Note") + .id(NOTE_DELETE) .accelerator("CmdOrCtrl+Backspace") .build(app)?; - let empty_trash = MenuItemBuilder::new("Empty Trash…") - .id(NOTE_EMPTY_TRASH) - .build(app)?; let open_new_window = MenuItemBuilder::new("Open in New Window") .id(NOTE_OPEN_IN_NEW_WINDOW) .accelerator("CmdOrCtrl+Shift+O") @@ -303,8 +294,7 @@ fn build_note_menu(app: &App) -> MenuResult { Ok(SubmenuBuilder::new(app, "Note") .item(&archive_note) - .item(&trash_note) - .item(&empty_trash) + .item(&delete_note) .separator() .item(&open_new_window) .separator() @@ -462,11 +452,9 @@ mod tests { VIEW_GO_FORWARD, GO_ALL_NOTES, GO_ARCHIVED, - GO_TRASH, GO_CHANGES, NOTE_ARCHIVE, - NOTE_TRASH, - NOTE_EMPTY_TRASH, + NOTE_DELETE, NOTE_OPEN_IN_NEW_WINDOW, VAULT_OPEN, VAULT_REMOVE, diff --git a/src-tauri/src/search.rs b/src-tauri/src/search.rs index 15dcd253..0132b93b 100644 --- a/src-tauri/src/search.rs +++ b/src-tauri/src/search.rs @@ -1,4 +1,3 @@ -use crate::vault; use serde::Serialize; use std::path::Path; use std::time::Instant; @@ -75,9 +74,6 @@ pub fn search_vault( if !path.extension().is_some_and(|ext| ext == "md") { continue; } - if vault::is_file_trashed(path) { - continue; - } // Skip hidden dirs and .laputa config if path .components() diff --git a/src-tauri/src/vault/cache.rs b/src-tauri/src/vault/cache.rs index 33dc4348..68c530bf 100644 --- a/src-tauri/src/vault/cache.rs +++ b/src-tauri/src/vault/cache.rs @@ -888,18 +888,18 @@ mod tests { let (_lock, _cache_tmp, dir) = setup_git_vault(); let vault = dir.path(); - create_test_file(vault, "note.md", "---\nTrashed: false\n---\n# Note\n"); + create_test_file(vault, "note.md", "---\n_archived: false\n---\n# Note\n"); git_add_commit(vault, "init"); - // Build cache — note is not trashed + // Build cache — note is not archived let entries = scan_vault_cached(vault).unwrap(); assert_eq!(entries.len(), 1); - assert!(!entries[0].trashed, "note must not be trashed initially"); + assert!(!entries[0].archived, "note must not be archived initially"); - // Simulate trashing the note on disk (update frontmatter directly) - create_test_file(vault, "note.md", "---\nTrashed: true\n---\n# Note\n"); + // Simulate archiving the note on disk (update frontmatter directly) + create_test_file(vault, "note.md", "---\n_archived: true\n---\n# Note\n"); // Stage the change so git sees it - git_add_commit(vault, "trash"); + git_add_commit(vault, "archive"); // Without invalidation, scan_vault_cached uses incremental update. // With invalidation, it must do a full rescan from disk. @@ -907,8 +907,8 @@ mod tests { let entries2 = scan_vault_cached(vault).unwrap(); assert_eq!(entries2.len(), 1); assert!( - entries2[0].trashed, - "note must be trashed after invalidate + rescan" + entries2[0].archived, + "note must be archived after invalidate + rescan" ); } @@ -936,23 +936,6 @@ mod tests { ); } - /// Integration test: `Trashed: Yes` (string) through full cached path. - #[test] - fn test_cached_vault_trashed_yes_string() { - let (_lock, _cache_tmp, dir) = setup_git_vault(); - let vault = dir.path(); - - create_test_file(vault, "trashed-note.md", "---\nTrashed: Yes\n---\n# Gone\n"); - git_add_commit(vault, "init"); - - let entries = scan_vault_cached(vault).unwrap(); - assert_eq!(entries.len(), 1); - assert!( - entries[0].trashed, - "'Trashed: Yes' must be parsed as true through the cached vault path" - ); - } - /// Integration test: stale cache with old version is invalidated and /// re-parses `Archived: Yes` correctly after cache version bump. #[test] diff --git a/src-tauri/src/vault/entry.rs b/src-tauri/src/vault/entry.rs index bc9c34f5..2f3b62ad 100644 --- a/src-tauri/src/vault/entry.rs +++ b/src-tauri/src/vault/entry.rs @@ -26,9 +26,6 @@ pub struct VaultEntry { pub related_to: Vec, pub status: Option, pub archived: bool, - pub trashed: bool, - #[serde(rename = "trashedAt")] - pub trashed_at: Option, #[serde(rename = "modifiedAt")] pub modified_at: Option, #[serde(rename = "createdAt")] diff --git a/src-tauri/src/vault/frontmatter.rs b/src-tauri/src/vault/frontmatter.rs index 3551ecb9..7f34e7b4 100644 --- a/src-tauri/src/vault/frontmatter.rs +++ b/src-tauri/src/vault/frontmatter.rs @@ -19,18 +19,8 @@ pub(crate) struct Frontmatter { deserialize_with = "deserialize_bool_or_string" )] pub archived: Option, - #[serde( - rename = "_trashed", - alias = "Trashed", - alias = "trashed", - default, - deserialize_with = "deserialize_bool_or_string" - )] - pub trashed: Option, #[serde(rename = "Status", alias = "status", default)] pub status: Option, - #[serde(rename = "_trashed_at", alias = "Trashed at", alias = "trashed_at")] - pub trashed_at: Option, #[serde(default)] pub icon: Option, #[serde(default)] @@ -158,12 +148,6 @@ fn parse_frontmatter(data: &HashMap) -> Frontmatter { "_archived", "Archived", "archived", - "_trashed", - "Trashed", - "trashed", - "_trashed_at", - "Trashed at", - "trashed_at", "icon", "color", "order", @@ -199,11 +183,6 @@ const SKIP_KEYS: &[&str] = &[ "aliases", "_archived", "archived", - "_trashed", - "trashed", - "_trashed_at", - "trashed at", - "trashed_at", "icon", "color", "order", diff --git a/src-tauri/src/vault/mod.rs b/src-tauri/src/vault/mod.rs index b77e63ae..96d8583c 100644 --- a/src-tauri/src/vault/mod.rs +++ b/src-tauri/src/vault/mod.rs @@ -23,7 +23,7 @@ pub use rename::{ detect_renames, rename_note, update_wikilinks_for_renames, DetectedRename, RenameResult, }; pub use title_sync::{sync_title_on_open, SyncAction}; -pub use trash::{batch_delete_notes, delete_note, empty_trash, is_file_trashed, purge_trash}; +pub use trash::{batch_delete_notes, delete_note}; pub use views::{ delete_view, evaluate_view, save_view, scan_views, FilterCondition, FilterGroup, FilterNode, FilterOp, ViewDefinition, ViewFile, @@ -98,12 +98,6 @@ pub fn parse_md_file(path: &Path, git_dates: Option<(u64, u64)>) -> Result Vec { links } -/// Parse an ISO 8601 date string to Unix timestamp (seconds since epoch). -/// Handles "2025-05-23T14:35:00.000Z" and "2025-05-23" formats. -pub(super) fn parse_iso_date(date_str: &str) -> Option { - use chrono::{NaiveDate, NaiveDateTime}; - - let trimmed = date_str.trim().trim_matches('"'); - - // Try full datetime with optional fractional seconds and Z suffix - if let Ok(dt) = NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M:%S%.fZ") { - return Some(dt.and_utc().timestamp() as u64); - } - if let Ok(dt) = NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M:%SZ") { - return Some(dt.and_utc().timestamp() as u64); - } - if let Ok(dt) = NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M:%S") { - return Some(dt.and_utc().timestamp() as u64); - } - - // Try date-only - if let Ok(d) = NaiveDate::parse_from_str(trimmed, "%Y-%m-%d") { - return Some(d.and_hms_opt(0, 0, 0)?.and_utc().timestamp() as u64); - } - - None -} - #[cfg(test)] mod tests { use super::*; @@ -747,50 +721,6 @@ mod tests { assert!(!contains_wikilink("only ]] closing")); } - // --- parse_iso_date tests --- - - #[test] - fn test_parse_iso_date_full_datetime_with_z() { - let ts = parse_iso_date("2025-05-23T14:35:00.000Z"); - assert!(ts.is_some()); - assert_eq!(ts.unwrap(), 1748010900); - } - - #[test] - fn test_parse_iso_date_datetime_no_fractional() { - let ts = parse_iso_date("2025-05-23T14:35:00Z"); - assert!(ts.is_some()); - assert_eq!(ts.unwrap(), 1748010900); - } - - #[test] - fn test_parse_iso_date_datetime_no_z() { - let ts = parse_iso_date("2025-05-23T14:35:00"); - assert!(ts.is_some()); - assert_eq!(ts.unwrap(), 1748010900); - } - - #[test] - fn test_parse_iso_date_date_only() { - let ts = parse_iso_date("2025-05-23"); - assert!(ts.is_some()); - assert_eq!(ts.unwrap(), 1747958400); - } - - #[test] - fn test_parse_iso_date_with_quotes_and_whitespace() { - let ts = parse_iso_date(" \"2025-05-23\" "); - assert!(ts.is_some()); - assert_eq!(ts.unwrap(), 1747958400); - } - - #[test] - fn test_parse_iso_date_invalid() { - assert!(parse_iso_date("not-a-date").is_none()); - assert!(parse_iso_date("").is_none()); - assert!(parse_iso_date("2025-13-45").is_none()); - } - // --- extract_outgoing_links tests --- #[test] diff --git a/src-tauri/src/vault/trash.rs b/src-tauri/src/vault/trash.rs index 5cd929c9..743b75c6 100644 --- a/src-tauri/src/vault/trash.rs +++ b/src-tauri/src/vault/trash.rs @@ -1,128 +1,5 @@ -use gray_matter::engine::YAML; -use gray_matter::Matter; use std::fs; -use std::io::Write as IoWrite; -use std::path::{Path, PathBuf}; -use walkdir::WalkDir; - -/// Check if a file path points to a markdown file. -fn is_markdown_file(path: &Path) -> bool { - path.is_file() && path.extension().is_some_and(|ext| ext == "md") -} - -/// Extract the "Trashed at" date string from parsed gray_matter data. -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")) - .or_else(|| map.get("trashed_at"))?; - match pod { - gray_matter::Pod::String(s) => Some(s.clone()), - _ => None, - } -} - -/// Parse a "Trashed at" date string into a NaiveDate. Supports "2026-01-01" and "2026-01-01T..." formats. -fn parse_trashed_date(date_str: &str) -> Option { - let trimmed = date_str.trim().trim_matches('"'); - let date_part = trimmed.split('T').next().unwrap_or(trimmed); - chrono::NaiveDate::parse_from_str(date_part, "%Y-%m-%d").ok() -} - -/// Check if `_trashed` (or aliases) is set to a truthy value in gray_matter data. -fn is_trashed_flag_set(data: &Option) -> bool { - let gray_matter::Pod::Hash(ref map) = data.as_ref().unwrap_or(&gray_matter::Pod::Null) else { - return false; - }; - let Some(pod) = map - .get("_trashed") - .or_else(|| map.get("Trashed")) - .or_else(|| map.get("trashed")) - else { - return false; - }; - match pod { - gray_matter::Pod::Boolean(b) => *b, - gray_matter::Pod::String(s) => { - matches!(s.to_ascii_lowercase().as_str(), "yes" | "true" | "1") - } - _ => false, - } -} - -/// Check whether a file path is strictly inside the given vault root. -fn is_inside_vault(file_path: &Path, vault_root: &Path) -> bool { - match (file_path.canonicalize(), vault_root.canonicalize()) { - (Ok(fp), Ok(vr)) => fp.starts_with(&vr), - _ => false, - } -} - -/// Move a file to OS trash. Falls back to fs::remove_file if OS trash fails. -fn trash_or_remove(path: &Path) -> Result<(), String> { - match trash::delete(path) { - Ok(()) => Ok(()), - Err(trash_err) => { - log::warn!( - "OS trash failed for {}: {} — falling back to fs::remove_file", - path.display(), - trash_err - ); - fs::remove_file(path).map_err(|e| format!("Failed to delete {}: {}", path.display(), e)) - } - } -} - -/// Delete a file (move to OS trash) and log the result. Returns the path string if successful. -fn try_purge_file(path: &Path) -> Option { - match trash_or_remove(path) { - Ok(()) => { - log::info!("Purged trashed file: {}", path.display()); - Some(path.to_string_lossy().to_string()) - } - Err(e) => { - log::warn!("Failed to purge {}: {}", path.display(), e); - None - } - } -} - -/// Append a purge run summary to `.laputa/purge.log`. -fn write_purge_log(vault_path: &Path, checked: usize, purged: &[String], dry_run: bool) { - let log_dir = vault_path.join(".laputa"); - if fs::create_dir_all(&log_dir).is_err() { - log::warn!("Could not create .laputa/ directory for purge log"); - return; - } - let log_path = log_dir.join("purge.log"); - let mut file = match fs::OpenOptions::new() - .create(true) - .append(true) - .open(&log_path) - { - Ok(f) => f, - Err(e) => { - log::warn!("Could not open purge.log: {}", e); - return; - } - }; - let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ"); - let mode = if dry_run { " [DRY-RUN]" } else { "" }; - let _ = writeln!( - file, - "[{}]{} checked={}, purged={}", - now, - mode, - checked, - purged.len() - ); - for path in purged { - let _ = writeln!(file, " - {}", path); - } -} +use std::path::Path; /// Permanently delete a single note file. /// Returns the deleted path on success, or an error if the file doesn't exist. @@ -139,41 +16,6 @@ pub fn delete_note(path: &str) -> Result { Ok(path.to_string()) } -/// Check whether a file's frontmatter marks it as trashed. -/// Returns `true` if `Trashed: true` or `Trashed at` is present. -pub fn is_file_trashed(path: &Path) -> bool { - let content = match fs::read_to_string(path) { - Ok(c) => c, - Err(_) => return false, - }; - let matter = Matter::::new(); - let parsed = matter.parse(&content); - - // Check for "Trashed at" field — its presence implies trashed - if extract_trashed_at_string(&parsed.data).is_some() { - return true; - } - - // 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")) - .or_else(|| map.get("trashed")) - { - return match pod { - gray_matter::Pod::Boolean(b) => *b, - gray_matter::Pod::String(s) => { - matches!(s.to_ascii_lowercase().as_str(), "yes" | "true") - } - _ => false, - }; - } - } - - false -} - /// Delete multiple note files from disk. /// Returns the list of successfully deleted paths. /// Skips files that don't exist or fail to delete (logs warnings). @@ -181,165 +23,23 @@ pub fn batch_delete_notes(paths: &[String]) -> Result, String> { let mut deleted = Vec::new(); for path in paths { let file = Path::new(path.as_str()); - match try_purge_file(file) { - Some(p) => deleted.push(p), - None if !file.exists() => { - log::warn!("File does not exist, skipping: {}", path); + if !file.exists() { + log::warn!("File does not exist, skipping: {}", path); + continue; + } + match fs::remove_file(file) { + Ok(()) => { + log::info!("Permanently deleted note: {}", path); + deleted.push(path.clone()); + } + Err(e) => { + log::warn!("Failed to delete {}: {}", path, e); } - None => {} // try_purge_file already logged the warning } } Ok(deleted) } -/// Scan all markdown files in the vault and delete ALL trashed notes -/// (regardless of age). Returns the list of deleted file paths. -pub fn empty_trash(vault_path: &str) -> Result, String> { - let vault = Path::new(vault_path); - if !vault.exists() || !vault.is_dir() { - return Err(format!( - "Vault path does not exist or is not a directory: {}", - vault_path - )); - } - - let mut deleted = Vec::new(); - for entry in WalkDir::new(vault) - .follow_links(true) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| is_markdown_file(e.path())) - .filter(|e| is_file_trashed(e.path())) - { - if let Some(p) = try_purge_file(entry.path()) { - deleted.push(p); - } - } - - Ok(deleted) -} - -/// Scan all markdown files in the vault and delete those where -/// `_trashed_at` frontmatter is more than 30 days ago. -/// -/// Safety checks enforced per file (all must pass): -/// 1. `_trashed: true` present in frontmatter -/// 2. `_trashed_at` present and parseable as a date -/// 3. Date is strictly more than 30 days ago -/// 4. File exists on disk -/// 5. File path is inside the vault root -/// -/// When `dry_run` is true, no files are deleted — only the list of candidates is returned. -/// Returns the list of purged (or would-be-purged) file paths. -pub fn purge_old_trash(vault_path: &Path, dry_run: bool) -> Result, String> { - if !vault_path.exists() || !vault_path.is_dir() { - return Err(format!( - "Vault path does not exist or is not a directory: {}", - vault_path.display() - )); - } - - let today = chrono::Utc::now().date_naive(); - let matter = Matter::::new(); - let max_age_days = 30; - let mut checked = 0usize; - let mut purged: Vec = Vec::new(); - let mut purged_paths: Vec = Vec::new(); - - for entry in WalkDir::new(vault_path) - .follow_links(true) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| is_markdown_file(e.path())) - { - let path = entry.path(); - - // Safety check 4: file exists - if !path.exists() { - continue; - } - - // Safety check 5: file is inside vault root - if !is_inside_vault(path, vault_path) { - log::warn!("Skipping file outside vault: {}", path.display()); - continue; - } - - let content = match fs::read_to_string(path) { - Ok(c) => c, - Err(_) => continue, - }; - let parsed = matter.parse(&content); - - // Safety check 1: _trashed: true - if !is_trashed_flag_set(&parsed.data) { - continue; - } - - // Safety check 2: _trashed_at present and parseable - let date_str = match extract_trashed_at_string(&parsed.data) { - Some(s) => s, - None => { - log::warn!( - "Trashed file missing _trashed_at, skipping: {}", - path.display() - ); - continue; - } - }; - let trashed_date = match parse_trashed_date(&date_str) { - Some(d) => d, - None => { - log::warn!( - "Unparseable _trashed_at '{}', skipping: {}", - date_str, - path.display() - ); - continue; - } - }; - - // Safety check 3: strictly more than 30 days old - let age = today.signed_duration_since(trashed_date); - if age.num_days() <= max_age_days { - continue; - } - - checked += 1; - - if dry_run { - log::info!( - "[DRY-RUN] Would purge: {} (trashed {} days ago)", - path.display(), - age.num_days() - ); - purged.push(path.to_string_lossy().to_string()); - purged_paths.push(path.to_path_buf()); - } else if let Some(p) = try_purge_file(path) { - purged.push(p); - purged_paths.push(path.to_path_buf()); - } - } - - write_purge_log(vault_path, checked, &purged, dry_run); - - Ok(purged_paths) -} - -/// Legacy wrapper that calls purge_old_trash with dry_run=false and returns string paths. -/// Used by the Tauri command and startup tasks. -pub fn purge_trash(vault_path: &str) -> Result, String> { - let vault = Path::new(vault_path); - purge_old_trash(vault, false) - .map(|paths| { - paths - .into_iter() - .map(|p| p.to_string_lossy().to_string()) - .collect() - }) - .map_err(|e| e.to_string()) -} - #[cfg(test)] mod tests { use super::*; @@ -377,285 +77,6 @@ mod tests { assert!(result.unwrap_err().contains("does not exist")); } - fn old_date(days_ago: i64) -> String { - (chrono::Utc::now().date_naive() - chrono::Duration::days(days_ago)) - .format("%Y-%m-%d") - .to_string() - } - - fn trashed_content(date: &str) -> String { - format!( - "---\n_trashed: true\n_trashed_at: \"{}\"\n---\n# Trashed\n", - date - ) - } - - #[test] - fn test_purge_old_trash_deletes_old_trashed_files() { - let dir = TempDir::new().unwrap(); - create_test_file(dir.path(), "old.md", &trashed_content("2025-01-01")); - create_test_file(dir.path(), "recent.md", &trashed_content(&old_date(5))); - create_test_file(dir.path(), "normal.md", "---\ntype: Note\n---\n# Normal\n"); - - let purged = purge_old_trash(dir.path(), false).unwrap(); - assert_eq!(purged.len(), 1); - assert!(!dir.path().join("old.md").exists()); - assert!(dir.path().join("recent.md").exists()); - assert!(dir.path().join("normal.md").exists()); - } - - #[test] - fn test_purge_old_trash_supports_datetime_format() { - let dir = TempDir::new().unwrap(); - create_test_file( - dir.path(), - "dt.md", - "---\n_trashed: true\n_trashed_at: \"2025-01-01T10:30:00Z\"\n---\n# DT\n", - ); - - let purged = purge_old_trash(dir.path(), false).unwrap(); - assert_eq!(purged.len(), 1); - } - - #[test] - fn test_purge_old_trash_empty_vault() { - let dir = TempDir::new().unwrap(); - let purged = purge_old_trash(dir.path(), false).unwrap(); - assert!(purged.is_empty()); - } - - #[test] - fn test_purge_old_trash_nonexistent_path() { - let result = purge_old_trash(Path::new("/nonexistent/path"), false); - assert!(result.is_err()); - } - - #[test] - fn test_purge_old_trash_exactly_30_days_not_deleted() { - let dir = TempDir::new().unwrap(); - create_test_file(dir.path(), "borderline.md", &trashed_content(&old_date(30))); - - let purged = purge_old_trash(dir.path(), false).unwrap(); - assert!(purged.is_empty()); - assert!(dir.path().join("borderline.md").exists()); - } - - #[test] - fn test_purge_old_trash_31_days_deleted() { - let dir = TempDir::new().unwrap(); - create_test_file(dir.path(), "expired.md", &trashed_content(&old_date(31))); - - let purged = purge_old_trash(dir.path(), false).unwrap(); - assert_eq!(purged.len(), 1); - assert!(!dir.path().join("expired.md").exists()); - } - - #[test] - fn test_purge_old_trash_nested_directories() { - let dir = TempDir::new().unwrap(); - create_test_file( - dir.path(), - "sub/deep/old.md", - &trashed_content("2025-01-01"), - ); - - let purged = purge_old_trash(dir.path(), false).unwrap(); - assert_eq!(purged.len(), 1); - } - - #[test] - fn test_purge_old_trash_dry_run_does_not_delete() { - let dir = TempDir::new().unwrap(); - create_test_file(dir.path(), "old.md", &trashed_content("2025-01-01")); - - let purged = purge_old_trash(dir.path(), true).unwrap(); - assert_eq!(purged.len(), 1); - assert!( - dir.path().join("old.md").exists(), - "dry-run must not delete files" - ); - } - - #[test] - fn test_purge_old_trash_missing_trashed_flag_skips() { - let dir = TempDir::new().unwrap(); - // Has _trashed_at but no _trashed: true - create_test_file( - dir.path(), - "no-flag.md", - "---\n_trashed_at: \"2025-01-01\"\n---\n# No flag\n", - ); - - let purged = purge_old_trash(dir.path(), false).unwrap(); - assert!(purged.is_empty()); - assert!(dir.path().join("no-flag.md").exists()); - } - - #[test] - fn test_purge_old_trash_missing_trashed_at_skips() { - let dir = TempDir::new().unwrap(); - // Has _trashed: true but no _trashed_at - create_test_file( - dir.path(), - "no-date.md", - "---\n_trashed: true\n---\n# No date\n", - ); - - let purged = purge_old_trash(dir.path(), false).unwrap(); - assert!(purged.is_empty()); - assert!(dir.path().join("no-date.md").exists()); - } - - #[test] - fn test_purge_old_trash_unparseable_date_skips() { - let dir = TempDir::new().unwrap(); - create_test_file( - dir.path(), - "bad-date.md", - "---\n_trashed: true\n_trashed_at: \"not-a-date\"\n---\n# Bad date\n", - ); - - let purged = purge_old_trash(dir.path(), false).unwrap(); - assert!(purged.is_empty()); - assert!(dir.path().join("bad-date.md").exists()); - } - - #[test] - fn test_purge_old_trash_trashed_false_skips() { - let dir = TempDir::new().unwrap(); - create_test_file( - dir.path(), - "not-trashed.md", - "---\n_trashed: false\n_trashed_at: \"2025-01-01\"\n---\n# Not trashed\n", - ); - - let purged = purge_old_trash(dir.path(), false).unwrap(); - assert!(purged.is_empty()); - assert!(dir.path().join("not-trashed.md").exists()); - } - - #[test] - fn test_purge_old_trash_legacy_field_names() { - let dir = TempDir::new().unwrap(); - create_test_file( - dir.path(), - "legacy.md", - "---\nTrashed: true\nTrashed at: \"2025-01-01\"\n---\n# Legacy\n", - ); - - let purged = purge_old_trash(dir.path(), false).unwrap(); - assert_eq!(purged.len(), 1); - assert!(!dir.path().join("legacy.md").exists()); - } - - #[test] - fn test_purge_old_trash_writes_purge_log() { - let dir = TempDir::new().unwrap(); - create_test_file(dir.path(), "old.md", &trashed_content("2025-01-01")); - - let _ = purge_old_trash(dir.path(), false).unwrap(); - let log_path = dir.path().join(".laputa/purge.log"); - assert!(log_path.exists(), "purge.log must be created"); - let log_content = fs::read_to_string(&log_path).unwrap(); - assert!(log_content.contains("purged=1")); - assert!(log_content.contains("old.md")); - } - - #[test] - fn test_purge_old_trash_wrapper_returns_strings() { - let dir = TempDir::new().unwrap(); - create_test_file(dir.path(), "old.md", &trashed_content("2025-01-01")); - - let deleted = purge_trash(dir.path().to_str().unwrap()).unwrap(); - assert_eq!(deleted.len(), 1); - assert!(deleted[0].contains("old.md")); - } - - #[test] - fn test_is_file_trashed_with_trashed_true() { - let dir = TempDir::new().unwrap(); - create_test_file( - dir.path(), - "trashed.md", - "---\nTrashed: true\n---\n# Gone\n", - ); - assert!(is_file_trashed(&dir.path().join("trashed.md"))); - } - - #[test] - fn test_is_file_trashed_with_trashed_at() { - let dir = TempDir::new().unwrap(); - create_test_file( - dir.path(), - "trashed.md", - "---\nTrashed at: \"2026-01-01\"\n---\n# Gone\n", - ); - assert!(is_file_trashed(&dir.path().join("trashed.md"))); - } - - #[test] - fn test_is_file_trashed_with_trashed_yes() { - let dir = TempDir::new().unwrap(); - create_test_file(dir.path(), "trashed.md", "---\nTrashed: Yes\n---\n# Gone\n"); - assert!(is_file_trashed(&dir.path().join("trashed.md"))); - } - - #[test] - fn test_is_file_trashed_normal_note() { - let dir = TempDir::new().unwrap(); - create_test_file(dir.path(), "normal.md", "---\ntype: Note\n---\n# Normal\n"); - assert!(!is_file_trashed(&dir.path().join("normal.md"))); - } - - #[test] - fn test_is_file_trashed_archived_not_trashed() { - let dir = TempDir::new().unwrap(); - create_test_file( - dir.path(), - "archived.md", - "---\nArchived: true\n---\n# Archived\n", - ); - assert!(!is_file_trashed(&dir.path().join("archived.md"))); - } - - #[test] - fn test_is_file_trashed_nonexistent_file() { - 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(); - create_test_file( - dir.path(), - "active.md", - "---\nTrashed: false\n---\n# Active\n", - ); - assert!(!is_file_trashed(&dir.path().join("active.md"))); - } - #[test] fn test_batch_delete_notes_removes_files() { let dir = TempDir::new().unwrap(); @@ -687,49 +108,4 @@ mod tests { assert_eq!(deleted.len(), 1); assert!(!dir.path().join("exists.md").exists()); } - - #[test] - fn test_empty_trash_deletes_all_trashed() { - let dir = TempDir::new().unwrap(); - // Recently trashed — should be deleted - let recent = chrono::Utc::now() - .date_naive() - .format("%Y-%m-%d") - .to_string(); - create_test_file( - dir.path(), - "recent.md", - &format!( - "---\nTrashed: true\nTrashed at: \"{}\"\n---\n# Recent\n", - recent - ), - ); - // Old trashed — should be deleted - create_test_file( - dir.path(), - "old.md", - "---\nTrashed: true\nTrashed at: \"2025-01-01\"\n---\n# Old\n", - ); - // Not trashed — should be kept - create_test_file(dir.path(), "normal.md", "---\ntype: Note\n---\n# Normal\n"); - - let deleted = empty_trash(dir.path().to_str().unwrap()).unwrap(); - assert_eq!(deleted.len(), 2); - assert!(!dir.path().join("recent.md").exists()); - assert!(!dir.path().join("old.md").exists()); - assert!(dir.path().join("normal.md").exists()); - } - - #[test] - fn test_empty_trash_empty_vault() { - let dir = TempDir::new().unwrap(); - let deleted = empty_trash(dir.path().to_str().unwrap()).unwrap(); - assert!(deleted.is_empty()); - } - - #[test] - fn test_empty_trash_nonexistent_path() { - let result = empty_trash("/nonexistent/path/that/does/not/exist"); - assert!(result.is_err()); - } } diff --git a/src-tauri/src/vault/views.rs b/src-tauri/src/vault/views.rs index eb352ff4..2d1e8c99 100644 --- a/src-tauri/src/vault/views.rs +++ b/src-tauri/src/vault/views.rs @@ -296,7 +296,6 @@ fn evaluate_condition(cond: &FilterCondition, entry: &VaultEntry) -> bool { // Boolean fields match field { "archived" => return evaluate_bool_field(entry.archived, &cond.op, &cond.value), - "trashed" => return evaluate_bool_field(entry.trashed, &cond.op, &cond.value), "favorite" => return evaluate_bool_field(entry.favorite, &cond.op, &cond.value), _ => {} } diff --git a/src/App.tsx b/src/App.tsx index 9bc2ab05..2f7b8cca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -339,8 +339,6 @@ function App() { }) const deleteActions = useDeleteActions({ - vaultPath: resolvedPath, - entries: vault.entries, onDeselectNote: (path: string) => { if (notes.activeTabPath === path) notes.closeAllTabs() }, removeEntry: vault.removeEntry, setToastMessage, @@ -467,7 +465,7 @@ function App() { onCreateNoteOfType: notes.handleCreateNoteImmediate, onSave: appSave.handleSave, onOpenSettings: dialogs.openSettings, - onTrashNote: entryActions.handleTrashNote, onRestoreNote: entryActions.handleRestoreNote, + onDeleteNote: deleteActions.handleDeleteNote, onArchiveNote: entryActions.handleArchiveNote, onUnarchiveNote: entryActions.handleUnarchiveNote, onCommitPush: commitFlow.openCommitDialog, onPull: autoSync.triggerSync, @@ -495,8 +493,6 @@ function App() { onInstallMcp: installMcp, claudeCodeStatus: claudeCodeStatus ?? undefined, claudeCodeVersion: claudeCodeVersion ?? undefined, - onEmptyTrash: deleteActions.handleEmptyTrash, - trashedCount: deleteActions.trashedCount, onReloadVault: vault.reloadVault, onRepairVault: handleRepairVault, onSetNoteIcon: handleSetNoteIconCommand, @@ -589,7 +585,7 @@ function App() { {selection.kind === 'filter' && selection.filter === 'pulse' ? ( setViewMode('all')} /> ) : ( - diffToggleRef.current()} views={vault.views} visibleNotesRef={visibleNotesRef} /> + diffToggleRef.current()} views={vault.views} visibleNotesRef={visibleNotesRef} /> )} @@ -624,8 +620,6 @@ function App() { noteListFilter={aiNoteListFilter} onToggleFavorite={entryActions.handleToggleFavorite} onToggleOrganized={entryActions.handleToggleOrganized} - onTrashNote={entryActions.handleTrashNote} - onRestoreNote={entryActions.handleRestoreNote} onDeleteNote={deleteActions.handleDeleteNote} onArchiveNote={entryActions.handleArchiveNote} onUnarchiveNote={entryActions.handleUnarchiveNote} diff --git a/src/components/AiPanel.test.tsx b/src/components/AiPanel.test.tsx index c5542cec..ecea89bc 100644 --- a/src/components/AiPanel.test.tsx +++ b/src/components/AiPanel.test.tsx @@ -31,8 +31,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, diff --git a/src/components/BreadcrumbBar.test.tsx b/src/components/BreadcrumbBar.test.tsx index aafc5b1b..7a93fb41 100644 --- a/src/components/BreadcrumbBar.test.tsx +++ b/src/components/BreadcrumbBar.test.tsx @@ -15,8 +15,6 @@ const baseEntry: VaultEntry = { owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 100, @@ -33,12 +31,6 @@ const archivedEntry: VaultEntry = { archived: true, } -const trashedEntry: VaultEntry = { - ...baseEntry, - trashed: true, - trashedAt: Date.now() / 1000 - 86400 * 5, -} - const defaultProps = { wordCount: 100, showDiffToggle: false, @@ -55,31 +47,17 @@ describe('BreadcrumbBar — drag region', () => { }) }) -describe('BreadcrumbBar — trash/restore', () => { - it('shows trash button for non-trashed note', () => { - render() - expect(screen.getByTitle('Move to trash (Cmd+Delete)')).toBeInTheDocument() - expect(screen.queryByTitle('Restore from trash')).not.toBeInTheDocument() +describe('BreadcrumbBar — delete', () => { + it('shows delete button', () => { + render() + expect(screen.getByTitle('Delete (Cmd+Delete)')).toBeInTheDocument() }) - it('shows restore button for trashed note', () => { - render() - expect(screen.getByTitle('Restore from trash')).toBeInTheDocument() - expect(screen.queryByTitle('Move to trash (Cmd+Delete)')).not.toBeInTheDocument() - }) - - it('calls onTrash when trash button is clicked', () => { - const onTrash = vi.fn() - render() - fireEvent.click(screen.getByTitle('Move to trash (Cmd+Delete)')) - expect(onTrash).toHaveBeenCalledOnce() - }) - - it('calls onRestore when restore button is clicked', () => { - const onRestore = vi.fn() - render() - fireEvent.click(screen.getByTitle('Restore from trash')) - expect(onRestore).toHaveBeenCalledOnce() + it('calls onDelete when delete button is clicked', () => { + const onDelete = vi.fn() + render() + fireEvent.click(screen.getByTitle('Delete (Cmd+Delete)')) + expect(onDelete).toHaveBeenCalledOnce() }) }) diff --git a/src/components/BreadcrumbBar.tsx b/src/components/BreadcrumbBar.tsx index fa414dd9..67ccb030 100644 --- a/src/components/BreadcrumbBar.tsx +++ b/src/components/BreadcrumbBar.tsx @@ -10,7 +10,6 @@ import { SlidersHorizontal, DotsThree, Trash, - ArrowCounterClockwise, Archive, ArrowUUpLeft, Star, @@ -34,8 +33,7 @@ interface BreadcrumbBarProps { onToggleInspector?: () => void onToggleFavorite?: () => void onToggleOrganized?: () => void - onTrash?: () => void - onRestore?: () => void + onDelete?: () => void onArchive?: () => void onUnarchive?: () => void /** Ref for direct DOM manipulation — avoids re-render on scroll. */ @@ -62,7 +60,7 @@ function RawToggleButton({ rawMode, onToggleRaw }: { rawMode?: boolean; onToggle function BreadcrumbActions({ entry, showDiffToggle, diffMode, diffLoading, onToggleDiff, rawMode, onToggleRaw, forceRawMode, showAIChat, onToggleAIChat, inspectorCollapsed, onToggleInspector, - onToggleFavorite, onToggleOrganized, onTrash, onRestore, onArchive, onUnarchive, + onToggleFavorite, onToggleOrganized, onDelete, onArchive, onUnarchive, }: Omit) { return (
@@ -151,23 +149,13 @@ function BreadcrumbActions({ entry, showDiffToggle, diffMode, diffLoading, onTog )} - {entry.trashed ? ( - - ) : ( - - )} + {inspectorCollapsed && ( - - - - ) -} - -function renderArchivedActions(onUnarchive: (() => void) | undefined, onTrash: () => void) { +function renderArchivedActions(onUnarchive: (() => void) | undefined, onDelete: () => void) { return ( <> - ) } -function renderDefaultActions(onArchive: () => void, onTrash: () => void) { +function renderDefaultActions(onArchive: () => void, onDelete: () => void) { return ( <> - ) } -function BulkActionBarInner({ count, isTrashView, isArchivedView, onArchive, onTrash, onRestore, onDeletePermanently, onUnarchive, onClear }: BulkActionBarProps) { +function BulkActionBarInner({ count, isArchivedView, onArchive, onDelete, onUnarchive, onClear }: BulkActionBarProps) { return (
- {isTrashView ? renderTrashActions(onRestore, onArchive, onDeletePermanently) - : isArchivedView ? renderArchivedActions(onUnarchive, onTrash) - : renderDefaultActions(onArchive, onTrash)} + {isArchivedView ? renderArchivedActions(onUnarchive, onDelete) + : renderDefaultActions(onArchive, onDelete)}
)} diff --git a/src/components/FilterBuilder.test.tsx b/src/components/FilterBuilder.test.tsx index 3203b569..a97fba16 100644 --- a/src/components/FilterBuilder.test.tsx +++ b/src/components/FilterBuilder.test.tsx @@ -15,8 +15,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, @@ -35,7 +33,6 @@ const entries: VaultEntry[] = [ makeEntry({ path: '/vault/topic/ai.md', filename: 'ai.md', title: 'AI Research', isA: 'Topic' }), makeEntry({ path: '/vault/note/plain.md', filename: 'plain.md', title: 'Plain Note', isA: null }), makeEntry({ path: '/vault/person/alice.md', filename: 'alice.md', title: 'Alice Smith', isA: 'Person', aliases: ['Alice'] }), - makeEntry({ path: '/vault/trashed.md', filename: 'trashed.md', title: 'Trashed Note', isA: null, trashed: true }), ] describe('FilterBuilder wikilink autocomplete', () => { @@ -129,15 +126,6 @@ describe('FilterBuilder wikilink autocomplete', () => { expect(screen.queryByTestId('wikilink-dropdown')).not.toBeInTheDocument() }) - it('excludes trashed notes from autocomplete', () => { - renderWithEntries({ - all: [{ field: 'title', op: 'contains', value: '[[Trashed' }], - }) - const input = screen.getByTestId('filter-value-input') - fireEvent.focus(input) - expect(screen.queryByText('Trashed Note')).not.toBeInTheDocument() - }) - it('matches on aliases', () => { renderWithEntries({ all: [{ field: 'title', op: 'contains', value: '[[Alice' }], diff --git a/src/components/FilterBuilder.tsx b/src/components/FilterBuilder.tsx index 5b55882e..f7b8ef1b 100644 --- a/src/components/FilterBuilder.tsx +++ b/src/components/FilterBuilder.tsx @@ -127,7 +127,7 @@ function matchWikilinkEntries(entries: VaultEntry[], typeEntryMap: Record !e.trashed && entryMatchesQuery(e, lowerQuery)) + .filter(e => entryMatchesQuery(e, lowerQuery)) .slice(0, MAX_WIKILINK_RESULTS) .map(e => toWikilinkMatch(e, typeEntryMap)) } diff --git a/src/components/Inspector.test.tsx b/src/components/Inspector.test.tsx index c340cd5c..ab5b4899 100644 --- a/src/components/Inspector.test.tsx +++ b/src/components/Inspector.test.tsx @@ -15,8 +15,6 @@ const mockEntry: VaultEntry = { owner: 'Luca Rossi', cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1707900000, createdAt: null, fileSize: 1024, @@ -60,8 +58,6 @@ const referrerEntry: VaultEntry = { owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1707900000, createdAt: null, fileSize: 200, @@ -345,8 +341,6 @@ This is a test note with some words to count. owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1707900000, createdAt: null, fileSize: 500, @@ -372,8 +366,6 @@ This is a test note with some words to count. owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1707900000, createdAt: null, fileSize: 300, @@ -399,8 +391,6 @@ This is a test note with some words to count. owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1707900000, createdAt: null, fileSize: 400, @@ -426,8 +416,6 @@ This is a test note with some words to count. owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1707900000, createdAt: null, fileSize: 200, @@ -569,8 +557,6 @@ Status: Active owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1707900000, createdAt: null, fileSize: 300, diff --git a/src/components/InspectorPanels.test.tsx b/src/components/InspectorPanels.test.tsx index 6a9d1d5c..38baa198 100644 --- a/src/components/InspectorPanels.test.tsx +++ b/src/components/InspectorPanels.test.tsx @@ -19,8 +19,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, @@ -261,21 +259,6 @@ describe('DynamicRelationshipsPanel', () => { expect(screen.getByTitle('Archived')).toBeInTheDocument() }) - it('shows trashed indicator for trashed entries', () => { - const trashedEntry = makeEntry({ - path: '/vault/project/trash.md', filename: 'trash.md', title: 'Trash Project', isA: 'Project', trashed: true, - }) - render( - - ) - expect(screen.getByTitle('Trashed')).toBeInTheDocument() - }) - it('handles aliased wikilinks [[path|Display]]', () => { render( { expect(screen.getByTitle('Archived')).toBeInTheDocument() }) - it('shows trashed indicator for trashed entries', () => { - const items: ReferencedByItem[] = [ - { entry: makeEntry({ path: '/vault/a.md', title: 'Trash Note', trashed: true }), viaKey: 'Has' }, - ] - render() - expect(screen.getByTitle('Trashed')).toBeInTheDocument() - }) }) describe('GitHistoryPanel', () => { @@ -873,18 +849,6 @@ describe('InstancesPanel', () => { expect(buttons[2].textContent).toContain('Q1 2026') }) - it('excludes trashed instances', () => { - const instances = [ - makeEntry({ path: '/vault/quarter/q1.md', title: 'Q1 2026', isA: 'Quarter', modifiedAt: 2000 }), - makeEntry({ path: '/vault/quarter/q2.md', title: 'Q2 Trashed', isA: 'Quarter', trashed: true, modifiedAt: 3000 }), - ] - render( - - ) - expect(screen.getByText('Q1 2026')).toBeInTheDocument() - expect(screen.queryByText('Q2 Trashed')).not.toBeInTheDocument() - }) - it('dims archived instances', () => { const instances = [ makeEntry({ path: '/vault/quarter/old.md', title: 'Q4 2024', isA: 'Quarter', archived: true, modifiedAt: 1000 }), diff --git a/src/components/NoteAutocomplete.test.tsx b/src/components/NoteAutocomplete.test.tsx index e4b14815..d6b9cb48 100644 --- a/src/components/NoteAutocomplete.test.tsx +++ b/src/components/NoteAutocomplete.test.tsx @@ -15,8 +15,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, diff --git a/src/components/NoteItem.test.tsx b/src/components/NoteItem.test.tsx index 3d0caf71..d50c6ab1 100644 --- a/src/components/NoteItem.test.tsx +++ b/src/components/NoteItem.test.tsx @@ -9,7 +9,7 @@ function makeEntry(overrides: Partial = {}): VaultEntry { return { path: '/vault/test.md', filename: 'test.md', title: 'Test Note', isA: 'Movie', aliases: [], belongsTo: [], relatedTo: [], - status: null, archived: false, trashed: false, trashedAt: null, + status: null, archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 100, snippet: 'A snippet', wordCount: 50, relationships: {}, icon: null, color: null, order: null, diff --git a/src/components/NoteItem.tsx b/src/components/NoteItem.tsx index 852c7c4c..1b88bab5 100644 --- a/src/components/NoteItem.tsx +++ b/src/components/NoteItem.tsx @@ -29,26 +29,6 @@ export function getTypeIcon(isA: string | null, customIcon?: string | null): Com return (isA && TYPE_ICON_MAP[isA]) || FileText } -const THIRTY_DAYS_SECS = 86400 * 30 - -function TrashDateLine({ entry }: { entry: VaultEntry }) { - const { isExpired, suffix } = useMemo(() => { - // eslint-disable-next-line react-hooks/purity -- Date.now() intentionally memoized on trashedAt - const trashedAge = entry.trashedAt ? (Date.now() / 1000 - entry.trashedAt) : 0 - const expired = trashedAge >= THIRTY_DAYS_SECS - return { - isExpired: expired, - suffix: expired ? ' — will be permanently deleted' : '', - } - }, [entry.trashedAt]) - const style = isExpired ? { color: 'var(--destructive)', fontWeight: 500 } as const : undefined - return ( -
- Trashed {relativeDate(entry.trashedAt)}{suffix} -
- ) -} - const NOTE_STATUS_DOT: Record = { pendingSave: { color: 'var(--accent-green)', testId: 'pending-save-indicator', title: 'Saving to disk…' }, new: { color: 'var(--accent-green)', testId: 'new-indicator', title: 'New (uncommitted)' }, @@ -68,7 +48,7 @@ function StatusDot({ noteStatus }: { noteStatus: NoteStatus }) { ) } -function StateBadge({ archived, trashed }: { archived: boolean; trashed: boolean }) { +function StateBadge({ archived }: { archived: boolean }) { if (archived) { return ( @@ -76,13 +56,6 @@ function StateBadge({ archived, trashed }: { archived: boolean; trashed: boolean ) } - if (trashed) { - return ( - - TRASHED - - ) - } return null } @@ -236,7 +209,7 @@ export function NoteItem({ entry, isSelected, isMultiSelected = false, isHighlig {noteStatus !== 'clean' && !isBinary && } {entry.icon && isEmoji(entry.icon) && {entry.icon}} {entry.title} - {!isBinary && } + {!isBinary && }
{entry.snippet && !isBinary && ( @@ -247,9 +220,8 @@ export function NoteItem({ entry, isSelected, isMultiSelected = false, isHighlig {!isBinary && te?.listPropertiesDisplay && te.listPropertiesDisplay.length > 0 && ( )} - {!isBinary && (entry.trashed && entry.trashedAt - ? - :
{relativeDate(getDisplayDate(entry))}
+ {!isBinary && ( +
{relativeDate(getDisplayDate(entry))}
)} )} diff --git a/src/components/NoteList.test.tsx b/src/components/NoteList.test.tsx index 4ce9ced0..213b8beb 100644 --- a/src/components/NoteList.test.tsx +++ b/src/components/NoteList.test.tsx @@ -25,8 +25,6 @@ const mockEntries: VaultEntry[] = [ owner: 'Luca', cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 1024, @@ -54,8 +52,6 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 847, @@ -84,8 +80,6 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 320, @@ -111,8 +105,6 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 512, @@ -138,8 +130,6 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 256, @@ -158,7 +148,7 @@ const mockEntries: VaultEntry[] = [ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ path: '/test.md', filename: 'test.md', title: 'Test', isA: null, aliases: [], belongsTo: [], relatedTo: [], status: null, - archived: false, trashed: false, trashedAt: null, + archived: false, modifiedAt: null, createdAt: null, fileSize: 100, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, view: null, visible: null, @@ -685,95 +675,7 @@ describe('NoteList sort controls', () => { }) }) -// --- Trash feature tests --- - -const trashedEntry: VaultEntry = { - path: '/vault/note/old-draft.md', - filename: 'old-draft.md', - title: 'Old Draft Notes', - isA: 'Note', - aliases: [], - belongsTo: [], - relatedTo: [], - status: null, - owner: null, - cadence: null, - archived: false, - trashed: true, - trashedAt: Date.now() / 1000 - 86400 * 5, - modifiedAt: 1700000000, - createdAt: null, - fileSize: 280, - snippet: 'Some draft content that is no longer needed.', - wordCount: 0, - relationships: {}, - icon: null, - color: null, - order: null, - template: null, sort: null, - outgoingLinks: [], - properties: {}, -} - -const expiredTrashedEntry: VaultEntry = { - path: '/vault/note/deprecated-api.md', - filename: 'deprecated-api.md', - title: 'Deprecated API Notes', - isA: 'Note', - aliases: [], - belongsTo: [], - relatedTo: [], - status: null, - owner: null, - cadence: null, - archived: false, - trashed: true, - trashedAt: Date.now() / 1000 - 86400 * 35, - modifiedAt: 1700000000, - createdAt: null, - fileSize: 190, - snippet: 'Old API docs replaced by v2.', - wordCount: 0, - relationships: {}, - icon: null, - color: null, - order: null, - template: null, sort: null, - outgoingLinks: [], - properties: {}, -} - -const entriesWithTrashed = [...mockEntries, trashedEntry, expiredTrashedEntry] - -describe('filterEntries — trash', () => { - it('excludes trashed entries from "all" filter', () => { - const result = filterEntries(entriesWithTrashed, { kind: 'filter', filter: 'all' }) - expect(result.find((e) => e.title === 'Old Draft Notes')).toBeUndefined() - expect(result.find((e) => e.title === 'Build Laputa App')).toBeDefined() - }) - - it('excludes trashed entries from section group', () => { - const result = filterEntries(entriesWithTrashed, { kind: 'sectionGroup', type: 'Note' }) - expect(result.find((e) => e.title === 'Old Draft Notes')).toBeUndefined() - }) - - it('trash filter returns only trashed entries', () => { - const result = filterEntries(entriesWithTrashed, { kind: 'filter', filter: 'trash' }) - expect(result).toHaveLength(2) - expect(result.every((e) => e.trashed)).toBe(true) - }) - - it('archived filter excludes trashed entries', () => { - const archivedAndTrashed = [ - ...mockEntries, - { ...mockEntries[0], path: '/archived.md', archived: true, trashed: false, trashedAt: null, title: 'Archived Note' }, - trashedEntry, - ] - const result = filterEntries(archivedAndTrashed, { kind: 'filter', filter: 'archived' }) - expect(result.find((e) => e.title === 'Archived Note')).toBeDefined() - expect(result.find((e) => e.title === 'Old Draft Notes')).toBeUndefined() - }) - +describe('filterEntries — entity', () => { it('entity filter returns empty (entity view uses relationship groups instead)', () => { const result = filterEntries(mockEntries, { kind: 'entity', entry: mockEntries[4] }) expect(result).toHaveLength(0) @@ -828,39 +730,6 @@ describe('NoteList — status indicators', () => { }) }) -describe('NoteList — trash view', () => { - const trashSelection: SidebarSelection = { kind: 'filter', filter: 'trash' } - - it('shows "Trash" header when trash filter is active', () => { - render() - expect(screen.getByText('Trash')).toBeInTheDocument() - }) - - it('shows only trashed entries in trash view', () => { - render() - expect(screen.getByText('Old Draft Notes')).toBeInTheDocument() - expect(screen.getByText('Deprecated API Notes')).toBeInTheDocument() - expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() - }) - - it('shows TRASHED badge on trashed entries', () => { - render() - const badges = screen.getAllByText('TRASHED') - expect(badges.length).toBeGreaterThanOrEqual(1) - }) - - it('shows 30-day warning banner when expired notes exist', () => { - render() - expect(screen.getByText('Notes in trash for 30+ days will be permanently deleted')).toBeInTheDocument() - expect(screen.getByText(/1 note is past the 30-day retention period/)).toBeInTheDocument() - }) - - it('shows "Trash is empty" when no trashed entries', () => { - render() - expect(screen.getByText('Trash is empty')).toBeInTheDocument() - }) -}) - // --- Virtual list performance tests --- describe('NoteList — virtual list with large datasets', () => { @@ -1214,10 +1083,10 @@ describe('NoteList — multi-select', () => { it.each([ { label: 'bulk archive via button', prop: 'onBulkArchive', trigger: () => fireEvent.click(screen.getByTestId('bulk-archive-btn')) }, - { label: 'bulk trash via button', prop: 'onBulkTrash', trigger: () => fireEvent.click(screen.getByTestId('bulk-trash-btn')) }, + { label: 'bulk delete via button', prop: 'onBulkDeletePermanently', trigger: () => fireEvent.click(screen.getByTestId('bulk-delete-btn')) }, { label: 'Cmd+E archives', prop: 'onBulkArchive', trigger: () => fireEvent.keyDown(window, { key: 'e', metaKey: true }) }, - { label: 'Cmd+Backspace trashes', prop: 'onBulkTrash', trigger: () => fireEvent.keyDown(window, { key: 'Backspace', metaKey: true }) }, - { label: 'Cmd+Delete trashes', prop: 'onBulkTrash', trigger: () => fireEvent.keyDown(window, { key: 'Delete', metaKey: true }) }, + { label: 'Cmd+Backspace deletes', prop: 'onBulkDeletePermanently', trigger: () => fireEvent.keyDown(window, { key: 'Backspace', metaKey: true }) }, + { label: 'Cmd+Delete deletes', prop: 'onBulkDeletePermanently', trigger: () => fireEvent.keyDown(window, { key: 'Delete', metaKey: true }) }, ])('$label selected notes and clears selection', ({ prop, trigger }) => { const handler = vi.fn() selectTwoNotes({ [prop]: handler }) @@ -1254,8 +1123,6 @@ const typeEntry: VaultEntry = { owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, @@ -1335,28 +1202,19 @@ describe('NoteList — traffic light padding when sidebar collapsed', () => { }) describe('countByFilter', () => { - it('counts open, archived, and trashed notes per type', () => { + it('counts open and archived notes per type', () => { const entries = [ makeEntry({ path: '/1.md', isA: 'Project' }), makeEntry({ path: '/2.md', isA: 'Project', archived: true }), - makeEntry({ path: '/3.md', isA: 'Project', trashed: true }), - makeEntry({ path: '/4.md', isA: 'Project' }), - makeEntry({ path: '/5.md', isA: 'Note' }), + makeEntry({ path: '/3.md', isA: 'Project' }), + makeEntry({ path: '/4.md', isA: 'Note' }), ] const counts = countByFilter(entries, 'Project') - expect(counts).toEqual({ open: 2, archived: 1, trashed: 1 }) + expect(counts).toEqual({ open: 2, archived: 1 }) }) it('returns zeros when type has no entries', () => { - expect(countByFilter([], 'Project')).toEqual({ open: 0, archived: 0, trashed: 0 }) - }) - - it('counts trashed note that is also archived as trashed only', () => { - const entries = [ - makeEntry({ path: '/1.md', isA: 'Project', archived: true, trashed: true }), - ] - const counts = countByFilter(entries, 'Project') - expect(counts).toEqual({ open: 0, archived: 0, trashed: 1 }) + expect(countByFilter([], 'Project')).toEqual({ open: 0, archived: 0 }) }) }) @@ -1365,7 +1223,6 @@ describe('NoteList — filter pills', () => { makeEntry({ path: '/p1.md', title: 'Open Project 1', isA: 'Project' }), makeEntry({ path: '/p2.md', title: 'Open Project 2', isA: 'Project' }), makeEntry({ path: '/p3.md', title: 'Archived Project', isA: 'Project', archived: true }), - makeEntry({ path: '/p4.md', title: 'Trashed Project', isA: 'Project', trashed: true, trashedAt: 1700000000 }), makeEntry({ path: '/n1.md', title: 'Some Note', isA: 'Note' }), ] @@ -1376,7 +1233,6 @@ describe('NoteList — filter pills', () => { expect(screen.getByTestId('filter-pills')).toBeInTheDocument() expect(screen.getByTestId('filter-pill-open')).toBeInTheDocument() expect(screen.getByTestId('filter-pill-archived')).toBeInTheDocument() - expect(screen.getByTestId('filter-pill-trashed')).toBeInTheDocument() }) it('shows filter pills in All Notes view', () => { @@ -1386,20 +1242,17 @@ describe('NoteList — filter pills', () => { expect(screen.getByTestId('filter-pills')).toBeInTheDocument() expect(screen.getByTestId('filter-pill-open')).toBeInTheDocument() expect(screen.getByTestId('filter-pill-archived')).toBeInTheDocument() - expect(screen.getByTestId('filter-pill-trashed')).toBeInTheDocument() }) it('shows correct All Notes count badges across all types', () => { render( ) - // projectEntries: 2 open Projects + 1 open Note = 3 open, 1 archived, 1 trashed + // projectEntries: 2 open Projects + 1 open Note = 3 open, 1 archived const openPill = screen.getByTestId('filter-pill-open') const archivedPill = screen.getByTestId('filter-pill-archived') - const trashedPill = screen.getByTestId('filter-pill-trashed') expect(openPill).toHaveTextContent('3') expect(archivedPill).toHaveTextContent('1') - expect(trashedPill).toHaveTextContent('1') }) it('shows archived notes in All Notes when filter is archived', () => { @@ -1411,28 +1264,17 @@ describe('NoteList — filter pills', () => { expect(screen.queryByText('Some Note')).not.toBeInTheDocument() }) - it('shows trashed notes in All Notes when filter is trashed', () => { - render( - - ) - expect(screen.getByText('Trashed Project')).toBeInTheDocument() - expect(screen.queryByText('Open Project 1')).not.toBeInTheDocument() - }) - it('shows correct count badges for each filter', () => { render( ) - // Open pill should show 2, Archived 1, Trashed 1 + // Open pill should show 2, Archived 1 const openPill = screen.getByTestId('filter-pill-open') const archivedPill = screen.getByTestId('filter-pill-archived') - const trashedPill = screen.getByTestId('filter-pill-trashed') expect(openPill).toHaveTextContent('Open') expect(openPill).toHaveTextContent('2') expect(archivedPill).toHaveTextContent('Archived') expect(archivedPill).toHaveTextContent('1') - expect(trashedPill).toHaveTextContent('Trashed') - expect(trashedPill).toHaveTextContent('1') }) it('calls onNoteListFilterChange when a pill is clicked', () => { @@ -1452,14 +1294,6 @@ describe('NoteList — filter pills', () => { expect(screen.queryByText('Open Project 1')).not.toBeInTheDocument() }) - it('shows trashed notes when filter is set to trashed', () => { - render( - - ) - expect(screen.getByText('Trashed Project')).toBeInTheDocument() - expect(screen.queryByText('Open Project 1')).not.toBeInTheDocument() - }) - it('shows empty state for archived filter when no archived notes exist', () => { const entriesNoArchived = projectEntries.filter(e => !e.archived) render( @@ -1473,7 +1307,6 @@ describe('NoteList — filterEntries with subFilter', () => { const entries = [ makeEntry({ path: '/1.md', title: 'Active', isA: 'Project' }), makeEntry({ path: '/2.md', title: 'Archived', isA: 'Project', archived: true }), - makeEntry({ path: '/3.md', title: 'Trashed', isA: 'Project', trashed: true }), makeEntry({ path: '/4.md', title: 'Other', isA: 'Note' }), ] @@ -1487,11 +1320,6 @@ describe('NoteList — filterEntries with subFilter', () => { expect(result.map(e => e.title)).toEqual(['Archived']) }) - it('filters sectionGroup by trashed sub-filter', () => { - const result = filterEntries(entries, { kind: 'sectionGroup', type: 'Project' }, 'trashed') - expect(result.map(e => e.title)).toEqual(['Trashed']) - }) - it('without sub-filter, defaults to active only', () => { const result = filterEntries(entries, { kind: 'sectionGroup', type: 'Project' }) expect(result.map(e => e.title)).toEqual(['Active']) @@ -1506,11 +1334,6 @@ describe('NoteList — filterEntries with subFilter', () => { const result = filterEntries(entries, { kind: 'filter', filter: 'all' }, 'archived') expect(result.map(e => e.title)).toEqual(['Archived']) }) - - it('filters all notes by trashed sub-filter', () => { - const result = filterEntries(entries, { kind: 'filter', filter: 'all' }, 'trashed') - expect(result.map(e => e.title)).toEqual(['Trashed']) - }) }) describe('countAllByFilter', () => { @@ -1519,11 +1342,9 @@ describe('countAllByFilter', () => { makeEntry({ path: '/1.md', isA: 'Project' }), makeEntry({ path: '/2.md', isA: 'Note' }), makeEntry({ path: '/3.md', isA: 'Project', archived: true }), - makeEntry({ path: '/4.md', isA: 'Note', trashed: true }), - makeEntry({ path: '/5.md', isA: 'Person', archived: true, trashed: true }), ] const counts = countAllByFilter(entries) - expect(counts).toEqual({ open: 2, archived: 1, trashed: 2 }) + expect(counts).toEqual({ open: 2, archived: 1 }) }) it('excludes non-markdown files from counts', () => { @@ -1533,7 +1354,7 @@ describe('countAllByFilter', () => { makeEntry({ path: '/3.png', isA: undefined, fileKind: 'binary' }), ] const counts = countAllByFilter(entries) - expect(counts).toEqual({ open: 1, archived: 0, trashed: 0 }) + expect(counts).toEqual({ open: 1, archived: 0 }) }) }) diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index 9c54ad08..126a7275 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -35,20 +35,14 @@ function useViewFlags(selection: SidebarSelection) { function useBulkActions( multiSelect: ReturnType, onBulkArchive: NoteListProps['onBulkArchive'], - onBulkTrash: NoteListProps['onBulkTrash'], - onBulkRestore: NoteListProps['onBulkRestore'], onBulkDeletePermanently: NoteListProps['onBulkDeletePermanently'], - isTrashView: boolean, isArchivedView: boolean, ) { const handleBulkArchive = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkArchive?.(paths) }, [multiSelect, onBulkArchive]) - const handleBulkTrash = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkTrash?.(paths) }, [multiSelect, onBulkTrash]) - const handleBulkRestore = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkRestore?.(paths) }, [multiSelect, onBulkRestore]) const handleBulkDeletePermanently = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkDeletePermanently?.(paths) }, [multiSelect, onBulkDeletePermanently]) - const handleBulkUnarchive = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkRestore?.(paths) }, [multiSelect, onBulkRestore]) - const bulkArchiveOrRestore = isTrashView ? handleBulkRestore : isArchivedView ? handleBulkUnarchive : handleBulkArchive - const bulkTrashOrDelete = isTrashView ? handleBulkDeletePermanently : handleBulkTrash - return { handleBulkArchive, handleBulkTrash, handleBulkRestore, handleBulkDeletePermanently, handleBulkUnarchive, bulkArchiveOrRestore, bulkTrashOrDelete } + const handleBulkUnarchive = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkArchive?.(paths) }, [multiSelect, onBulkArchive]) + const bulkArchiveOrUnarchive = isArchivedView ? handleBulkUnarchive : handleBulkArchive + return { handleBulkArchive, handleBulkDeletePermanently, handleBulkUnarchive, bulkArchiveOrUnarchive } } function ChangesContextMenu({ isChangesView, onDiscardFile, modifiedFiles }: { isChangesView: boolean; onDiscardFile?: (relativePath: string) => Promise; modifiedFiles?: ModifiedFile[] }) { @@ -130,10 +124,7 @@ interface NoteListProps { onReplaceActiveTab: (entry: VaultEntry) => void onCreateNote: (type?: string) => void onBulkArchive?: (paths: string[]) => void - onBulkTrash?: (paths: string[]) => void - onBulkRestore?: (paths: string[]) => void onBulkDeletePermanently?: (paths: string[]) => void - onEmptyTrash?: () => void onUpdateTypeSort?: (path: string, key: string, value: string | number | boolean | string[] | null) => void updateEntry?: (path: string, patch: Partial) => void onOpenInNewWindow?: (entry: VaultEntry) => void @@ -143,14 +134,14 @@ interface NoteListProps { visibleNotesRef?: React.MutableRefObject } -function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNoteListFilterChange, inboxPeriod = 'all', modifiedFiles, modifiedFilesError, getNoteStatus, sidebarCollapsed, onSelectNote, onReplaceActiveTab, onCreateNote, onBulkArchive, onBulkTrash, onBulkRestore, onBulkDeletePermanently, onEmptyTrash, onUpdateTypeSort, updateEntry, onOpenInNewWindow, onDiscardFile, onAutoTriggerDiff, views, visibleNotesRef }: NoteListProps) { +function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNoteListFilterChange, inboxPeriod = 'all', modifiedFiles, modifiedFilesError, getNoteStatus, sidebarCollapsed, onSelectNote, onReplaceActiveTab, onCreateNote, onBulkArchive, onBulkDeletePermanently, onUpdateTypeSort, updateEntry, onOpenInNewWindow, onDiscardFile, onAutoTriggerDiff, views, visibleNotesRef }: NoteListProps) { const { modifiedPathSet, modifiedSuffixes, resolvedGetNoteStatus } = useModifiedFilesState(modifiedFiles, getNoteStatus) const { isSectionGroup, isFolderView, isInboxView, isAllNotesView, isChangesView, showFilterPills } = useViewFlags(selection) const subFilter = showFilterPills ? noteListFilter : undefined const filterCounts = useMemo( - () => isSectionGroup && selection.kind === 'sectionGroup' ? countByFilter(entries, selection.type) : (isAllNotesView || isFolderView) ? countAllByFilter(entries) : { open: 0, archived: 0, trashed: 0 }, + () => isSectionGroup && selection.kind === 'sectionGroup' ? countByFilter(entries, selection.type) : (isAllNotesView || isFolderView) ? countAllByFilter(entries) : { open: 0, archived: 0 }, [entries, isSectionGroup, isAllNotesView, isFolderView, selection], ) @@ -169,7 +160,7 @@ function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNot } return map }, [isChangesView, modifiedFiles]) - const { isEntityView, isTrashView, isArchivedView, searched, searchedGroups, expiredTrashCount } = useNoteListData({ entries, selection, query, listSort, listDirection, modifiedPathSet, modifiedSuffixes, subFilter, inboxPeriod: isInboxView ? inboxPeriod : undefined, views }) + const { isEntityView, isArchivedView, searched, searchedGroups } = useNoteListData({ entries, selection, query, listSort, listDirection, modifiedPathSet, modifiedSuffixes, subFilter, inboxPeriod: isInboxView ? inboxPeriod : undefined, views }) // Keep the visible notes ref in sync for keyboard navigation (Cmd+Option+Arrow) if (visibleNotesRef) { visibleNotesRef.current = isEntityView @@ -194,8 +185,8 @@ function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNot } }, [onReplaceActiveTab, onSelectNote, onOpenInNewWindow, multiSelect, isChangesView, onAutoTriggerDiff]) - const { handleBulkArchive, handleBulkTrash, handleBulkRestore, handleBulkDeletePermanently, handleBulkUnarchive, bulkArchiveOrRestore, bulkTrashOrDelete } = useBulkActions(multiSelect, onBulkArchive, onBulkTrash, onBulkRestore, onBulkDeletePermanently, isTrashView, isArchivedView) - useMultiSelectKeyboard(multiSelect, isEntityView, bulkArchiveOrRestore, bulkTrashOrDelete) + const { handleBulkArchive, handleBulkDeletePermanently, handleBulkUnarchive, bulkArchiveOrUnarchive } = useBulkActions(multiSelect, onBulkArchive, onBulkDeletePermanently, isArchivedView) + useMultiSelectKeyboard(multiSelect, isEntityView, bulkArchiveOrUnarchive, handleBulkDeletePermanently) const { handleNoteContextMenu, contextMenuNode, dialogNode } = ChangesContextMenu({ isChangesView, onDiscardFile, modifiedFiles }) @@ -222,20 +213,20 @@ function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNot return (
- +
{entitySelection ? ( ) : ( - + )}
{isChangesView && deletedCount > 0 && } {showFilterPills && }
{multiSelect.isMultiSelecting && ( - + )} {contextMenuNode} {dialogNode} diff --git a/src/components/QuickOpenPalette.test.tsx b/src/components/QuickOpenPalette.test.tsx index ee486bf2..efd27cee 100644 --- a/src/components/QuickOpenPalette.test.tsx +++ b/src/components/QuickOpenPalette.test.tsx @@ -15,8 +15,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, diff --git a/src/components/RawEditorView.test.tsx b/src/components/RawEditorView.test.tsx index a11080d4..5fd1c649 100644 --- a/src/components/RawEditorView.test.tsx +++ b/src/components/RawEditorView.test.tsx @@ -7,7 +7,7 @@ function entry(title: string, path = `/vault/note/${title}.md`) { return { path, filename: `${title}.md`, title, isA: 'Note', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, - cadence: null, archived: false, trashed: false, trashedAt: null, + cadence: null, archived: false, modifiedAt: null, createdAt: null, fileSize: 0, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, outgoingLinks: [], diff --git a/src/components/RawEditorView.tsx b/src/components/RawEditorView.tsx index 8f451f98..c4bd308a 100644 --- a/src/components/RawEditorView.tsx +++ b/src/components/RawEditorView.tsx @@ -58,7 +58,7 @@ export function RawEditorView({ content, path, entries, onContentChange, onSave, const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) const baseItems = useMemo( - () => deduplicateByPath(entries.filter(e => !e.trashed).map(entry => ({ + () => deduplicateByPath(entries.map(entry => ({ title: entry.title, aliases: [...new Set([entry.filename.replace(/\.md$/, ''), ...entry.aliases])], group: entry.isA || 'Note', diff --git a/src/components/SearchPanel.test.tsx b/src/components/SearchPanel.test.tsx index c1280a51..0cf39b06 100644 --- a/src/components/SearchPanel.test.tsx +++ b/src/components/SearchPanel.test.tsx @@ -27,8 +27,6 @@ const MOCK_ENTRIES: VaultEntry[] = [ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: NOW - 7200, createdAt: NOW - 86400 * 30, fileSize: 500, @@ -54,8 +52,6 @@ const MOCK_ENTRIES: VaultEntry[] = [ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: NOW - 86400 * 5, createdAt: NOW - 86400 * 5, fileSize: 300, diff --git a/src/components/Sidebar.test.ts b/src/components/Sidebar.test.ts index 1f5f3444..1dbce775 100644 --- a/src/components/Sidebar.test.ts +++ b/src/components/Sidebar.test.ts @@ -6,7 +6,7 @@ import { GearSix, CookingPot, FileText } from '@phosphor-icons/react' const baseEntry: VaultEntry = { path: '', filename: '', title: '', isA: null, aliases: [], belongsTo: [], relatedTo: [], - status: null, archived: false, trashed: false, trashedAt: null, + status: null, archived: false, modifiedAt: null, createdAt: null, fileSize: 0, snippet: '', relationships: {}, wordCount: 0, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, @@ -100,15 +100,6 @@ describe('buildDynamicSections', () => { expect(projectSections).toHaveLength(1) }) - it('excludes trashed type definitions', () => { - const entries: VaultEntry[] = [] - const typeEntryMap: Record = { - Deleted: { ...baseEntry, title: 'Deleted', isA: 'Type', trashed: true }, - } - const sections = buildDynamicSections(entries, typeEntryMap) - expect(sections.map((s) => s.type)).not.toContain('Deleted') - }) - it('excludes archived type definitions', () => { const entries: VaultEntry[] = [] const typeEntryMap: Record = { diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 7e03c62f..abaaaca6 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -16,8 +16,6 @@ const mockEntries: VaultEntry[] = [ owner: 'Luca', cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 1024, @@ -44,8 +42,6 @@ const mockEntries: VaultEntry[] = [ owner: 'Luca', cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 512, @@ -72,8 +68,6 @@ const mockEntries: VaultEntry[] = [ owner: 'Luca', cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 256, @@ -100,8 +94,6 @@ const mockEntries: VaultEntry[] = [ owner: 'Luca', cadence: 'Weekly', archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 128, @@ -128,8 +120,6 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 256, @@ -156,8 +146,6 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 180, @@ -184,8 +172,6 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 100, @@ -212,8 +198,6 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, @@ -328,8 +312,6 @@ describe('Sidebar', () => { owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, @@ -356,8 +338,6 @@ describe('Sidebar', () => { owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, @@ -384,8 +364,6 @@ describe('Sidebar', () => { owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 300, @@ -411,8 +389,6 @@ describe('Sidebar', () => { owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 400, @@ -441,7 +417,7 @@ describe('Sidebar', () => { it('shows section for type with zero active entries when type definition exists', () => { // Only Type definitions exist for Book, no actual Book instances - // New behavior: types are shown in sidebar as long as the Type definition exists (not trashed/archived) + // New behavior: types are shown in sidebar as long as the Type definition exists (not archived) const entriesNoBookInstance = entriesWithCustomTypes.filter((e) => !(e.isA === 'Book' && e.title !== 'Book')) render( {}} />) // Books should still appear because the Book type definition exists @@ -450,21 +426,6 @@ describe('Sidebar', () => { expect(screen.getByText('Recipes')).toBeInTheDocument() }) - it('hides type section when all entries of that type are trashed', () => { - const entriesWithTrashedOnly: VaultEntry[] = [ - { - path: '/vault/event/cancelled.md', filename: 'cancelled.md', title: 'Cancelled Event', - 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, sidebarLabel: null, outgoingLinks: [], - properties: {}, - }, - ] - render( {}} />) - expect(screen.queryByText('Events')).not.toBeInTheDocument() - }) - it('shows no sections when entries list is empty', () => { render( {}} />) expect(screen.queryByText('Projects')).not.toBeInTheDocument() @@ -485,8 +446,6 @@ describe('Sidebar', () => { owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, @@ -513,7 +472,7 @@ describe('Sidebar', () => { { path: '/vault/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, + archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: 'News', outgoingLinks: [], properties: {}, @@ -521,7 +480,7 @@ describe('Sidebar', () => { { 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, + archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 300, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, outgoingLinks: [], properties: {}, @@ -539,7 +498,7 @@ describe('Sidebar', () => { { path: '/vault/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, + archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: 'Contacts', outgoingLinks: [], properties: {}, @@ -570,8 +529,6 @@ describe('Sidebar', () => { owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, @@ -693,7 +650,7 @@ describe('Sidebar', () => { { path: '/vault/project.md', filename: 'project.md', title: 'Project', isA: 'Type', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, - archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '', + archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: 5, sidebarLabel: null, outgoingLinks: [], properties: {}, @@ -701,7 +658,7 @@ describe('Sidebar', () => { { path: '/vault/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: '', + archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: 0, sidebarLabel: null, outgoingLinks: [], properties: {}, @@ -709,7 +666,7 @@ describe('Sidebar', () => { { path: '/vault/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: '', + archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: 1, sidebarLabel: null, outgoingLinks: [], properties: {}, @@ -802,7 +759,7 @@ describe('Sidebar', () => { { path: '/vault/note.md', filename: 'note.md', title: 'Note', isA: 'Type', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, - archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, + archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, outgoingLinks: [], properties: {}, @@ -810,7 +767,7 @@ describe('Sidebar', () => { { path: '/vault/explicit-note.md', filename: 'explicit-note.md', title: 'Explicit Note', isA: 'Note', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, - cadence: null, archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, + cadence: null, archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 300, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, outgoingLinks: [], properties: {}, @@ -818,7 +775,7 @@ describe('Sidebar', () => { { path: '/vault/untyped-note.md', filename: 'untyped-note.md', title: 'Untyped Note', isA: null, aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, - cadence: null, archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, + cadence: null, archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 150, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, outgoingLinks: [], properties: {}, @@ -841,7 +798,7 @@ describe('Sidebar', () => { { path: '/vault/plain.md', filename: 'plain.md', title: 'Plain Note', isA: null, aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, - cadence: null, archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, + cadence: null, archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 100, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, outgoingLinks: [], properties: {}, @@ -862,7 +819,7 @@ describe('Sidebar', () => { isA: 'Monday Ideas', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, - archived: false, trashed: false, trashedAt: null, + archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 310, snippet: '', wordCount: 120, relationships: {}, icon: null, color: null, order: null, @@ -876,7 +833,7 @@ describe('Sidebar', () => { isA: 'Monday Ideas', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, - archived: false, trashed: false, trashedAt: null, + archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 280, snippet: '', wordCount: 95, relationships: {}, icon: null, color: null, order: null, @@ -915,7 +872,7 @@ describe('Sidebar', () => { { path: '/vault/project/build-app.md', filename: 'build-app.md', title: 'Build App', isA: 'Project', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, - cadence: null, archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, + cadence: null, archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 300, snippet: '', wordCount: 0, relationships: {}, icon: '🚀', color: null, order: null, sidebarLabel: null, template: null, sort: null, view: null, visible: null, outgoingLinks: [], properties: {}, @@ -929,7 +886,7 @@ describe('Sidebar', () => { const favEntry: VaultEntry = { path: '/vault/project/fav.md', filename: 'fav.md', title: 'My Favorite Note', isA: 'Project', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, - cadence: null, archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, + cadence: null, archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 100, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, view: null, visible: null, outgoingLinks: [], properties: {}, @@ -947,12 +904,6 @@ describe('Sidebar', () => { expect(screen.queryByText('FAVORITES')).not.toBeInTheDocument() }) - it('hides trashed favorites from the section', () => { - const trashedFav = { ...favEntry, trashed: true } - render( {}} />) - expect(screen.queryByText('FAVORITES')).not.toBeInTheDocument() - }) - it('calls onSelect with favorites filter when clicking a favorite', () => { const onSelect = vi.fn() render() diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index fa5466ae..e0d499ac 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -100,13 +100,12 @@ function useSidebarCollapsed() { function useEntryCounts(entries: VaultEntry[]) { return useMemo(() => { - let active = 0, archived = 0, trashed = 0 + let active = 0, archived = 0 for (const e of entries) { - if (e.trashed) trashed++ - else if (e.archived) archived++ + if (e.archived) archived++ else active++ } - return { activeCount: active, archivedCount: archived, trashedCount: trashed } + return { activeCount: active, archivedCount: archived } }, [entries]) } @@ -314,7 +313,7 @@ function SortableSection({ group, sectionProps }: { }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: group.type }) const itemCount = sectionProps.entries.filter((e) => - !e.archived && !e.trashed && (group.type === 'Note' ? (e.isA === 'Note' || !e.isA) : e.isA === group.type), + !e.archived && (group.type === 'Note' ? (e.isA === 'Note' || !e.isA) : e.isA === group.type), ).length const isRenaming = sectionProps.renamingType === group.type @@ -364,7 +363,7 @@ function FavoritesSection({ entries, selection, onSelect, onSelectNote, onReorde }) { const favorites = useMemo( () => entries - .filter((e) => e.favorite && !e.archived && !e.trashed) + .filter((e) => e.favorite && !e.archived) .sort((a, b) => (a.favoriteIndex ?? Infinity) - (b.favoriteIndex ?? Infinity)), [entries], ) @@ -502,7 +501,7 @@ export const Sidebar = memo(function Sidebar({ const isSectionVisible = useCallback((type: string) => typeEntryMap[type]?.visible !== false, [typeEntryMap]) const toggleVisibility = useCallback((type: string) => onToggleTypeVisibility?.(type), [onToggleTypeVisibility]) - const { activeCount, archivedCount, trashedCount } = useEntryCounts(entries) + const { activeCount, archivedCount } = useEntryCounts(entries) const closeContextMenu = useCallback(() => { setContextMenuPos(null); setContextMenuType(null) }, []) const closeCustomize = useCallback(() => setShowCustomize(false), []) @@ -559,7 +558,7 @@ export const Sidebar = memo(function Sidebar({ const { collapsed: groupCollapsed, toggle: toggleGroup } = useSidebarCollapsed() - const hasFavorites = entries.some((e) => e.favorite && !e.archived && !e.trashed) + const hasFavorites = entries.some((e) => e.favorite && !e.archived) const hasViews = views.length > 0 || !!onCreateView return ( @@ -571,7 +570,6 @@ export const Sidebar = memo(function Sidebar({ onSelect({ kind: 'filter', filter: 'inbox' })} /> onSelect({ kind: 'filter', filter: 'all' })} /> onSelect({ kind: 'filter', filter: 'archived' })} /> - onSelect({ kind: 'filter', filter: 'trash' })} />
{/* Favorites */} diff --git a/src/components/SingleEditorView.tsx b/src/components/SingleEditorView.tsx index 07f67df8..8d662652 100644 --- a/src/components/SingleEditorView.tsx +++ b/src/components/SingleEditorView.tsx @@ -73,7 +73,7 @@ export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) const baseItems = useMemo( - () => deduplicateByPath(entries.filter(e => !e.trashed).map(entry => ({ + () => deduplicateByPath(entries.map(entry => ({ title: entry.title, aliases: [...new Set([entry.filename.replace(/\.md$/, ''), ...entry.aliases])], group: entry.isA || 'Note', diff --git a/src/components/TrashedNoteBanner.test.tsx b/src/components/TrashedNoteBanner.test.tsx deleted file mode 100644 index c30548ca..00000000 --- a/src/components/TrashedNoteBanner.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react' -import { describe, it, expect, vi } from 'vitest' -import { TrashedNoteBanner } from './TrashedNoteBanner' - -describe('TrashedNoteBanner', () => { - it('renders the banner with trash message', () => { - render() - expect(screen.getByText('This note is in the Trash')).toBeInTheDocument() - expect(screen.getByTestId('trashed-note-banner')).toBeInTheDocument() - }) - - it('shows Restore and Delete permanently buttons', () => { - render() - expect(screen.getByText('Restore')).toBeInTheDocument() - expect(screen.getByText('Delete permanently')).toBeInTheDocument() - }) - - it('calls onRestore when Restore button is clicked', () => { - const onRestore = vi.fn() - render() - fireEvent.click(screen.getByTestId('trashed-banner-restore')) - expect(onRestore).toHaveBeenCalledOnce() - }) - - it('calls onDeletePermanently when Delete permanently button is clicked', () => { - const onDeletePermanently = vi.fn() - render() - fireEvent.click(screen.getByTestId('trashed-banner-delete')) - expect(onDeletePermanently).toHaveBeenCalledOnce() - }) -}) diff --git a/src/components/TrashedNoteBanner.tsx b/src/components/TrashedNoteBanner.tsx deleted file mode 100644 index 09ed9f1e..00000000 --- a/src/components/TrashedNoteBanner.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { memo } from 'react' -import { Trash, ArrowCounterClockwise } from '@phosphor-icons/react' - -interface TrashedNoteBannerProps { - onRestore: () => void - onDeletePermanently: () => void -} - -export const TrashedNoteBanner = memo(function TrashedNoteBanner({ - onRestore, - onDeletePermanently, -}: TrashedNoteBannerProps) { - return ( -
- - This note is in the Trash - - -
- ) -}) diff --git a/src/components/WikilinkChatInput.test.tsx b/src/components/WikilinkChatInput.test.tsx index f6ede97a..69543907 100644 --- a/src/components/WikilinkChatInput.test.tsx +++ b/src/components/WikilinkChatInput.test.tsx @@ -16,8 +16,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ owner: null, cadence: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, @@ -35,7 +33,6 @@ const entries: VaultEntry[] = [ makeEntry({ path: '/vault/alpha.md', title: 'Alpha', filename: 'alpha.md', isA: 'Project' }), makeEntry({ path: '/vault/beta.md', title: 'Beta', filename: 'beta.md', isA: 'Person' }), makeEntry({ path: '/vault/gamma.md', title: 'Gamma', filename: 'gamma.md' }), - makeEntry({ path: '/vault/trashed.md', title: 'Trashed', filename: 'trashed.md', trashed: true }), makeEntry({ path: '/vault/archived.md', title: 'Archived', filename: 'archived.md', archived: true }), ] @@ -101,13 +98,12 @@ describe('WikilinkChatInput', () => { vi.useRealTimers() }) - it('excludes trashed and archived entries from suggestions', async () => { + it('excludes archived entries from suggestions', async () => { vi.useFakeTimers() render() - await typeAndWait('[[t') + await typeAndWait('[[arch') const menu = screen.queryByTestId('wikilink-menu') if (menu) { - expect(menu.textContent).not.toContain('Trashed') expect(menu.textContent).not.toContain('Archived') } vi.useRealTimers() diff --git a/src/components/WikilinkChatInput.tsx b/src/components/WikilinkChatInput.tsx index c87167cc..ad4ff906 100644 --- a/src/components/WikilinkChatInput.tsx +++ b/src/components/WikilinkChatInput.tsx @@ -49,7 +49,7 @@ function matchEntries( const lower = query.toLowerCase() const matches = entries .filter(e => - !e.trashed && !e.archived && ( + !e.archived && ( e.title.toLowerCase().includes(lower) || e.aliases.some(a => a.toLowerCase().includes(lower)) ), diff --git a/src/components/inspector/BacklinksPanel.tsx b/src/components/inspector/BacklinksPanel.tsx index 555c03d4..45074072 100644 --- a/src/components/inspector/BacklinksPanel.tsx +++ b/src/components/inspector/BacklinksPanel.tsx @@ -1,5 +1,5 @@ import type { VaultEntry } from '../../types' -import { ArrowUpRight, Trash } from '@phosphor-icons/react' +import { ArrowUpRight } from '@phosphor-icons/react' import { isEmoji } from '../../utils/emoji' import { entryStatusTitle } from './shared' import { StatusSuffix } from './LinkButton' @@ -14,7 +14,7 @@ function BacklinkEntry({ entry, context, onNavigate }: { context: string | null onNavigate: (target: string) => void }) { - const isDimmed = entry.archived || entry.trashed + const isDimmed = entry.archived return ( - )} - {!isTrashView && ( - - )} + {searchVisible && ( diff --git a/src/components/note-list/NoteListViews.tsx b/src/components/note-list/NoteListViews.tsx index fe851c74..560b3a52 100644 --- a/src/components/note-list/NoteListViews.tsx +++ b/src/components/note-list/NoteListViews.tsx @@ -3,18 +3,11 @@ import type { VaultEntry } from '../../types' import type { SortOption, SortDirection, SortConfig, RelationshipGroup } from '../../utils/noteListHelpers' import { PinnedCard } from './PinnedCard' import { RelationshipGroupSection } from './RelationshipGroupSection' -import { TrashWarningBanner, EmptyMessage } from './TrashWarningBanner' +import { EmptyMessage } from './TrashWarningBanner' -function ListViewHeader({ isTrashView, expiredTrashCount }: { - isTrashView: boolean; expiredTrashCount: number -}) { - return -} - -function resolveEmptyText(isChangesView: boolean, changesError: string | null | undefined, isTrashView: boolean, isArchivedView: boolean, isInboxView: boolean, query: string): string { +function resolveEmptyText(isChangesView: boolean, changesError: string | null | undefined, isArchivedView: boolean, isInboxView: boolean, query: string): string { if (isChangesView && changesError) return `Failed to load changes: ${changesError}` if (isChangesView) return 'No pending changes' - if (isTrashView) return 'Trash is empty' if (isArchivedView) return 'No archived notes' if (isInboxView) return query ? 'No matching notes' : 'All notes are organized' return query ? 'No matching notes' : 'No notes found' @@ -40,20 +33,18 @@ export function EntityView({ entity, groups, query, collapsedGroups, sortPrefs, ) } -export function ListView({ isTrashView, isArchivedView, isChangesView, isInboxView, changesError, expiredTrashCount, deletedCount = 0, searched, query, renderItem, virtuosoRef }: { - isTrashView: boolean; isArchivedView?: boolean; isChangesView?: boolean; isInboxView?: boolean; changesError?: string | null; expiredTrashCount: number +export function ListView({ isArchivedView, isChangesView, isInboxView, changesError, deletedCount = 0, searched, query, renderItem, virtuosoRef }: { + isArchivedView?: boolean; isChangesView?: boolean; isInboxView?: boolean; changesError?: string | null deletedCount?: number; searched: VaultEntry[]; query: string renderItem: (entry: VaultEntry) => React.ReactNode virtuosoRef?: React.RefObject }) { - const emptyText = resolveEmptyText(!!isChangesView, changesError ?? null, isTrashView, !!isArchivedView, !!isInboxView, query) - const hasHeader = isTrashView && expiredTrashCount > 0 + const emptyText = resolveEmptyText(!!isChangesView, changesError ?? null, !!isArchivedView, !!isInboxView, query) const hasDeletedOnly = !!isChangesView && deletedCount > 0 && searched.length === 0 if (searched.length === 0 && !hasDeletedOnly) { return (
- {hasHeader && }
) @@ -69,9 +60,6 @@ export function ListView({ isTrashView, isArchivedView, isChangesView, isInboxVi style={{ height: '100%' }} data={searched} overscan={200} - components={{ - Header: hasHeader ? () => : undefined, - }} itemContent={(_index, entry) => renderItem(entry)} /> ) diff --git a/src/components/note-list/TrashWarningBanner.tsx b/src/components/note-list/TrashWarningBanner.tsx index d884bf1f..4e8914a5 100644 --- a/src/components/note-list/TrashWarningBanner.tsx +++ b/src/components/note-list/TrashWarningBanner.tsx @@ -1,17 +1,4 @@ -import { Warning, TrashSimple } from '@phosphor-icons/react' - -export function TrashWarningBanner({ expiredCount }: { expiredCount: number }) { - if (expiredCount === 0) return null - return ( -
- -
-
Notes in trash for 30+ days will be permanently deleted
-
{expiredCount} {expiredCount === 1 ? 'note is' : 'notes are'} past the 30-day retention period
-
-
- ) -} +import { TrashSimple } from '@phosphor-icons/react' export function EmptyMessage({ text }: { text: string }) { return
{text}
diff --git a/src/components/note-list/noteListHooks.ts b/src/components/note-list/noteListHooks.ts index efb3f470..4928274e 100644 --- a/src/components/note-list/noteListHooks.ts +++ b/src/components/note-list/noteListHooks.ts @@ -9,7 +9,7 @@ import { } from '../../utils/noteListHelpers' import type { InboxPeriod } from '../../types' import { buildTypeEntryMap } from '../../utils/typeColors' -import { filterByQuery, filterGroupsByQuery, countExpiredTrash, createNoteStatusResolver, isModifiedEntry } from './noteListUtils' +import { filterByQuery, filterGroupsByQuery, createNoteStatusResolver, isModifiedEntry } from './noteListUtils' import type { MultiSelectState } from '../../hooks/useMultiSelect' // --- useTypeEntryMap --- @@ -45,7 +45,6 @@ interface NoteListDataParams { export function useNoteListData({ entries, selection, query, listSort, listDirection, modifiedPathSet, modifiedSuffixes, subFilter, inboxPeriod, views }: NoteListDataParams) { const isEntityView = selection.kind === 'entity' - const isTrashView = (selection.kind === 'filter' && selection.filter === 'trash') || subFilter === 'trashed' const isArchivedView = (selection.kind === 'filter' && selection.filter === 'archived') || subFilter === 'archived' const filteredEntries = useFilteredEntries(entries, selection, modifiedPathSet, modifiedSuffixes, subFilter, inboxPeriod, views) @@ -64,12 +63,7 @@ export function useNoteListData({ entries, selection, query, listSort, listDirec return filterGroupsByQuery(groups, query) }, [isEntityView, selection, entries, query]) - const expiredTrashCount = useMemo( - () => isTrashView ? countExpiredTrash(searched) : 0, - [isTrashView, searched], - ) - - return { isEntityView, isTrashView, isArchivedView, searched, searchedGroups, expiredTrashCount } + return { isEntityView, isArchivedView, searched, searchedGroups } } // --- useNoteListSearch --- @@ -193,22 +187,22 @@ function handleSelectAllKey(e: KeyboardEvent, multiSelect: MultiSelectState, isE multiSelect.selectAll() } -function handleBulkActionKey(e: KeyboardEvent, multiSelect: MultiSelectState, onArchive: () => void, onTrash: () => void) { +function handleBulkActionKey(e: KeyboardEvent, multiSelect: MultiSelectState, onArchive: () => void, onDelete: () => void) { if (!multiSelect.isMultiSelecting || !(e.metaKey || e.ctrlKey)) return if (e.key === 'e') { e.preventDefault(); e.stopPropagation(); onArchive() } - if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); e.stopPropagation(); onTrash() } + if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); e.stopPropagation(); onDelete() } } -export function useMultiSelectKeyboard(multiSelect: MultiSelectState, isEntityView: boolean, onBulkArchive: () => void, onBulkTrash: () => void) { +export function useMultiSelectKeyboard(multiSelect: MultiSelectState, isEntityView: boolean, onBulkArchive: () => void, onBulkDelete: () => void) { useEffect(() => { function handleKeyDown(e: KeyboardEvent) { handleEscapeKey(e, multiSelect) handleSelectAllKey(e, multiSelect, isEntityView) - handleBulkActionKey(e, multiSelect, onBulkArchive, onBulkTrash) + handleBulkActionKey(e, multiSelect, onBulkArchive, onBulkDelete) } window.addEventListener('keydown', handleKeyDown, true) return () => window.removeEventListener('keydown', handleKeyDown, true) - }, [multiSelect, isEntityView, onBulkArchive, onBulkTrash]) + }, [multiSelect, isEntityView, onBulkArchive, onBulkDelete]) } // --- useModifiedFilesState --- diff --git a/src/components/note-list/noteListUtils.test.ts b/src/components/note-list/noteListUtils.test.ts index b67c6bd6..b31ff121 100644 --- a/src/components/note-list/noteListUtils.test.ts +++ b/src/components/note-list/noteListUtils.test.ts @@ -6,7 +6,7 @@ function makeEntry(path = '/test.md'): VaultEntry { return { path, filename: 'test.md', title: 'Test', isA: null, aliases: [], belongsTo: [], relatedTo: [], status: null, - archived: false, trashed: false, trashedAt: null, + archived: false, modifiedAt: null, createdAt: null, fileSize: 0, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, diff --git a/src/components/note-list/noteListUtils.ts b/src/components/note-list/noteListUtils.ts index 2d030391..e318b29e 100644 --- a/src/components/note-list/noteListUtils.ts +++ b/src/components/note-list/noteListUtils.ts @@ -24,11 +24,6 @@ export function filterGroupsByQuery(groups: RelationshipGroup[], query: string): return groups.map((g) => ({ ...g, entries: filterByQuery(g.entries, query) })).filter((g) => g.entries.length > 0) } -export function countExpiredTrash(entries: VaultEntry[]): number { - const now = Date.now() / 1000 - return entries.filter((e) => e.trashedAt && (now - e.trashedAt) >= 86400 * 30).length -} - export interface ClickActions { onReplace: (entry: VaultEntry) => void onSelect: (entry: VaultEntry) => void diff --git a/src/components/useNoteListSort.test.tsx b/src/components/useNoteListSort.test.tsx index 67925683..04d59883 100644 --- a/src/components/useNoteListSort.test.tsx +++ b/src/components/useNoteListSort.test.tsx @@ -19,7 +19,7 @@ function makeEntry(overrides: Partial = {}): VaultEntry { path: '/test/note.md', filename: 'note.md', title: 'Test Note', isA: 'Note', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null, archived: false, - trashed: false, trashedAt: null, modifiedAt: 1700000000, + modifiedAt: 1700000000, createdAt: null, fileSize: 100, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, diff --git a/src/hooks/commands/filterCommands.ts b/src/hooks/commands/filterCommands.ts index 050f39a0..1dd053f3 100644 --- a/src/hooks/commands/filterCommands.ts +++ b/src/hooks/commands/filterCommands.ts @@ -12,6 +12,5 @@ export function buildFilterCommands(config: FilterCommandsConfig): CommandAction return [ { id: 'filter-open', label: 'Show Open Notes', group: 'Navigation', keywords: ['filter', 'open', 'active', 'pill'], enabled: !!isSectionGroup && noteListFilter !== 'open', execute: () => onSetNoteListFilter?.('open') }, { id: 'filter-archived', label: 'Show Archived Notes', group: 'Navigation', keywords: ['filter', 'archived', 'pill'], enabled: !!isSectionGroup && noteListFilter !== 'archived', execute: () => onSetNoteListFilter?.('archived') }, - { id: 'filter-trashed', label: 'Show Trashed Notes', group: 'Navigation', keywords: ['filter', 'trashed', 'trash', 'pill', 'deleted'], enabled: !!isSectionGroup && noteListFilter !== 'trashed', execute: () => onSetNoteListFilter?.('trashed') }, ] } diff --git a/src/hooks/commands/noteCommands.ts b/src/hooks/commands/noteCommands.ts index a4f15998..429a8d63 100644 --- a/src/hooks/commands/noteCommands.ts +++ b/src/hooks/commands/noteCommands.ts @@ -4,18 +4,14 @@ interface NoteCommandsConfig { hasActiveNote: boolean activeTabPath: string | null isArchived: boolean - isTrashed: boolean activeNoteHasIcon?: boolean - trashedCount?: number onCreateNote: () => void onCreateType?: () => void onOpenDailyNote: () => void onSave: () => void - onTrashNote: (path: string) => void - onRestoreNote: (path: string) => void + onDeleteNote: (path: string) => void onArchiveNote: (path: string) => void onUnarchiveNote: (path: string) => void - onEmptyTrash?: () => void onSetNoteIcon?: () => void onRemoveNoteIcon?: () => void onOpenInNewWindow?: () => void @@ -27,10 +23,9 @@ interface NoteCommandsConfig { export function buildNoteCommands(config: NoteCommandsConfig): CommandAction[] { const { - hasActiveNote, activeTabPath, isArchived, isTrashed, + hasActiveNote, activeTabPath, isArchived, onCreateNote, onCreateType, onOpenDailyNote, onSave, - onTrashNote, onRestoreNote, onArchiveNote, onUnarchiveNote, - onEmptyTrash, trashedCount, + onDeleteNote, onArchiveNote, onUnarchiveNote, onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon, onOpenInNewWindow, onToggleFavorite, isFavorite, onToggleOrganized, isOrganized, @@ -41,11 +36,10 @@ export function buildNoteCommands(config: NoteCommandsConfig): CommandAction[] { { id: 'create-type', label: 'New Type', group: 'Note', keywords: ['new', 'create', 'type', 'template'], enabled: !!onCreateType, execute: () => onCreateType?.() }, { id: 'open-daily-note', label: "Open Today's Note", group: 'Note', shortcut: '⌘J', keywords: ['daily', 'journal', 'today'], enabled: true, execute: onOpenDailyNote }, { id: 'save-note', label: 'Save Note', group: 'Note', shortcut: '⌘S', keywords: ['write'], enabled: hasActiveNote, execute: onSave }, - { id: 'empty-trash', label: 'Empty Trash', group: 'Note', keywords: ['delete', 'permanently', 'purge', 'clear', 'trash'], enabled: (trashedCount ?? 0) > 0, execute: () => onEmptyTrash?.() }, { - id: 'trash-note', label: isTrashed ? 'Restore Note' : 'Trash Note', group: 'Note', shortcut: '⌘⌫', - keywords: ['delete', 'remove', 'restore', 'trash'], enabled: hasActiveNote, - execute: () => { if (activeTabPath) (isTrashed ? onRestoreNote : onTrashNote)(activeTabPath) }, + id: 'delete-note', label: 'Delete Note', group: 'Note', shortcut: '⌘⌫', + keywords: ['delete', 'remove'], enabled: hasActiveNote, + execute: () => { if (activeTabPath) onDeleteNote(activeTabPath) }, }, { id: 'archive-note', label: isArchived ? 'Unarchive Note' : 'Archive Note', group: 'Note', shortcut: '⌘E', diff --git a/src/hooks/commands/typeCommands.ts b/src/hooks/commands/typeCommands.ts index 5048e66d..c14ecd30 100644 --- a/src/hooks/commands/typeCommands.ts +++ b/src/hooks/commands/typeCommands.ts @@ -18,7 +18,6 @@ export function pluralizeType(type: string): string { export function extractVaultTypes(entries: VaultEntry[]): string[] { const typeSet = new Set() for (const e of entries) { - if (e.trashed) continue if (e.isA === 'Type' && e.title) { typeSet.add(e.title) } else if (e.isA && e.isA !== 'Type') { diff --git a/src/hooks/frontmatterOps.ts b/src/hooks/frontmatterOps.ts index 32a7179b..3f525a4f 100644 --- a/src/hooks/frontmatterOps.ts +++ b/src/hooks/frontmatterOps.ts @@ -12,7 +12,6 @@ const ENTRY_DELETE_MAP: Record> = { icon: { icon: null }, sidebar_label: { sidebarLabel: null }, aliases: { aliases: [] }, belongs_to: { belongsTo: [] }, related_to: { relatedTo: [] }, _archived: { archived: false }, archived: { archived: false }, - _trashed: { trashed: false }, trashed: { trashed: false }, order: { order: null }, template: { template: null }, sort: { sort: null }, visible: { visible: null }, _organized: { organized: false }, @@ -60,7 +59,6 @@ export function frontmatterToEntryPatch( icon: { icon: str }, sidebar_label: { sidebarLabel: str }, aliases: { aliases: arr }, belongs_to: { belongsTo: arr }, related_to: { relatedTo: arr }, _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/useAppCommands.ts b/src/hooks/useAppCommands.ts index 1b19c417..6200048d 100644 --- a/src/hooks/useAppCommands.ts +++ b/src/hooks/useAppCommands.ts @@ -23,8 +23,7 @@ interface AppCommandsConfig { onCreateNoteOfType: (type: string) => void onSave: () => void onOpenSettings: () => void - onTrashNote: (path: string) => void - onRestoreNote: (path: string) => void + onDeleteNote: (path: string) => void onArchiveNote: (path: string) => void onUnarchiveNote: (path: string) => void onCommitPush: () => void @@ -58,8 +57,6 @@ interface AppCommandsConfig { onInstallMcp?: () => void claudeCodeStatus?: string claudeCodeVersion?: string - onEmptyTrash?: () => void - trashedCount?: number onReloadVault?: () => void onRepairVault?: () => void onSetNoteIcon?: () => void @@ -83,10 +80,6 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { ;(entry?.archived ? config.onUnarchiveNote : config.onArchiveNote)(path) }, [config.onArchiveNote, config.onUnarchiveNote]) - const toggleTrash = useCallback((path: string) => { - const entry = entriesRef.current.find(e => e.path === path) - ;(entry?.trashed ? config.onRestoreNote : config.onTrashNote)(path) - }, [config.onTrashNote, config.onRestoreNote]) const { onSelect } = config @@ -106,7 +99,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { onOpenDailyNote: config.onOpenDailyNote, onSave: config.onSave, onOpenSettings: config.onOpenSettings, - onTrashNote: toggleTrash, + onDeleteNote: config.onDeleteNote, onArchiveNote: toggleArchive, onSetViewMode: config.onSetViewMode, onZoomIn: config.onZoomIn, @@ -136,7 +129,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { onZoomOut: config.onZoomOut, onZoomReset: config.onZoomReset, onArchiveNote: toggleArchive, - onTrashNote: toggleTrash, + onDeleteNote: config.onDeleteNote, onSearch: config.onSearch, onToggleRawEditor: config.onToggleRawEditor, onToggleDiff: config.onToggleDiff, @@ -155,7 +148,6 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { onInstallMcp: config.onInstallMcp, onReloadVault: config.onReloadVault, onRepairVault: config.onRepairVault, - onEmptyTrash: config.onEmptyTrash, onOpenInNewWindow: config.onOpenInNewWindow, activeTabPathRef: config.activeTabPathRef, activeTabPath: config.activeTabPath, @@ -171,8 +163,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { onCreateNoteOfType: config.onCreateNoteOfType, onSave: config.onSave, onOpenSettings: config.onOpenSettings, - onTrashNote: config.onTrashNote, - onRestoreNote: config.onRestoreNote, + onDeleteNote: config.onDeleteNote, onArchiveNote: config.onArchiveNote, onUnarchiveNote: config.onUnarchiveNote, onCommitPush: config.onCommitPush, @@ -203,8 +194,6 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { vaultCount: config.vaultCount, mcpStatus: config.mcpStatus, onInstallMcp: config.onInstallMcp, - onEmptyTrash: config.onEmptyTrash, - trashedCount: config.trashedCount, onReloadVault: config.onReloadVault, onRepairVault: config.onRepairVault, onSetNoteIcon: config.onSetNoteIcon, diff --git a/src/hooks/useAppKeyboard.test.ts b/src/hooks/useAppKeyboard.test.ts index 6e212dca..a8225a55 100644 --- a/src/hooks/useAppKeyboard.test.ts +++ b/src/hooks/useAppKeyboard.test.ts @@ -24,7 +24,7 @@ function makeActions() { onOpenDailyNote: vi.fn(), onSave: vi.fn(), onOpenSettings: vi.fn(), - onTrashNote: vi.fn(), + onDeleteNote: vi.fn(), onArchiveNote: vi.fn(), onSetViewMode: vi.fn(), onZoomIn: vi.fn(), @@ -130,20 +130,20 @@ describe('useAppKeyboard', () => { try { fn() } finally { document.body.removeChild(input) } } - it('Cmd+Backspace does not trash note when text input is focused', () => { + it('Cmd+Backspace does not delete note when text input is focused', () => { const actions = makeActions() renderHook(() => useAppKeyboard(actions)) withFocusedInput(() => { fireKey('Backspace', { metaKey: true }) - expect(actions.onTrashNote).not.toHaveBeenCalled() + expect(actions.onDeleteNote).not.toHaveBeenCalled() }) }) - it('Cmd+Backspace trashes note when no text input is focused', () => { + it('Cmd+Backspace deletes note when no text input is focused', () => { const actions = makeActions() renderHook(() => useAppKeyboard(actions)) fireKey('Backspace', { metaKey: true }) - expect(actions.onTrashNote).toHaveBeenCalledWith('/vault/test.md') + expect(actions.onDeleteNote).toHaveBeenCalledWith('/vault/test.md') }) it('Cmd+K still works when text input is focused', () => { diff --git a/src/hooks/useAppKeyboard.ts b/src/hooks/useAppKeyboard.ts index 1ff0eb41..362a8a7b 100644 --- a/src/hooks/useAppKeyboard.ts +++ b/src/hooks/useAppKeyboard.ts @@ -10,7 +10,7 @@ interface KeyboardActions { onOpenDailyNote: () => void onSave: () => void onOpenSettings: () => void - onTrashNote: (path: string) => void + onDeleteNote: (path: string) => void onArchiveNote: (path: string) => void onSetViewMode: (mode: ViewMode) => void onZoomIn: () => void @@ -66,7 +66,7 @@ function handleCmdKey(e: KeyboardEvent, keyMap: Record) } export function useAppKeyboard({ - onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, + onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onDeleteNote, onArchiveNote, onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, onToggleInspector, onToggleFavorite, onOpenInNewWindow, activeTabPathRef, }: KeyboardActions) { useEffect(() => { @@ -84,8 +84,8 @@ export function useAppKeyboard({ ',': onOpenSettings, d: withActiveTab((path) => onToggleFavorite?.(path)), e: withActiveTab(onArchiveNote), - Backspace: withActiveTab(onTrashNote), - Delete: withActiveTab(onTrashNote), + Backspace: withActiveTab(onDeleteNote), + Delete: withActiveTab(onDeleteNote), '[': () => onGoBack?.(), ']': () => onGoForward?.(), '=': onZoomIn, @@ -127,5 +127,5 @@ export function useAppKeyboard({ } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, [onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, activeTabPathRef, onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, onToggleInspector, onOpenInNewWindow]) + }, [onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onDeleteNote, onArchiveNote, activeTabPathRef, onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, onToggleInspector, onOpenInNewWindow]) } diff --git a/src/hooks/useBulkActions.test.ts b/src/hooks/useBulkActions.test.ts index 61e0550a..374e88dc 100644 --- a/src/hooks/useBulkActions.test.ts +++ b/src/hooks/useBulkActions.test.ts @@ -4,21 +4,17 @@ import { useBulkActions } from './useBulkActions' describe('useBulkActions', () => { let handleArchiveNote: ReturnType - let handleTrashNote: ReturnType - let handleRestoreNote: ReturnType let setToastMessage: ReturnType beforeEach(() => { handleArchiveNote = vi.fn().mockResolvedValue(undefined) - handleTrashNote = vi.fn().mockResolvedValue(undefined) - handleRestoreNote = vi.fn().mockResolvedValue(undefined) setToastMessage = vi.fn() }) function renderBulkActions() { return renderHook(() => useBulkActions( - { handleArchiveNote, handleTrashNote, handleRestoreNote }, + { handleArchiveNote }, setToastMessage, ), ) @@ -77,104 +73,4 @@ describe('useBulkActions', () => { expect(setToastMessage).not.toHaveBeenCalled() }) }) - - // --- handleBulkTrash --- - - describe('handleBulkTrash', () => { - it('trashes each path and shows plural toast', async () => { - const { result } = renderBulkActions() - await act(async () => { - await result.current.handleBulkTrash(['/vault/a.md', '/vault/b.md']) - }) - expect(handleTrashNote).toHaveBeenCalledTimes(2) - expect(setToastMessage).toHaveBeenCalledWith('2 notes moved to trash') - }) - - it('shows singular toast when one note trashed', async () => { - const { result } = renderBulkActions() - await act(async () => { - await result.current.handleBulkTrash(['/vault/a.md']) - }) - expect(setToastMessage).toHaveBeenCalledWith('1 note moved to trash') - }) - - it('does not show toast when empty array given', async () => { - const { result } = renderBulkActions() - await act(async () => { - await result.current.handleBulkTrash([]) - }) - expect(setToastMessage).not.toHaveBeenCalled() - }) - - it('skips failed paths and counts only successes', async () => { - handleTrashNote - .mockRejectedValueOnce(new Error('fail')) - .mockResolvedValueOnce(undefined) - const { result } = renderBulkActions() - await act(async () => { - await result.current.handleBulkTrash(['/vault/a.md', '/vault/b.md']) - }) - expect(setToastMessage).toHaveBeenCalledWith('1 note moved to trash') - }) - - it('shows no toast when all fail', async () => { - handleTrashNote.mockRejectedValue(new Error('fail')) - const { result } = renderBulkActions() - await act(async () => { - await result.current.handleBulkTrash(['/vault/a.md']) - }) - expect(setToastMessage).not.toHaveBeenCalled() - }) - }) - - // --- handleBulkRestore --- - - describe('handleBulkRestore', () => { - it('restores each path and shows plural toast', async () => { - const { result } = renderBulkActions() - await act(async () => { - await result.current.handleBulkRestore(['/vault/a.md', '/vault/b.md', '/vault/c.md']) - }) - expect(handleRestoreNote).toHaveBeenCalledTimes(3) - expect(setToastMessage).toHaveBeenCalledWith('3 notes restored') - }) - - it('shows singular toast when one note restored', async () => { - const { result } = renderBulkActions() - await act(async () => { - await result.current.handleBulkRestore(['/vault/only.md']) - }) - expect(setToastMessage).toHaveBeenCalledWith('1 note restored') - }) - - it('does not show toast when empty array given', async () => { - const { result } = renderBulkActions() - await act(async () => { - await result.current.handleBulkRestore([]) - }) - expect(setToastMessage).not.toHaveBeenCalled() - }) - - it('partial failure: counts only successful restores in toast', async () => { - handleRestoreNote - .mockResolvedValueOnce(undefined) // /vault/a.md ok - .mockRejectedValueOnce(new Error('not found')) // /vault/b.md fails - .mockResolvedValueOnce(undefined) // /vault/c.md ok - const { result } = renderBulkActions() - await act(async () => { - await result.current.handleBulkRestore(['/vault/a.md', '/vault/b.md', '/vault/c.md']) - }) - expect(handleRestoreNote).toHaveBeenCalledTimes(3) - expect(setToastMessage).toHaveBeenCalledWith('2 notes restored') - }) - - it('shows no toast when all restores fail', async () => { - handleRestoreNote.mockRejectedValue(new Error('fail')) - const { result } = renderBulkActions() - await act(async () => { - await result.current.handleBulkRestore(['/vault/a.md', '/vault/b.md']) - }) - expect(setToastMessage).not.toHaveBeenCalled() - }) - }) }) diff --git a/src/hooks/useBulkActions.ts b/src/hooks/useBulkActions.ts index ff28bfcc..8ed10585 100644 --- a/src/hooks/useBulkActions.ts +++ b/src/hooks/useBulkActions.ts @@ -2,8 +2,6 @@ import { useCallback } from 'react' interface BulkEntryActions { handleArchiveNote: (path: string) => Promise - handleTrashNote: (path: string) => Promise - handleRestoreNote: (path: string) => Promise } export function useBulkActions( @@ -19,23 +17,5 @@ export function useBulkActions( if (ok > 0) setToastMessage(`${ok} note${ok > 1 ? 's' : ''} archived`) }, [entryActions, setToastMessage]) - const handleBulkTrash = useCallback(async (paths: string[]) => { - let ok = 0 - for (const path of paths) { - try { await entryActions.handleTrashNote(path); ok++ } - catch { /* error toast already shown by flushBeforeAction */ } - } - if (ok > 0) setToastMessage(`${ok} note${ok > 1 ? 's' : ''} moved to trash`) - }, [entryActions, setToastMessage]) - - const handleBulkRestore = useCallback(async (paths: string[]) => { - let ok = 0 - for (const path of paths) { - try { await entryActions.handleRestoreNote(path); ok++ } - catch { /* skip — error toast already shown */ } - } - if (ok > 0) setToastMessage(`${ok} note${ok > 1 ? 's' : ''} restored`) - }, [entryActions, setToastMessage]) - - return { handleBulkArchive, handleBulkTrash, handleBulkRestore } + return { handleBulkArchive } } diff --git a/src/hooks/useCommandRegistry.test.ts b/src/hooks/useCommandRegistry.test.ts index 823acce6..b31ed578 100644 --- a/src/hooks/useCommandRegistry.test.ts +++ b/src/hooks/useCommandRegistry.test.ts @@ -13,8 +13,7 @@ function makeConfig(overrides: Record = {}) { onCreateNoteOfType: vi.fn(), onSave: vi.fn(), onOpenSettings: vi.fn(), - onTrashNote: vi.fn(), - onRestoreNote: vi.fn(), + onDeleteNote: vi.fn(), onArchiveNote: vi.fn(), onUnarchiveNote: vi.fn(), onCommitPush: vi.fn(), @@ -186,16 +185,9 @@ describe('extractVaultTypes', () => { expect(types).toHaveLength(2) }) - it('excludes trashed entries', () => { - const entries = [ - { path: '/a', title: 'A', isA: 'Project', trashed: true }, - ] as never[] - expect(extractVaultTypes(entries)).toEqual(['Event', 'Person', 'Project', 'Note']) - }) - it('includes types from Type definition entries', () => { const entries = [ - { path: '/book.md', title: 'Book', isA: 'Type', trashed: false }, + { path: '/book.md', title: 'Book', isA: 'Type' }, ] as never[] const types = extractVaultTypes(entries) expect(types).toContain('Book') @@ -203,9 +195,9 @@ describe('extractVaultTypes', () => { it('includes types from both definitions and instances', () => { const entries = [ - { path: '/book.md', title: 'Book', isA: 'Type', trashed: false }, - { path: '/hp.md', title: 'Harry Potter', isA: 'Book', trashed: false }, - { path: '/person.md', title: 'Person', isA: 'Type', trashed: false }, + { path: '/book.md', title: 'Book', isA: 'Type' }, + { path: '/hp.md', title: 'Harry Potter', isA: 'Book' }, + { path: '/person.md', title: 'Person', isA: 'Type' }, ] as never[] const types = extractVaultTypes(entries) expect(types).toContain('Book') @@ -213,12 +205,6 @@ describe('extractVaultTypes', () => { expect(types).toHaveLength(2) }) - it('excludes trashed Type definition entries', () => { - const entries = [ - { path: '/book.md', title: 'Book', isA: 'Type', trashed: true }, - ] as never[] - expect(extractVaultTypes(entries)).toEqual(['Event', 'Person', 'Project', 'Note']) - }) }) describe('groupSortKey', () => { diff --git a/src/hooks/useCommandRegistry.ts b/src/hooks/useCommandRegistry.ts index a9b247fb..3e8dcd6a 100644 --- a/src/hooks/useCommandRegistry.ts +++ b/src/hooks/useCommandRegistry.ts @@ -23,8 +23,6 @@ interface CommandRegistryConfig { activeNoteHasIcon?: boolean mcpStatus?: string onInstallMcp?: () => void - onEmptyTrash?: () => void - trashedCount?: number onReloadVault?: () => void onRepairVault?: () => void onSetNoteIcon?: () => void @@ -39,8 +37,7 @@ interface CommandRegistryConfig { onOpenSettings: () => void onOpenVault?: () => void onCreateType?: () => void - onTrashNote: (path: string) => void - onRestoreNote: (path: string) => void + onDeleteNote: (path: string) => void onArchiveNote: (path: string) => void onUnarchiveNote: (path: string) => void onCommitPush: () => void @@ -76,7 +73,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com const { activeTabPath, entries, modifiedCount, onQuickOpen, onCreateNote, onCreateNoteOfType, onSave, onOpenSettings, - onTrashNote, onRestoreNote, onArchiveNote, onUnarchiveNote, + onDeleteNote, onArchiveNote, onUnarchiveNote, onCommitPush, onPull, onResolveConflicts, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onToggleAIChat, onOpenVault, activeNoteModified, onZoomIn, onZoomOut, onZoomReset, zoomLevel, @@ -84,7 +81,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com onGoBack, onGoForward, canGoBack, canGoForward, onCheckForUpdates, onCreateType, onRemoveActiveVault, onRestoreGettingStarted, isGettingStartedHidden, vaultCount, - mcpStatus, onInstallMcp, onEmptyTrash, trashedCount, + mcpStatus, onInstallMcp, onReloadVault, onRepairVault, onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon, onOpenInNewWindow, onToggleFavorite, onToggleOrganized, @@ -98,7 +95,6 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com [entries, activeTabPath, hasActiveNote], ) const isArchived = activeEntry?.archived ?? false - const isTrashed = activeEntry?.trashed ?? false const isFavorite = activeEntry?.favorite ?? false const isSectionGroup = selection?.kind === 'sectionGroup' @@ -107,10 +103,10 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com return useMemo(() => [ ...buildNavigationCommands({ onQuickOpen, onSelect, onOpenDailyNote, onGoBack, onGoForward, canGoBack, canGoForward }), ...buildNoteCommands({ - hasActiveNote, activeTabPath, isArchived, isTrashed, + hasActiveNote, activeTabPath, isArchived, onCreateNote, onCreateType, onOpenDailyNote, onSave, - onTrashNote, onRestoreNote, onArchiveNote, onUnarchiveNote, - onEmptyTrash, trashedCount, onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon, onOpenInNewWindow, onToggleFavorite, isFavorite, + onDeleteNote, onArchiveNote, onUnarchiveNote, + onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon, onOpenInNewWindow, onToggleFavorite, isFavorite, onToggleOrganized, isOrganized: activeEntry?.organized ?? false, }), ...buildGitCommands({ modifiedCount, onCommitPush, onPull, onResolveConflicts, onSelect }), @@ -126,9 +122,9 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com ...buildTypeCommands(vaultTypes, onCreateNoteOfType, onSelect), ...buildFilterCommands({ isSectionGroup, noteListFilter, onSetNoteListFilter }), ], [ - hasActiveNote, activeTabPath, isArchived, isTrashed, modifiedCount, activeNoteModified, + hasActiveNote, activeTabPath, isArchived, modifiedCount, activeNoteModified, onQuickOpen, onCreateNote, onCreateNoteOfType, onCreateType, onSave, onOpenSettings, - onTrashNote, onRestoreNote, onArchiveNote, onUnarchiveNote, + onDeleteNote, onArchiveNote, onUnarchiveNote, onCommitPush, onPull, onResolveConflicts, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onToggleAIChat, onOpenVault, onCheckForUpdates, onZoomIn, onZoomOut, onZoomReset, zoomLevel, @@ -136,7 +132,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com onGoBack, onGoForward, canGoBack, canGoForward, vaultTypes, onRemoveActiveVault, onRestoreGettingStarted, isGettingStartedHidden, vaultCount, - mcpStatus, onInstallMcp, onEmptyTrash, trashedCount, + mcpStatus, onInstallMcp, onReloadVault, onRepairVault, onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon, isSectionGroup, noteListFilter, onSetNoteListFilter, diff --git a/src/hooks/useDeleteActions.test.ts b/src/hooks/useDeleteActions.test.ts index 5745a28f..72d42414 100644 --- a/src/hooks/useDeleteActions.test.ts +++ b/src/hooks/useDeleteActions.test.ts @@ -10,10 +10,6 @@ vi.mock('../mock-tauri', () => ({ const { mockInvoke } = await import('../mock-tauri') const mockInvokeFn = mockInvoke as ReturnType -function makeEntry(path: string, trashed = false) { - return { path, trashed } as { path: string; trashed: boolean } -} - describe('useDeleteActions', () => { let onDeselectNote: ReturnType let removeEntry: ReturnType @@ -26,11 +22,9 @@ describe('useDeleteActions', () => { mockInvokeFn.mockReset() }) - function renderDeleteActions(entries = [makeEntry('/vault/a.md'), makeEntry('/vault/t.md', true)]) { + function renderDeleteActions() { return renderHook(() => useDeleteActions({ - vaultPath: '/vault', - entries, onDeselectNote, removeEntry, setToastMessage, @@ -38,24 +32,6 @@ describe('useDeleteActions', () => { ) } - // --- trashedCount --- - - describe('trashedCount', () => { - it('counts trashed entries', () => { - const { result } = renderDeleteActions([ - makeEntry('/vault/a.md'), - makeEntry('/vault/b.md', true), - makeEntry('/vault/c.md', true), - ]) - expect(result.current.trashedCount).toBe(2) - }) - - it('returns 0 when no trashed entries', () => { - const { result } = renderDeleteActions([makeEntry('/vault/a.md')]) - expect(result.current.trashedCount).toBe(0) - }) - }) - // --- deleteNoteFromDisk --- describe('deleteNoteFromDisk', () => { @@ -145,65 +121,6 @@ describe('useDeleteActions', () => { }) }) - // --- handleEmptyTrash --- - - describe('handleEmptyTrash', () => { - it('does nothing when trashedCount is 0', () => { - const { result } = renderDeleteActions([makeEntry('/vault/a.md')]) - act(() => { - result.current.handleEmptyTrash() - }) - expect(result.current.confirmDelete).toBeNull() - }) - - it('sets confirmDelete with trash count in message', () => { - const { result } = renderDeleteActions([ - makeEntry('/vault/a.md'), - makeEntry('/vault/t1.md', true), - makeEntry('/vault/t2.md', true), - ]) - act(() => { - result.current.handleEmptyTrash() - }) - expect(result.current.confirmDelete?.title).toBe('Empty Trash?') - expect(result.current.confirmDelete?.confirmLabel).toBe('Empty Trash') - }) - - it('onConfirm invokes empty_trash, closes tabs, removes entries', async () => { - mockInvokeFn.mockResolvedValue(['/vault/t1.md', '/vault/t2.md']) - const { result } = renderDeleteActions([ - makeEntry('/vault/a.md'), - makeEntry('/vault/t1.md', true), - makeEntry('/vault/t2.md', true), - ]) - act(() => { - result.current.handleEmptyTrash() - }) - await act(async () => { - await result.current.confirmDelete!.onConfirm() - }) - expect(result.current.confirmDelete).toBeNull() - expect(mockInvokeFn).toHaveBeenCalledWith('empty_trash', { vaultPath: '/vault' }) - expect(onDeselectNote).toHaveBeenCalledWith('/vault/t1.md') - expect(onDeselectNote).toHaveBeenCalledWith('/vault/t2.md') - expect(removeEntry).toHaveBeenCalledWith('/vault/t1.md') - expect(removeEntry).toHaveBeenCalledWith('/vault/t2.md') - expect(setToastMessage).toHaveBeenCalledWith('2 notes permanently deleted') - }) - - it('shows error toast when empty_trash fails', async () => { - mockInvokeFn.mockRejectedValue(new Error('oops')) - const { result } = renderDeleteActions([makeEntry('/vault/t.md', true)]) - act(() => { - result.current.handleEmptyTrash() - }) - await act(async () => { - await result.current.confirmDelete!.onConfirm() - }) - expect(setToastMessage).toHaveBeenCalledWith(expect.stringContaining('Failed to empty trash')) - }) - }) - // --- setConfirmDelete --- describe('setConfirmDelete', () => { diff --git a/src/hooks/useDeleteActions.ts b/src/hooks/useDeleteActions.ts index c2aefb2a..24f5b691 100644 --- a/src/hooks/useDeleteActions.ts +++ b/src/hooks/useDeleteActions.ts @@ -1,7 +1,6 @@ -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useState } from 'react' import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from '../mock-tauri' -import type { VaultEntry } from '../types' import { trackEvent } from '../lib/telemetry' interface ConfirmDeleteState { @@ -12,8 +11,6 @@ interface ConfirmDeleteState { } interface UseDeleteActionsInput { - vaultPath: string - entries: VaultEntry[] /** Called to deselect the note if it is currently open. */ onDeselectNote: (path: string) => void removeEntry: (path: string) => void @@ -21,16 +18,12 @@ interface UseDeleteActionsInput { } export function useDeleteActions({ - vaultPath, - entries, onDeselectNote, removeEntry, setToastMessage, }: UseDeleteActionsInput) { const [confirmDelete, setConfirmDelete] = useState(null) - const trashedCount = useMemo(() => entries.filter(e => e.trashed).length, [entries]) - const deleteNoteFromDisk = useCallback(async (path: string) => { try { if (isTauri()) await invoke('delete_note', { path }) @@ -47,7 +40,7 @@ export function useDeleteActions({ const handleDeleteNote = useCallback(async (path: string) => { setConfirmDelete({ title: 'Delete permanently?', - message: 'This note will be permanently deleted. This cannot be undone.', + message: 'Delete permanently? This cannot be undone. You can recover it from Git history.', onConfirm: async () => { setConfirmDelete(null) const ok = await deleteNoteFromDisk(path) @@ -75,36 +68,11 @@ export function useDeleteActions({ }) }, [deleteNoteFromDisk, setToastMessage]) - const handleEmptyTrash = useCallback(() => { - if (trashedCount === 0) return - setConfirmDelete({ - title: 'Empty Trash?', - message: `Permanently delete all ${trashedCount} trashed ${trashedCount === 1 ? 'note' : 'notes'}? This cannot be undone.`, - confirmLabel: 'Empty Trash', - onConfirm: async () => { - setConfirmDelete(null) - try { - const tauriInvoke = isTauri() ? invoke : mockInvoke - const deleted = await tauriInvoke('empty_trash', { vaultPath }) - for (const path of deleted) { - onDeselectNote(path) - removeEntry(path) - } - setToastMessage(`${deleted.length} note${deleted.length !== 1 ? 's' : ''} permanently deleted`) - } catch (e) { - setToastMessage(`Failed to empty trash: ${e}`) - } - }, - }) - }, [trashedCount, vaultPath, onDeselectNote, removeEntry, setToastMessage]) - return { confirmDelete, setConfirmDelete, - trashedCount, deleteNoteFromDisk, handleDeleteNote, handleBulkDeletePermanently, - handleEmptyTrash, } } diff --git a/src/hooks/useEntryActions.test.ts b/src/hooks/useEntryActions.test.ts index 5b4efe95..209aa7fd 100644 --- a/src/hooks/useEntryActions.test.ts +++ b/src/hooks/useEntryActions.test.ts @@ -13,8 +13,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ relatedTo: [], status: 'Active', archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, @@ -59,58 +57,6 @@ describe('useEntryActions', () => { vi.clearAllMocks() }) - describe('handleTrashNote', () => { - it('sets trashed frontmatter and updates entry state', async () => { - const { result } = setup() - - await act(async () => { - 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(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', { - trashed: true, - trashedAt: expect.any(Number), - }) - expect(setToastMessage).toHaveBeenCalledWith('Note moved to trash') - expect(onFrontmatterPersisted).toHaveBeenCalledTimes(1) - }) - - it('final toast is contextual, not "Property updated"', async () => { - const { result } = setup() - const toastCalls: (string | null)[] = [] - setToastMessage.mockImplementation((msg: string | null) => toastCalls.push(msg)) - - await act(async () => { - await result.current.handleTrashNote('/vault/note/test.md') - }) - - // The only toast should be "Note moved to trash", never "Property updated" - expect(toastCalls).toEqual(['Note moved to trash']) - }) - }) - - describe('handleRestoreNote', () => { - it('clears trashed frontmatter and updates entry state', async () => { - const { result } = setup() - - await act(async () => { - 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(handleUpdateFrontmatter).not.toHaveBeenCalled() - expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', { - trashed: false, - trashedAt: null, - }) - expect(setToastMessage).toHaveBeenCalledWith('Note restored from trash') - expect(onFrontmatterPersisted).toHaveBeenCalledTimes(1) - }) - }) - describe('handleArchiveNote', () => { it('sets archived frontmatter and updates entry state', async () => { const { result } = setup() @@ -431,28 +377,6 @@ describe('useEntryActions', () => { }) describe('optimistic rollback on disk write failure', () => { - it('rolls back trashed state when frontmatter write fails', async () => { - handleUpdateFrontmatter.mockRejectedValueOnce(new Error('disk full')) - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const { result } = setup() - - await act(async () => { - await result.current.handleTrashNote('/vault/note/test.md') - }) - - // First call: optimistic update (trashed: true) - // Second call: rollback (trashed: false) - expect(updateEntry).toHaveBeenCalledTimes(2) - expect(updateEntry).toHaveBeenNthCalledWith(1, '/vault/note/test.md', { - trashed: true, trashedAt: expect.any(Number), - }) - expect(updateEntry).toHaveBeenNthCalledWith(2, '/vault/note/test.md', { - trashed: false, trashedAt: null, - }) - expect(setToastMessage).toHaveBeenCalledWith('Failed to trash note — rolled back') - errorSpy.mockRestore() - }) - it('rolls back archived state when frontmatter write fails', async () => { handleUpdateFrontmatter.mockRejectedValueOnce(new Error('disk full')) const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) @@ -469,24 +393,6 @@ describe('useEntryActions', () => { errorSpy.mockRestore() }) - it('rolls back restore state when frontmatter write fails', async () => { - handleDeleteProperty.mockRejectedValueOnce(new Error('disk full')) - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const { result } = setup() - - await act(async () => { - await result.current.handleRestoreNote('/vault/note/test.md') - }) - - expect(updateEntry).toHaveBeenCalledTimes(2) - expect(updateEntry).toHaveBeenNthCalledWith(1, '/vault/note/test.md', { trashed: false, trashedAt: null }) - expect(updateEntry).toHaveBeenNthCalledWith(2, '/vault/note/test.md', { - trashed: true, trashedAt: expect.any(Number), - }) - expect(setToastMessage).toHaveBeenCalledWith('Failed to restore note — rolled back') - errorSpy.mockRestore() - }) - it('rolls back unarchive state when frontmatter write fails', async () => { handleDeleteProperty.mockRejectedValueOnce(new Error('disk full')) const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) @@ -503,22 +409,6 @@ describe('useEntryActions', () => { errorSpy.mockRestore() }) - it('trash: updateEntry is called BEFORE frontmatter writes (optimistic)', async () => { - const callOrder: string[] = [] - updateEntry.mockImplementation(() => { callOrder.push('updateEntry') }) - handleUpdateFrontmatter.mockImplementation(() => { - callOrder.push('handleUpdateFrontmatter') - return Promise.resolve() - }) - const { result } = setup() - - await act(async () => { - await result.current.handleTrashNote('/vault/note/test.md') - }) - - expect(callOrder[0]).toBe('updateEntry') - expect(callOrder[1]).toBe('handleUpdateFrontmatter') - }) }) describe('handleToggleFavorite', () => { @@ -619,27 +509,6 @@ describe('useEntryActions', () => { ) } - it('calls onBeforeAction before trashing a note', async () => { - const callOrder: string[] = [] - const onBeforeAction = vi.fn().mockImplementation(() => { - callOrder.push('beforeAction') - return Promise.resolve() - }) - handleUpdateFrontmatter.mockImplementation(() => { - callOrder.push('updateFrontmatter') - return Promise.resolve() - }) - const { result } = setupWithBeforeAction(onBeforeAction) - - await act(async () => { - await result.current.handleTrashNote('/vault/note/test.md') - }) - - expect(onBeforeAction).toHaveBeenCalledWith('/vault/note/test.md') - expect(callOrder[0]).toBe('beforeAction') - expect(callOrder[1]).toBe('updateFrontmatter') - }) - it('calls onBeforeAction before archiving a note', async () => { const onBeforeAction = vi.fn().mockResolvedValue(undefined) const { result } = setupWithBeforeAction(onBeforeAction) @@ -652,7 +521,6 @@ describe('useEntryActions', () => { }) it.each([ - ['trash', 'handleTrashNote'] as const, ['archive', 'handleArchiveNote'] as const, ])('does not proceed with %s when onBeforeAction rejects', async (_label, method) => { const { result } = setupWithBeforeAction(vi.fn().mockRejectedValue(new Error('Save failed'))) diff --git a/src/hooks/useEntryActions.ts b/src/hooks/useEntryActions.ts index b62a80e4..00dc3211 100644 --- a/src/hooks/useEntryActions.ts +++ b/src/hooks/useEntryActions.ts @@ -28,40 +28,6 @@ async function findOrCreateType( export function useEntryActions({ entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, setToastMessage, createTypeEntry, onFrontmatterPersisted, onBeforeAction, }: EntryActionsConfig) { - const handleTrashNote = useCallback(async (path: string) => { - await onBeforeAction?.(path) - // Optimistic: update UI immediately, write to disk async with rollback on failure - const trashedAt = Date.now() / 1000 - updateEntry(path, { trashed: true, trashedAt }) - trackEvent('note_trashed') - 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 }) - onFrontmatterPersisted?.() - } catch (err) { - updateEntry(path, { trashed: false, trashedAt: null }) - setToastMessage('Failed to trash note — rolled back') - console.error('Optimistic trash rollback:', err) - } - }, [onBeforeAction, handleUpdateFrontmatter, updateEntry, setToastMessage, onFrontmatterPersisted]) - - const handleRestoreNote = useCallback(async (path: string) => { - // Optimistic: update UI immediately - 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 }) - onFrontmatterPersisted?.() - } catch (err) { - updateEntry(path, { trashed: true, trashedAt: Date.now() / 1000 }) - setToastMessage('Failed to restore note — rolled back') - console.error('Optimistic restore rollback:', err) - } - }, [handleDeleteProperty, updateEntry, setToastMessage, onFrontmatterPersisted]) - const handleArchiveNote = useCallback(async (path: string) => { await onBeforeAction?.(path) // Optimistic: update UI immediately, write to disk async with rollback on failure @@ -204,5 +170,5 @@ export function useEntryActions({ onFrontmatterPersisted?.() }, [entries, handleUpdateFrontmatter, handleDeleteProperty, updateEntry, createTypeEntry, onFrontmatterPersisted]) - return { handleTrashNote, handleRestoreNote, handleArchiveNote, handleUnarchiveNote, handleCustomizeType, handleReorderSections, handleUpdateTypeTemplate, handleRenameSection, handleToggleTypeVisibility, handleToggleFavorite, handleToggleOrganized, handleReorderFavorites } + return { handleArchiveNote, handleUnarchiveNote, handleCustomizeType, handleReorderSections, handleUpdateTypeTemplate, handleRenameSection, handleToggleTypeVisibility, handleToggleFavorite, handleToggleOrganized, handleReorderFavorites } } diff --git a/src/hooks/useKeyboardNavigation.test.ts b/src/hooks/useKeyboardNavigation.test.ts index 7e93930a..4be08ab8 100644 --- a/src/hooks/useKeyboardNavigation.test.ts +++ b/src/hooks/useKeyboardNavigation.test.ts @@ -17,8 +17,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ relatedTo: [], status: 'Active', archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, diff --git a/src/hooks/useMenuEvents.test.ts b/src/hooks/useMenuEvents.test.ts index b1b32b81..2fdaee04 100644 --- a/src/hooks/useMenuEvents.test.ts +++ b/src/hooks/useMenuEvents.test.ts @@ -16,7 +16,7 @@ function makeHandlers(): MenuEventHandlers { onZoomOut: vi.fn(), onZoomReset: vi.fn(), onArchiveNote: vi.fn(), - onTrashNote: vi.fn(), + onDeleteNote: vi.fn(), onSearch: vi.fn(), onToggleRawEditor: vi.fn(), onToggleDiff: vi.fn(), @@ -141,17 +141,17 @@ describe('dispatchMenuEvent', () => { expect(h.onArchiveNote).not.toHaveBeenCalled() }) - it('note-trash triggers trash on active tab', () => { + it('note-delete triggers delete on active tab', () => { const h = makeHandlers() - dispatchMenuEvent('note-trash', h) - expect(h.onTrashNote).toHaveBeenCalledWith('/vault/test.md') + dispatchMenuEvent('note-delete', h) + expect(h.onDeleteNote).toHaveBeenCalledWith('/vault/test.md') }) - it('note-trash does nothing when no active tab', () => { + it('note-delete does nothing when no active tab', () => { const h = makeHandlers() h.activeTabPathRef = { current: null } - dispatchMenuEvent('note-trash', h) - expect(h.onTrashNote).not.toHaveBeenCalled() + dispatchMenuEvent('note-delete', h) + expect(h.onDeleteNote).not.toHaveBeenCalled() }) // Optional handler events @@ -218,12 +218,6 @@ describe('dispatchMenuEvent', () => { expect(h.onSelectFilter).toHaveBeenCalledWith('archived') }) - it('go-trash selects trash filter', () => { - const h = makeHandlers() - dispatchMenuEvent('go-trash', h) - expect(h.onSelectFilter).toHaveBeenCalledWith('trash') - }) - it('go-changes selects changes filter', () => { const h = makeHandlers() dispatchMenuEvent('go-changes', h) diff --git a/src/hooks/useMenuEvents.ts b/src/hooks/useMenuEvents.ts index f2df6ef6..92511a63 100644 --- a/src/hooks/useMenuEvents.ts +++ b/src/hooks/useMenuEvents.ts @@ -17,7 +17,7 @@ export interface MenuEventHandlers { onZoomOut: () => void onZoomReset: () => void onArchiveNote: (path: string) => void - onTrashNote: (path: string) => void + onDeleteNote: (path: string) => void onSearch: () => void onToggleRawEditor?: () => void onToggleDiff?: () => void @@ -37,7 +37,6 @@ export interface MenuEventHandlers { onOpenInNewWindow?: () => void onReloadVault?: () => void onRepairVault?: () => void - onEmptyTrash?: () => void activeTabPathRef: React.MutableRefObject activeTabPath: string | null modifiedCount?: number @@ -70,7 +69,6 @@ const SIMPLE_EVENT_MAP: Record = { const FILTER_MAP: Record = { 'go-all-notes': 'all', 'go-archived': 'archived', - 'go-trash': 'trash', 'go-changes': 'changes', 'go-inbox': 'inbox', } @@ -80,7 +78,6 @@ type OptionalHandler = | 'onCreateType' | 'onToggleRawEditor' | 'onToggleDiff' | 'onToggleAIChat' | 'onOpenVault' | 'onRemoveActiveVault' | 'onRestoreGettingStarted' | 'onCommitPush' | 'onPull' | 'onResolveConflicts' | 'onViewChanges' | 'onInstallMcp' | 'onReloadVault' | 'onRepairVault' - | 'onEmptyTrash' | 'onOpenInNewWindow' const OPTIONAL_EVENT_MAP: Record = { @@ -101,15 +98,14 @@ const OPTIONAL_EVENT_MAP: Record = { 'vault-install-mcp': 'onInstallMcp', 'vault-reload': 'onReloadVault', 'vault-repair': 'onRepairVault', - 'note-empty-trash': 'onEmptyTrash', 'note-open-in-new-window': 'onOpenInNewWindow', } function dispatchActiveTabEvent(id: string, h: MenuEventHandlers): boolean { const path = h.activeTabPathRef.current - if (!path) return id === 'note-archive' || id === 'note-trash' + if (!path) return id === 'note-archive' || id === 'note-delete' if (id === 'note-archive') { h.onArchiveNote(path); return true } - if (id === 'note-trash') { h.onTrashNote(path); return true } + if (id === 'note-delete') { h.onDeleteNote(path); return true } return false } diff --git a/src/hooks/useNoteActions.test.ts b/src/hooks/useNoteActions.test.ts index f1fea1e9..00818dd9 100644 --- a/src/hooks/useNoteActions.test.ts +++ b/src/hooks/useNoteActions.test.ts @@ -47,8 +47,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ relatedTo: [], status: 'Active', archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, @@ -125,7 +123,6 @@ describe('buildNewEntry', () => { expect(entry.isA).toBe('Note') expect(entry.status).toBe('Active') expect(entry.archived).toBe(false) - expect(entry.trashed).toBe(false) expect(entry.modifiedAt).toBeGreaterThan(0) expect(entry.createdAt).toBe(entry.modifiedAt) }) @@ -339,7 +336,6 @@ describe('frontmatterToEntryPatch', () => { ['icon', 'star', { icon: 'star' }], ['sidebar_label', 'Projects', { sidebarLabel: 'Projects' }], ['archived', true, { archived: true }], - ['trashed', true, { trashed: true }], ['order', 5, { order: 5 }], ['template', '## Heading\n\n', { template: '## Heading\n\n' }], ['visible', false, { visible: false }], diff --git a/src/hooks/useNoteCreation.test.ts b/src/hooks/useNoteCreation.test.ts index 749a2003..d01e540b 100644 --- a/src/hooks/useNoteCreation.test.ts +++ b/src/hooks/useNoteCreation.test.ts @@ -32,7 +32,7 @@ vi.mock('../mock-tauri', () => ({ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ path: '/vault/test.md', filename: 'test.md', title: 'Test Note', isA: 'Note', - aliases: [], belongsTo: [], relatedTo: [], status: 'Active', archived: false, trashed: false, trashedAt: null, + aliases: [], belongsTo: [], relatedTo: [], status: 'Active', archived: false, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, outgoingLinks: [], template: null, sort: null, sidebarLabel: null, @@ -68,7 +68,6 @@ describe('buildNewEntry', () => { expect(entry.isA).toBe('Note') expect(entry.status).toBe('Active') expect(entry.archived).toBe(false) - expect(entry.trashed).toBe(false) }) it('sets null status when provided', () => { diff --git a/src/hooks/useNoteCreation.ts b/src/hooks/useNoteCreation.ts index 9c4a4c6f..ea6d1690 100644 --- a/src/hooks/useNoteCreation.ts +++ b/src/hooks/useNoteCreation.ts @@ -18,7 +18,7 @@ export function buildNewEntry({ path, slug, title, type, status }: NewEntryParam return { path, filename: `${slug}.md`, title, isA: type, aliases: [], belongsTo: [], relatedTo: [], - status, archived: false, trashed: false, trashedAt: null, + status, archived: false, modifiedAt: now, createdAt: now, fileSize: 0, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, outgoingLinks: [], sidebarLabel: null, template: null, sort: null, view: null, visible: null, properties: {}, organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [], } diff --git a/src/hooks/useNoteListKeyboard.test.ts b/src/hooks/useNoteListKeyboard.test.ts index cc2247cc..ae36a462 100644 --- a/src/hooks/useNoteListKeyboard.test.ts +++ b/src/hooks/useNoteListKeyboard.test.ts @@ -15,8 +15,6 @@ function makeEntry(path: string, title: string): VaultEntry { status: null, favorite: false, archived: false, - trashed: false, - trashedAt: null, createdAt: null, modifiedAt: null, fileSize: 100, diff --git a/src/hooks/useNoteRename.test.ts b/src/hooks/useNoteRename.test.ts index ec73647f..25f8f29a 100644 --- a/src/hooks/useNoteRename.test.ts +++ b/src/hooks/useNoteRename.test.ts @@ -20,7 +20,7 @@ vi.mock('../mock-tauri', () => ({ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ path: '/vault/test.md', filename: 'test.md', title: 'Test Note', isA: 'Note', - aliases: [], belongsTo: [], relatedTo: [], status: 'Active', archived: false, trashed: false, trashedAt: null, + aliases: [], belongsTo: [], relatedTo: [], status: 'Active', archived: false, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, outgoingLinks: [], template: null, sort: null, sidebarLabel: null, diff --git a/src/hooks/useNoteSearch.test.ts b/src/hooks/useNoteSearch.test.ts index fa8f066b..6d93389e 100644 --- a/src/hooks/useNoteSearch.test.ts +++ b/src/hooks/useNoteSearch.test.ts @@ -13,8 +13,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ relatedTo: [], status: 'Active', archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, @@ -218,26 +216,6 @@ describe('useNoteSearch', () => { expect(result.current.results[0].title).toBe('Refactoring Notes') }) - it('excludes trashed notes from results', () => { - const withTrashed: VaultEntry[] = [ - makeEntry({ path: '/vault/a.md', title: 'Active Note', modifiedAt: 1700000003 }), - makeEntry({ path: '/vault/t.md', title: 'Trashed Note', trashed: true, trashedAt: 1700000000, modifiedAt: 1700000002 }), - makeEntry({ path: '/vault/b.md', title: 'Another Active', modifiedAt: 1700000001 }), - ] - const { result } = renderHook(() => useNoteSearch(withTrashed, '')) - expect(result.current.results.map(r => r.title)).toEqual(['Active Note', 'Another Active']) - }) - - it('excludes trashed notes from search results by query', () => { - const withTrashed: VaultEntry[] = [ - makeEntry({ path: '/vault/a.md', title: 'Meeting Notes', modifiedAt: 1700000002 }), - makeEntry({ path: '/vault/t.md', title: 'Meeting Draft', trashed: true, modifiedAt: 1700000001 }), - ] - const { result } = renderHook(() => useNoteSearch(withTrashed, 'Meeting')) - expect(result.current.results).toHaveLength(1) - expect(result.current.results[0].title).toBe('Meeting Notes') - }) - it('does not exclude archived notes from results', () => { const withArchived: VaultEntry[] = [ makeEntry({ path: '/vault/a.md', title: 'Active Note', modifiedAt: 1700000002 }), diff --git a/src/hooks/useNoteSearch.ts b/src/hooks/useNoteSearch.ts index fee375f2..424333f2 100644 --- a/src/hooks/useNoteSearch.ts +++ b/src/hooks/useNoteSearch.ts @@ -34,7 +34,7 @@ export function useNoteSearch(entries: VaultEntry[], query: string, maxResults = const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) const searchableEntries = useMemo( - () => entries.filter((e) => !e.trashed && !SEARCH_EXCLUDED_TYPES.has(e.isA ?? '')), + () => entries.filter((e) => !SEARCH_EXCLUDED_TYPES.has(e.isA ?? '')), [entries], ) diff --git a/src/hooks/usePropertyPanelState.ts b/src/hooks/usePropertyPanelState.ts index 0911af75..97bf8608 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', '_trashed_at', 'trashed_at', 'trashed at', '_archived', 'archived', 'archived_at', 'icon', '_favorite', '_favorite_index', '_organized']) +const SKIP_KEYS = new Set(['aliases', 'workspace', 'title', 'type', 'is_a', 'is a', '_archived', 'archived', 'archived_at', 'icon', '_favorite', '_favorite_index', '_organized']) function coerceValue(raw: string): FrontmatterValue { if (raw.toLowerCase() === 'true') return true diff --git a/src/hooks/useTabManagement.test.ts b/src/hooks/useTabManagement.test.ts index 3235a691..bf6078f0 100644 --- a/src/hooks/useTabManagement.test.ts +++ b/src/hooks/useTabManagement.test.ts @@ -19,8 +19,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ relatedTo: [], status: 'Active', archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, @@ -171,20 +169,20 @@ describe('useTabManagement (single-note model)', () => { describe('setTabs entry sync', () => { it('updates note entry via setTabs mapper (vault entry sync pattern)', async () => { const { result } = renderHook(() => useTabManagement()) - const entry = makeEntry({ path: '/vault/a.md', trashed: false }) + const entry = makeEntry({ path: '/vault/a.md', archived: false }) await act(async () => { await result.current.handleSelectNote(entry) }) - const freshEntry = { ...entry, trashed: true, trashedAt: Date.now() / 1000 } + const freshEntry = { ...entry, archived: true } act(() => { result.current.setTabs(prev => prev.map(tab => tab.entry.path === freshEntry.path ? { ...tab, entry: freshEntry } : tab )) }) - expect(result.current.tabs[0].entry.trashed).toBe(true) + expect(result.current.tabs[0].entry.archived).toBe(true) }) }) diff --git a/src/hooks/useVaultLoader.test.ts b/src/hooks/useVaultLoader.test.ts index 128ff363..e887121f 100644 --- a/src/hooks/useVaultLoader.test.ts +++ b/src/hooks/useVaultLoader.test.ts @@ -8,7 +8,7 @@ const mockEntries: VaultEntry[] = [ path: '/vault/note/hello.md', filename: 'hello.md', title: 'Hello', isA: 'Note', aliases: [], belongsTo: [], relatedTo: [], status: 'Active', - archived: false, trashed: false, trashedAt: null, + archived: false, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, template: null, sort: null, outgoingLinks: [], }, diff --git a/src/mock-tauri/mock-content.ts b/src/mock-tauri/mock-content.ts index fbd68c81..349f6471 100644 --- a/src/mock-tauri/mock-content.ts +++ b/src/mock-tauri/mock-content.ts @@ -623,44 +623,6 @@ A **book** you're reading or have read. Track reading progress, notes, and key t - **Author**: The book's author - **Status**: Reading, Finished, Abandoned - **Rating**: 1-5 stars -`, - '/Users/luca/Laputa/old-draft-notes.md': `--- -title: Old Draft Notes -type: Note -trashed: true -trashed_at: ${new Date(Date.now() - 86400000 * 5).toISOString().slice(0, 10)} -belongs_to: - - "[[26q1-laputa-app]]" ---- - -# Old Draft Notes - -Some rough draft content that is no longer relevant. Moving to trash. -`, - '/Users/luca/Laputa/deprecated-api-notes.md': `--- -title: Deprecated API Notes -type: Note -trashed: true -trashed_at: ${new Date(Date.now() - 86400000 * 35).toISOString().slice(0, 10)} ---- - -# Deprecated API Notes - -Old API documentation for the v1 endpoint. Replaced by v2 docs. -`, - '/Users/luca/Laputa/failed-seo-experiment.md': `--- -title: Failed SEO Experiment -type: Experiment -status: Dropped -trashed: true -trashed_at: ${new Date(Date.now() - 86400000 * 10).toISOString().slice(0, 10)} -related_to: - - "[[grow-newsletter]]" ---- - -# Failed SEO Experiment - -Tried programmatic SEO pages. Results were negligible — trashing this. `, '/Users/luca/Laputa/25q3-website-redesign.md': `--- title: Website Redesign diff --git a/src/mock-tauri/mock-entries.ts b/src/mock-tauri/mock-entries.ts index c9471ad7..8ac9bbad 100644 --- a/src/mock-tauri/mock-entries.ts +++ b/src/mock-tauri/mock-entries.ts @@ -18,8 +18,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: ['[[software-development]]'], status: 'Active', archived: false, - trashed: false, - trashedAt: null, modifiedAt: now, createdAt: now - 86400 * 60, fileSize: 2048, @@ -49,8 +47,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: ['[[growth]]'], status: 'Active', archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 3600, createdAt: now - 86400 * 180, fileSize: 1024, @@ -85,8 +81,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: 'Active', archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 7200, createdAt: now - 86400 * 150, fileSize: 890, @@ -115,8 +109,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: 'Paused', archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400, createdAt: now - 86400 * 120, fileSize: 512, @@ -145,8 +137,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: 'Done', archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 2, createdAt: now - 86400 * 100, fileSize: 640, @@ -175,8 +165,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: ['[[trading]]', '[[algorithmic-trading]]'], status: 'Paused', archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400, createdAt: now - 86400 * 45, fileSize: 3200, @@ -206,8 +194,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: ['[[growth]]', '[[ads]]'], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 3600 * 5, createdAt: now - 86400 * 30, fileSize: 847, @@ -237,8 +223,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400, createdAt: now - 86400 * 20, fileSize: 560, @@ -267,8 +251,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 7, createdAt: now - 86400 * 200, fileSize: 320, @@ -296,8 +278,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 3, createdAt: now - 86400 * 150, fileSize: 280, @@ -325,8 +305,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 5, createdAt: now - 86400 * 120, fileSize: 240, @@ -354,8 +332,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 10, createdAt: now - 86400 * 90, fileSize: 200, @@ -383,8 +359,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: ['[[26q1-laputa-app]]', '[[matteo-cellini]]'], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 3600 * 2, createdAt: now - 86400 * 7, fileSize: 1200, @@ -413,8 +387,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 30, createdAt: now - 86400 * 365, fileSize: 256, @@ -443,8 +415,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 14, createdAt: now - 86400 * 300, fileSize: 180, @@ -473,8 +443,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 3, createdAt: now - 86400 * 14, fileSize: 4200, @@ -503,8 +471,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: ['[[software-development]]'], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 7, createdAt: now - 86400 * 30, fileSize: 3800, @@ -534,8 +500,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 10, createdAt: now - 86400 * 21, fileSize: 5100, @@ -565,8 +529,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 90, createdAt: now - 86400 * 365, fileSize: 320, @@ -592,8 +554,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 90, createdAt: now - 86400 * 365, fileSize: 280, @@ -619,8 +579,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 90, createdAt: now - 86400 * 365, fileSize: 310, @@ -646,8 +604,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 90, createdAt: now - 86400 * 365, fileSize: 290, @@ -673,8 +629,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 90, createdAt: now - 86400 * 365, fileSize: 200, @@ -700,8 +654,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 90, createdAt: now - 86400 * 365, fileSize: 180, @@ -727,8 +679,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 90, createdAt: now - 86400 * 365, fileSize: 170, @@ -754,8 +704,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 90, createdAt: now - 86400 * 365, fileSize: 200, @@ -781,8 +729,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 90, createdAt: now - 86400 * 365, fileSize: 190, @@ -809,8 +755,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 30, createdAt: now - 86400 * 365, fileSize: 300, @@ -836,8 +780,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 30, createdAt: now - 86400 * 365, fileSize: 250, @@ -863,8 +805,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 30, createdAt: now - 86400 * 365, fileSize: 220, @@ -891,8 +831,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 5, createdAt: now - 86400 * 30, fileSize: 1200, @@ -920,8 +858,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 5, createdAt: now - 86400 * 10, fileSize: 420, @@ -949,8 +885,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 14, createdAt: now - 86400 * 60, fileSize: 380, @@ -968,96 +902,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ properties: { Author: 'Martin Kleppmann', Rating: 5, 'Year published': 2017 }, organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [], }, - // --- Trashed entries --- - { - path: '/Users/luca/Laputa/old-draft-notes.md', - filename: 'old-draft-notes.md', - title: 'Old Draft Notes', - isA: 'Note', - aliases: [], - belongsTo: ['[[26q1-laputa-app]]'], - relatedTo: [], - status: null, - archived: false, - trashed: true, - trashedAt: now - 86400 * 5, - modifiedAt: now - 86400 * 10, - createdAt: null, - fileSize: 280, - snippet: 'Some rough draft content that is no longer relevant. Moving to trash.', - wordCount: 100, - relationships: { - 'Belongs to': ['[[26q1-laputa-app]]'], - 'Type': ['[[note]]'], - }, - icon: null, - color: null, - order: null, - sidebarLabel: null, - template: null, sort: null, view: null, visible: null, - outgoingLinks: [], - properties: {}, - organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [], - }, - { - path: '/Users/luca/Laputa/deprecated-api-notes.md', - filename: 'deprecated-api-notes.md', - title: 'Deprecated API Notes', - isA: 'Note', - aliases: [], - belongsTo: [], - relatedTo: [], - status: null, - archived: false, - trashed: true, - trashedAt: now - 86400 * 35, - modifiedAt: now - 86400 * 40, - createdAt: null, - fileSize: 190, - snippet: 'Old API documentation for the v1 endpoint. Replaced by v2 docs.', - wordCount: 85, - relationships: { - 'Type': ['[[note]]'], - }, - icon: null, - color: null, - order: null, - sidebarLabel: null, - template: null, sort: null, view: null, visible: null, - outgoingLinks: [], - properties: {}, - organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [], - }, - { - path: '/Users/luca/Laputa/failed-seo-experiment.md', - filename: 'failed-seo-experiment.md', - title: 'Failed SEO Experiment', - isA: 'Experiment', - aliases: [], - belongsTo: [], - relatedTo: ['[[grow-newsletter]]'], - status: 'Dropped', - archived: false, - trashed: true, - trashedAt: now - 86400 * 10, - modifiedAt: now - 86400 * 15, - createdAt: null, - fileSize: 340, - snippet: 'Tried programmatic SEO pages. Results were negligible — trashing this.', - wordCount: 120, - relationships: { - 'Related to': ['[[grow-newsletter]]'], - 'Type': ['[[experiment]]'], - }, - icon: null, - color: null, - order: null, - sidebarLabel: null, - template: null, sort: null, view: null, visible: null, - outgoingLinks: [], - properties: { Owner: 'Luca Rossi' }, - organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [], - }, // --- Archived entries --- { path: '/Users/luca/Laputa/25q3-website-redesign.md', @@ -1069,8 +913,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: 'Done', archived: true, - trashed: false, - trashedAt: null, icon: null, color: null, order: null, @@ -1099,8 +941,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: ['[[grow-newsletter]]'], status: 'Done', archived: true, - trashed: false, - trashedAt: null, icon: null, color: null, order: null, @@ -1130,8 +970,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: 'Active', archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 30, createdAt: now - 86400 * 365, fileSize: 1200, @@ -1157,8 +995,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 5, createdAt: now - 86400 * 60, fileSize: 800, @@ -1184,8 +1020,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 10, createdAt: now - 86400 * 90, fileSize: 600, @@ -1211,8 +1045,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - 86400 * 15, createdAt: now - 86400 * 120, fileSize: 950, @@ -1264,8 +1096,6 @@ function generateBulkEntries(count: number): VaultEntry[] { relatedTo: [], status: i % 4 === 0 ? 'Active' : i % 4 === 1 ? 'Paused' : i % 4 === 2 ? 'Done' : null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: now - i * 600, createdAt: now - 86400 * 90 - i * 3600, fileSize: 500 + (i % 2000), diff --git a/src/mock-tauri/mock-handlers.ts b/src/mock-tauri/mock-handlers.ts index 8f9c706b..0f4a8edf 100644 --- a/src/mock-tauri/mock-handlers.ts +++ b/src/mock-tauri/mock-handlers.ts @@ -138,7 +138,7 @@ export const mockHandlers: Record any> = { save_view_cmd: () => {}, delete_view_cmd: () => {}, reload_vault: () => MOCK_ENTRIES, - reload_vault_entry: (args: { path: string }) => MOCK_ENTRIES.find(e => e.path === args.path) ?? { path: args.path, title: 'Unknown', filename: 'unknown.md', aliases: [], belongsTo: [], relatedTo: [], archived: false, trashed: false, snippet: '', wordCount: 0, fileSize: 0, relationships: {}, outgoingLinks: [], properties: {} }, + reload_vault_entry: (args: { path: string }) => MOCK_ENTRIES.find(e => e.path === args.path) ?? { path: args.path, title: 'Unknown', filename: 'unknown.md', aliases: [], belongsTo: [], relatedTo: [], archived: false, snippet: '', wordCount: 0, fileSize: 0, relationships: {}, outgoingLinks: [], properties: {} }, sync_note_title: () => false, get_note_content: (args: { path: string }) => MOCK_CONTENT[args.path] ?? '', get_all_content: () => MOCK_CONTENT, @@ -250,7 +250,6 @@ export const mockHandlers: Record any> = { if (!q) return { results: [], elapsed_ms: 0, query: q, mode: args.mode } const matches = MOCK_ENTRIES .filter(e => { - if (e.trashed) return false const content = MOCK_CONTENT[e.path] ?? '' return e.title.toLowerCase().includes(q) || content.toLowerCase().includes(q) }) diff --git a/src/types.ts b/src/types.ts index 1f2b3145..ecf72ac8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,8 +10,6 @@ export interface VaultEntry { // Note: owner and cadence are now stored in the generic `properties` map, // accessed via entry.properties?.Owner and entry.properties?.Cadence archived: boolean - trashed: boolean - trashedAt: number | null modifiedAt: number | null createdAt: number | null fileSize: number @@ -180,7 +178,7 @@ export interface PulseCommit { deleted: number } -export type SidebarFilter = 'all' | 'archived' | 'trash' | 'changes' | 'pulse' | 'inbox' | 'favorites' +export type SidebarFilter = 'all' | 'archived' | 'changes' | 'pulse' | 'inbox' | 'favorites' export type InboxPeriod = 'week' | 'month' | 'quarter' | 'all' diff --git a/src/utils/ai-context.test.ts b/src/utils/ai-context.test.ts index 704ad0c7..12a66bdf 100644 --- a/src/utils/ai-context.test.ts +++ b/src/utils/ai-context.test.ts @@ -12,8 +12,6 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, diff --git a/src/utils/noteListHelpers.test.ts b/src/utils/noteListHelpers.test.ts index bb6b18b0..b9a765dd 100644 --- a/src/utils/noteListHelpers.test.ts +++ b/src/utils/noteListHelpers.test.ts @@ -7,7 +7,6 @@ function makeEntry(overrides: Partial = {}): VaultEntry { path: '/vault/note/test.md', filename: 'test.md', title: 'Test', isA: 'Note', aliases: [], belongsTo: [], relatedTo: [], status: null, organized: false, archived: false, - trashed: false, trashedAt: null, modifiedAt: null, createdAt: null, fileSize: 0, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, template: null, sort: null, outgoingLinks: [], @@ -577,11 +576,6 @@ describe('isInboxEntry', () => { expect(isInboxEntry(note)).toBe(false) }) - it('returns false for a trashed note', () => { - const note = makeEntry({ trashed: true }) - expect(isInboxEntry(note)).toBe(false) - }) - it('returns false for an archived note', () => { const note = makeEntry({ archived: true }) expect(isInboxEntry(note)).toBe(false) @@ -686,7 +680,7 @@ describe('filterEntries — folder selection', () => { expect(result).toEqual([]) }) - it('excludes archived and trashed entries by default', () => { + it('excludes archived entries by default', () => { const withArchived = [ ...entries, makeEntry({ path: '/vault/projects/laputa/archived.md', title: 'Archived', archived: true }), diff --git a/src/utils/noteListHelpers.ts b/src/utils/noteListHelpers.ts index b293021b..6f09a319 100644 --- a/src/utils/noteListHelpers.ts +++ b/src/utils/noteListHelpers.ts @@ -2,7 +2,7 @@ import type { VaultEntry, SidebarSelection, InboxPeriod, ViewFile } from '../typ import { evaluateView } from './viewFilters' import { wikilinkTarget, resolveEntry } from './wikilink' -export type NoteListFilter = 'open' | 'archived' | 'trashed' +export type NoteListFilter = 'open' | 'archived' export interface RelationshipGroup { label: string @@ -306,12 +306,11 @@ export function buildRelationshipGroups( return b.groups } -const isActive = (e: VaultEntry) => !e.archived && !e.trashed +const isActive = (e: VaultEntry) => !e.archived const isMarkdown = (e: VaultEntry) => e.fileKind === 'markdown' || !e.fileKind function applySubFilter(entries: VaultEntry[], subFilter: NoteListFilter): VaultEntry[] { - if (subFilter === 'archived') return entries.filter((e) => e.archived && !e.trashed) - if (subFilter === 'trashed') return entries.filter((e) => e.trashed) + if (subFilter === 'archived') return entries.filter((e) => e.archived) return entries.filter(isActive) } @@ -344,9 +343,8 @@ function filterByKind(entries: VaultEntry[], selection: SidebarSelection, subFil function filterByFilterType(entries: VaultEntry[], filter: string): VaultEntry[] { if (filter === 'all') return entries.filter(isActive) - if (filter === 'archived') return entries.filter((e) => e.archived && !e.trashed) - if (filter === 'trash') return entries.filter((e) => e.trashed) - if (filter === 'favorites') return entries.filter((e) => e.favorite && !e.archived && !e.trashed) + if (filter === 'archived') return entries.filter((e) => e.archived) + if (filter === 'favorites') return entries.filter((e) => e.favorite && !e.archived) if (filter === 'pulse') return [] return [] } @@ -357,34 +355,32 @@ export function filterEntries(entries: VaultEntry[], selection: SidebarSelection /** Count notes per sub-filter for a given type. */ export function countByFilter(entries: VaultEntry[], type: string): Record { - let open = 0, archived = 0, trashed = 0 + let open = 0, archived = 0 for (const e of entries) { if (!isMarkdown(e) || e.isA !== type) continue - if (e.trashed) trashed++ - else if (e.archived) archived++ + if (e.archived) archived++ else open++ } - return { open, archived, trashed } + return { open, archived } } /** Count notes per sub-filter across all entries (no type filter). */ export function countAllByFilter(entries: VaultEntry[]): Record { - let open = 0, archived = 0, trashed = 0 + let open = 0, archived = 0 for (const e of entries) { if (!isMarkdown(e)) continue - if (e.trashed) trashed++ - else if (e.archived) archived++ + if (e.archived) archived++ else open++ } - return { open, archived, trashed } + return { open, archived } } // --- Inbox --- -/** Check if entry belongs in the Inbox (markdown only, not organized, not trashed/archived, not a Type). */ +/** Check if entry belongs in the Inbox (markdown only, not organized, not archived, not a Type). */ export function isInboxEntry(entry: VaultEntry): boolean { if (!isMarkdown(entry)) return false - if (entry.trashed || entry.archived) return false + if (entry.archived) return false if (entry.isA === 'Type') return false return !entry.organized } diff --git a/src/utils/sidebarSections.ts b/src/utils/sidebarSections.ts index 0cd9d2cd..24d8986f 100644 --- a/src/utils/sidebarSections.ts +++ b/src/utils/sidebarSections.ts @@ -27,9 +27,9 @@ const BUILT_IN_SECTION_GROUPS: SectionGroup[] = [ const BUILT_IN_TYPE_MAP = new Map(BUILT_IN_SECTION_GROUPS.map((sg) => [sg.type, sg])) const isMarkdown = (e: VaultEntry) => e.fileKind === 'markdown' || !e.fileKind -const isActive = (e: VaultEntry) => !e.trashed && !e.archived +const isActive = (e: VaultEntry) => !e.archived -/** Collect unique isA values from active (non-trashed, non-archived) markdown entries. Untyped entries count as 'Note'. */ +/** Collect unique isA values from active (non-archived) markdown entries. Untyped entries count as 'Note'. */ export function collectActiveTypes(entries: VaultEntry[]): Set { const types = new Set() for (const e of entries) { diff --git a/src/utils/suggestionEnrichment.test.ts b/src/utils/suggestionEnrichment.test.ts index b2914101..460ce116 100644 --- a/src/utils/suggestionEnrichment.test.ts +++ b/src/utils/suggestionEnrichment.test.ts @@ -11,7 +11,7 @@ function makeEntry(overrides: Partial = {}): VaultEntry { return { path: '/test.md', filename: 'test.md', title: 'Test', isA: null, aliases: [], belongsTo: [], relatedTo: [], status: null, - archived: false, trashed: false, trashedAt: null, modifiedAt: null, createdAt: null, + archived: false, modifiedAt: null, createdAt: null, fileSize: 0, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, template: null, sort: null, outgoingLinks: [], ...overrides, diff --git a/src/utils/typeColors.test.ts b/src/utils/typeColors.test.ts index 0af6f210..ae05f680 100644 --- a/src/utils/typeColors.test.ts +++ b/src/utils/typeColors.test.ts @@ -59,7 +59,7 @@ describe('getTypeLightColor', () => { const baseEntry: VaultEntry = { path: '', filename: '', title: '', isA: null, aliases: [], belongsTo: [], relatedTo: [], - status: null, archived: false, trashed: false, trashedAt: null, + status: null, archived: false, modifiedAt: null, createdAt: null, fileSize: 0, snippet: '', relationships: {}, wordCount: 0, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, diff --git a/src/utils/viewFilters.test.ts b/src/utils/viewFilters.test.ts index 114ab8cc..6fba9fb5 100644 --- a/src/utils/viewFilters.test.ts +++ b/src/utils/viewFilters.test.ts @@ -8,7 +8,7 @@ function makeEntry(overrides: Partial): VaultEntry { return { path: '/vault/test.md', filename: 'test.md', title: 'Test', isA: null, aliases: [], belongsTo: [], relatedTo: [], status: null, - archived: false, trashed: false, trashedAt: null, + archived: false, modifiedAt: NOW, createdAt: NOW, fileSize: 100, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, view: null, @@ -95,7 +95,7 @@ describe('evaluateView', () => { expect(result.map((e) => e.title)).toEqual(['Has']) }) - it('excludes archived and trashed entries', () => { + it('excludes archived entries', () => { const view: ViewDefinition = { name: 'All', icon: null, color: null, sort: null, filters: { all: [{ field: 'type', op: 'equals', value: 'Note' }] }, @@ -103,7 +103,6 @@ describe('evaluateView', () => { const entries = [ makeEntry({ isA: 'Note', title: 'Active' }), makeEntry({ isA: 'Note', title: 'Archived', archived: true }), - makeEntry({ isA: 'Note', title: 'Trashed', trashed: true }), ] const result = evaluateView(view, entries) expect(result.map((e) => e.title)).toEqual(['Active']) diff --git a/src/utils/viewFilters.ts b/src/utils/viewFilters.ts index c94e100c..f84a8650 100644 --- a/src/utils/viewFilters.ts +++ b/src/utils/viewFilters.ts @@ -2,7 +2,7 @@ import type { VaultEntry, ViewDefinition, FilterGroup, FilterNode, FilterConditi /** Evaluate a view's filters against a list of entries, returning only matches. */ export function evaluateView(definition: ViewDefinition, entries: VaultEntry[]): VaultEntry[] { - return entries.filter((e) => !e.trashed && !e.archived && evaluateGroup(definition.filters, e)) + return entries.filter((e) => !e.archived && evaluateGroup(definition.filters, e)) } function evaluateGroup(group: FilterGroup, entry: VaultEntry): boolean { @@ -27,7 +27,6 @@ function resolveField(entry: VaultEntry, field: string): { scalar?: string | num if (lower === 'title') return { scalar: entry.title } if (lower === 'filename') return { scalar: entry.filename } if (lower === 'archived') return { scalar: entry.archived } - if (lower === 'trashed') return { scalar: entry.trashed } if (lower === 'favorite') return { scalar: entry.favorite } if (lower === 'body') return { scalar: entry.snippet } diff --git a/src/utils/wikilink.test.ts b/src/utils/wikilink.test.ts index 1faf3bb8..f170733a 100644 --- a/src/utils/wikilink.test.ts +++ b/src/utils/wikilink.test.ts @@ -5,7 +5,7 @@ import { wikilinkTarget, wikilinkDisplay, resolveEntry, relativePathStem } from function makeEntry(overrides: Partial): VaultEntry { return { path: '/vault/note.md', filename: 'note.md', title: 'Note', isA: null, - aliases: [], belongsTo: [], relatedTo: [], status: null, archived: false, trashed: false, trashedAt: null, + aliases: [], belongsTo: [], relatedTo: [], status: null, archived: false, modifiedAt: null, createdAt: null, fileSize: 100, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, template: null, sort: null, outgoingLinks: [], sidebarLabel: null, view: null, visible: null, diff --git a/src/utils/wikilinkColors.test.ts b/src/utils/wikilinkColors.test.ts index 7e93b31f..c2c71c3f 100644 --- a/src/utils/wikilinkColors.test.ts +++ b/src/utils/wikilinkColors.test.ts @@ -13,8 +13,6 @@ function makeEntry(overrides: Partial): VaultEntry { relatedTo: [], status: null, archived: false, - trashed: false, - trashedAt: null, modifiedAt: null, createdAt: null, fileSize: 100, diff --git a/tests/smoke/exclude-trashed-from-search.spec.ts b/tests/smoke/exclude-trashed-from-search.spec.ts deleted file mode 100644 index c2fc403e..00000000 --- a/tests/smoke/exclude-trashed-from-search.spec.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { test, expect } from '@playwright/test' -import { sendShortcut } from './helpers' - -const QUICK_OPEN_INPUT = 'input[placeholder="Search notes..."]' -const SEARCH_INPUT = 'input[placeholder="Search in all notes..."]' - -/** Known trashed note titles from mock data */ -const TRASHED_TITLES = ['Old Draft Notes', 'Deprecated API Notes', 'Failed SEO Experiment'] -/** Query specific enough to find a known active note */ -const ACTIVE_QUERY = 'Laputa App' - -async function openQuickOpen(page: import('@playwright/test').Page) { - await page.locator('body').click() - await sendShortcut(page, 'p', ['Control']) - await expect(page.locator(QUICK_OPEN_INPUT)).toBeVisible() -} - -/** Get result titles scoped to a specific container (avoids matching sidebar items). */ -function getResultTitles(container: import('@playwright/test').Locator) { - return container.locator('span.truncate').allTextContents() -} - -/** The Quick Open dialog overlay */ -function quickOpenPanel(page: import('@playwright/test').Page) { - return page.locator('.fixed.inset-0').filter({ has: page.locator(QUICK_OPEN_INPUT) }) -} - -/** The full-text search dialog overlay */ -function searchPanel(page: import('@playwright/test').Page) { - return page.locator('.fixed.inset-0').filter({ has: page.locator(SEARCH_INPUT) }) -} - -test.describe('Exclude trashed notes from search and autocomplete', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/') - await page.waitForLoadState('networkidle') - }) - - test('trashed notes do not appear in Quick Open search', async ({ page }) => { - await openQuickOpen(page) - const panel = quickOpenPanel(page) - for (const title of TRASHED_TITLES) { - // Use a unique enough substring to trigger results - const query = title.split(' ')[0] - await page.locator(QUICK_OPEN_INPUT).fill(query) - await page.waitForTimeout(400) - const titles = await getResultTitles(panel) - expect(titles, `"${title}" should not appear for query "${query}"`).not.toContain(title) - } - }) - - test('active notes still appear in Quick Open search', async ({ page }) => { - await openQuickOpen(page) - const panel = quickOpenPanel(page) - await page.locator(QUICK_OPEN_INPUT).fill(ACTIVE_QUERY) - await page.waitForTimeout(400) - const titles = await getResultTitles(panel) - expect(titles.some(t => t.includes('Laputa App'))).toBe(true) - }) - - test('trashed notes do not appear in full-text search', async ({ page }) => { - // Full-text search panel opened via Ctrl/Cmd+Shift+F - const searchVisible = async () => { - await page.locator('body').click() - await sendShortcut(page, 'f', ['Control', 'Shift']) - try { - await expect(page.locator(SEARCH_INPUT)).toBeVisible({ timeout: 2000 }) - return true - } catch { return false } - } - if (!await searchVisible()) { - // Retry with Meta (macOS Cmd key) in case Playwright routes it differently - await sendShortcut(page, 'f', ['Meta', 'Shift']) - try { - await expect(page.locator(SEARCH_INPUT)).toBeVisible({ timeout: 2000 }) - } catch { - test.skip() - return - } - } - await page.locator(SEARCH_INPUT).fill('Old Draft') - await page.waitForTimeout(600) - const titles = await getResultTitles(searchPanel(page)) - expect(titles).not.toContain('Old Draft Notes') - }) - - test('wikilink autocomplete does not suggest trashed notes', async ({ page }) => { - // Open any note first (click the first note in the sidebar) - const firstNote = page.locator('[data-testid="note-item"]').first() - if (await firstNote.isVisible()) { - await firstNote.click() - await page.waitForTimeout(300) - } - - // Focus the editor and type [[ to trigger autocomplete - const editor = page.locator('.bn-editor').first() - if (await editor.isVisible()) { - await editor.click() - await page.keyboard.type('[[Old Draft') - await page.waitForTimeout(500) - - // Check autocomplete suggestions - should not contain the trashed note - const suggestions = page.locator('.bn-suggestion-menu [class*="item"], .bn-suggestion-menu button, [data-testid="wikilink-suggestion"]') - const count = await suggestions.count() - for (let i = 0; i < count; i++) { - const text = await suggestions.nth(i).textContent() - expect(text).not.toContain('Old Draft Notes') - } - } - }) -}) diff --git a/tests/smoke/fix-archived-trashed-detection-v2.spec.ts b/tests/smoke/fix-archived-trashed-detection-v2.spec.ts index 3f0a0176..6d2a083b 100644 --- a/tests/smoke/fix-archived-trashed-detection-v2.spec.ts +++ b/tests/smoke/fix-archived-trashed-detection-v2.spec.ts @@ -20,7 +20,7 @@ function getResultTitles(container: import('@playwright/test').Locator) { return container.locator('span.truncate').allTextContents() } -test.describe('Archived/Trashed Yes/No detection', () => { +test.describe('Archived Yes/No detection', () => { test.beforeEach(async ({ page }) => { await page.goto('/') await page.waitForLoadState('networkidle') diff --git a/tests/smoke/trash-archive-ui-consistency.spec.ts b/tests/smoke/trash-archive-ui-consistency.spec.ts deleted file mode 100644 index 3b00a7dd..00000000 --- a/tests/smoke/trash-archive-ui-consistency.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { test, expect } from '@playwright/test' -import { openCommandPalette, executeCommand } from './helpers' - -const NOTE_ITEM = '[data-testid="note-list-container"] .cursor-pointer' - -test.describe('Trash/archive state consistency across UI', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/') - await page.waitForLoadState('networkidle') - }) - - test('trashing a note shows TrashedNoteBanner and updates Properties', async ({ page }) => { - // Select first note in the list - const firstNote = page.locator(NOTE_ITEM).first() - await firstNote.click() - await page.waitForTimeout(500) - - // Verify no trash banner initially - await expect(page.locator('[data-testid="trashed-note-banner"]')).not.toBeVisible() - - // Trash via command palette - await openCommandPalette(page) - await executeCommand(page, 'Trash Note') - await page.waitForTimeout(500) - - // Banner should appear - await expect(page.locator('[data-testid="trashed-note-banner"]')).toBeVisible({ timeout: 3000 }) - - // Toast should say "Note moved to trash", NOT "Property updated" - const toast = page.locator('.fixed.bottom-8') - await expect(toast).toContainText(/moved to trash/i, { timeout: 3000 }) - }) - - test('archiving a note shows ArchivedNoteBanner', async ({ page }) => { - // Select first note in the list - const firstNote = page.locator(NOTE_ITEM).first() - await firstNote.click() - await page.waitForTimeout(500) - - // Verify no archive banner initially - await expect(page.locator('[data-testid="archived-note-banner"]')).not.toBeVisible() - - // Archive via command palette - await openCommandPalette(page) - await executeCommand(page, 'Archive Note') - await page.waitForTimeout(500) - - // Banner should appear - await expect(page.locator('[data-testid="archived-note-banner"]')).toBeVisible({ timeout: 3000 }) - }) - - test('raw editor shows updated frontmatter after trash', async ({ page }) => { - // Select first note - const firstNote = page.locator(NOTE_ITEM).first() - await firstNote.click() - await page.waitForTimeout(500) - - // Switch to raw editor mode - await openCommandPalette(page) - await executeCommand(page, 'Toggle Raw Editor') - await page.waitForTimeout(300) - - const rawEditor = page.locator('[data-testid="raw-editor-codemirror"]') - await expect(rawEditor).toBeVisible({ timeout: 3000 }) - - // Trash the note - await openCommandPalette(page) - await executeCommand(page, 'Trash Note') - await page.waitForTimeout(500) - - // Raw editor should show Trashed: true in frontmatter - const editorText = await rawEditor.textContent() - expect(editorText).toContain('Trashed') - }) - - test('Changes list updates after trashing a note', async ({ page }) => { - const sidebar = page.locator('.app__sidebar') - const secondaryArea = sidebar.locator('[data-testid="sidebar-secondary"]') - const changesRow = secondaryArea.locator('div', { hasText: /^Changes/ }).first() - await changesRow.waitFor({ timeout: 5000 }) - - const badge = changesRow.locator('span').last() - const initialCount = Number(await badge.textContent()) - - // Select and trash a note - const firstNote = page.locator(NOTE_ITEM).first() - await firstNote.click() - await page.waitForTimeout(300) - - await openCommandPalette(page) - await executeCommand(page, 'Trash Note') - - // Badge count should increase - await expect(async () => { - const text = await badge.textContent() - expect(Number(text)).toBeGreaterThan(initialCount) - }).toPass({ timeout: 5000 }) - }) -}) diff --git a/tests/smoke/trash-management.spec.ts b/tests/smoke/trash-management.spec.ts deleted file mode 100644 index 4cc76a3e..00000000 --- a/tests/smoke/trash-management.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { test, expect } from '@playwright/test' -import { openCommandPalette, findCommand, executeCommand } from './helpers' - -const NOTE_ITEM = '[data-testid="note-list-container"] .cursor-pointer' - -async function navigateToTrash(page: import('@playwright/test').Page) { - await openCommandPalette(page) - await executeCommand(page, 'Go to Trash') - await page.waitForTimeout(500) -} - -async function selectAllNotes(page: import('@playwright/test').Page) { - // Focus the note list container first, then Cmd+A - await page.locator('[data-testid="note-list-container"]').focus() - await page.keyboard.press('Meta+a') - await page.waitForTimeout(300) -} - -test.describe('Trash management', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/') - await page.waitForLoadState('networkidle') - }) - - test('trash view shows trashed notes with correct header', async ({ page }) => { - await navigateToTrash(page) - await expect(page.locator('text=Trash').first()).toBeVisible() - const noteItems = page.locator(NOTE_ITEM) - await expect(noteItems.first()).toBeVisible({ timeout: 5000 }) - const count = await noteItems.count() - expect(count).toBeGreaterThanOrEqual(1) - }) - - test('Empty Trash button is visible in trash view header', async ({ page }) => { - await navigateToTrash(page) - const emptyTrashBtn = page.locator('[data-testid="empty-trash-btn"]') - await expect(emptyTrashBtn).toBeVisible({ timeout: 3000 }) - }) - - test('Empty Trash button is NOT visible in normal "All Notes" view', async ({ page }) => { - const emptyTrashBtn = page.locator('[data-testid="empty-trash-btn"]') - await expect(emptyTrashBtn).not.toBeVisible() - }) - - test('Empty Trash command is available in command palette', async ({ page }) => { - await openCommandPalette(page) - const found = await findCommand(page, 'Empty Trash') - expect(found).toBe(true) - }) - - test('Empty Trash button shows confirmation dialog', async ({ page }) => { - await navigateToTrash(page) - const emptyTrashBtn = page.locator('[data-testid="empty-trash-btn"]') - await expect(emptyTrashBtn).toBeVisible({ timeout: 3000 }) - await emptyTrashBtn.click() - const dialog = page.locator('[data-testid="confirm-delete-dialog"]') - await expect(dialog).toBeVisible({ timeout: 3000 }) - await expect(dialog).toContainText('Empty Trash') - }) - - test('confirmation dialog can be cancelled', async ({ page }) => { - await navigateToTrash(page) - const emptyTrashBtn = page.locator('[data-testid="empty-trash-btn"]') - await expect(emptyTrashBtn).toBeVisible({ timeout: 3000 }) - await emptyTrashBtn.click() - const dialog = page.locator('[data-testid="confirm-delete-dialog"]') - await expect(dialog).toBeVisible({ timeout: 3000 }) - await page.locator('button').filter({ hasText: 'Cancel' }).click() - await expect(dialog).not.toBeVisible() - // Notes should still be in the list - const noteItems = page.locator(NOTE_ITEM) - const count = await noteItems.count() - expect(count).toBeGreaterThanOrEqual(1) - }) - - test('bulk selection in trash view shows Restore and Delete permanently buttons', async ({ page }) => { - await navigateToTrash(page) - await expect(page.locator(NOTE_ITEM).first()).toBeVisible({ timeout: 5000 }) - await selectAllNotes(page) - const bulkBar = page.locator('[data-testid="bulk-action-bar"]') - await expect(bulkBar).toBeVisible({ timeout: 3000 }) - await expect(page.locator('[data-testid="bulk-restore-btn"]')).toBeVisible() - await expect(page.locator('[data-testid="bulk-archive-btn"]')).toBeVisible() - await expect(page.locator('[data-testid="bulk-delete-btn"]')).toBeVisible() - // Trash button should NOT be visible in trash view (notes are already trashed) - await expect(page.locator('[data-testid="bulk-trash-btn"]')).not.toBeVisible() - }) - - test('bulk Delete permanently shows confirmation dialog', async ({ page }) => { - await navigateToTrash(page) - await expect(page.locator(NOTE_ITEM).first()).toBeVisible({ timeout: 5000 }) - await selectAllNotes(page) - const deleteBtn = page.locator('[data-testid="bulk-delete-btn"]') - await expect(deleteBtn).toBeVisible({ timeout: 3000 }) - await deleteBtn.click() - const dialog = page.locator('[data-testid="confirm-delete-dialog"]') - await expect(dialog).toBeVisible({ timeout: 3000 }) - await expect(dialog).toContainText('permanently') - }) - - test('trashed note banner shows Restore and Delete permanently in editor', async ({ page }) => { - await navigateToTrash(page) - const firstNote = page.locator(NOTE_ITEM).first() - await expect(firstNote).toBeVisible({ timeout: 5000 }) - await firstNote.click() - await page.waitForTimeout(500) - const banner = page.locator('[data-testid="trashed-note-banner"]') - await expect(banner).toBeVisible({ timeout: 3000 }) - await expect(page.locator('[data-testid="trashed-banner-restore"]')).toBeVisible() - await expect(page.locator('[data-testid="trashed-banner-delete"]')).toBeVisible() - }) - - test('bulk selection in normal view shows Archive and Trash (not Restore/Delete)', async ({ page }) => { - // Ensure note list is visible - await expect(page.locator(NOTE_ITEM).first()).toBeVisible({ timeout: 5000 }) - await selectAllNotes(page) - const bulkBar = page.locator('[data-testid="bulk-action-bar"]') - await expect(bulkBar).toBeVisible({ timeout: 3000 }) - await expect(page.locator('[data-testid="bulk-archive-btn"]')).toBeVisible() - await expect(page.locator('[data-testid="bulk-trash-btn"]')).toBeVisible() - await expect(page.locator('[data-testid="bulk-restore-btn"]')).not.toBeVisible() - await expect(page.locator('[data-testid="bulk-delete-btn"]')).not.toBeVisible() - }) - - test('Empty Trash via command palette shows confirmation dialog', async ({ page }) => { - await openCommandPalette(page) - await executeCommand(page, 'Empty Trash') - await page.waitForTimeout(300) - const dialog = page.locator('[data-testid="confirm-delete-dialog"]') - await expect(dialog).toBeVisible({ timeout: 3000 }) - }) -}) diff --git a/tests/smoke/trashed-archived-not-saved.spec.ts b/tests/smoke/trashed-archived-not-saved.spec.ts deleted file mode 100644 index 3426f1a9..00000000 --- a/tests/smoke/trashed-archived-not-saved.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { test, expect } from '@playwright/test' -import { openCommandPalette, executeCommand } from './helpers' - -test.describe('Trash/archive notes appear in Changes', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/') - await page.waitForLoadState('networkidle') - }) - - test('trashing a note increments the Changes badge', async ({ page }) => { - const sidebar = page.locator('.app__sidebar') - - // Wait for Changes nav item in the secondary bottom area (mock starts with 3 modified files) - const secondaryArea = sidebar.locator('[data-testid="sidebar-secondary"]') - const changesRow = secondaryArea.locator('div', { hasText: /^Changes/ }).first() - await changesRow.waitFor({ timeout: 5000 }) - - // Read the initial badge count from the Changes row - const badge = changesRow.locator('span').last() - const initialCount = Number(await badge.textContent()) - expect(initialCount).toBeGreaterThan(0) - - // Click the first note in the list to select it - const noteListContainer = page.locator('[data-testid="note-list-container"]') - await noteListContainer.waitFor({ timeout: 5000 }) - // Click on the first visible note item (border-b items inside the list) - const firstNote = noteListContainer.locator('.cursor-pointer').first() - await firstNote.click() - - // Trash the active note via command palette - await openCommandPalette(page) - await executeCommand(page, 'Trash Note') - - // Wait for the badge count to increase - await expect(async () => { - const text = await badge.textContent() - expect(Number(text)).toBeGreaterThan(initialCount) - }).toPass({ timeout: 5000 }) - }) -})