diff --git a/crates/ely_app/src/shell/internal_pages/notes.rs b/crates/ely_app/src/shell/internal_pages/notes.rs index 083783b..51c9554 100644 --- a/crates/ely_app/src/shell/internal_pages/notes.rs +++ b/crates/ely_app/src/shell/internal_pages/notes.rs @@ -2,7 +2,7 @@ use std::time::{Duration, SystemTime}; use ely_browser_core::BrowserSnapshot; use ely_design_system::colors; -use ely_domain::NoteEntry; +use ely_domain::{NoteEntry, NoteId}; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, @@ -79,6 +79,7 @@ impl ElyShell { ) -> AnyElement { let url = note.source_url().clone(); let open_url = note.source_url().clone(); + let note_id = note.id().clone(); let space_name = note_space_name(snapshot, note); div() @@ -161,6 +162,7 @@ impl ElyShell { shell.open_url(open_url.clone(), window, cx); })), ) + .child(render_remove_note_action(index, note_id, cx)) .into_any_element() } } @@ -207,6 +209,23 @@ fn notes_count_label(count: usize) -> String { } } +fn render_remove_note_action( + index: usize, + note_id: NoteId, + cx: &mut Context, +) -> AnyElement { + Button::new(("remove-note", index)) + .danger() + .xsmall() + .icon(IconName::Delete) + .label("Remove") + .tooltip("Remove Note") + .on_click(cx.listener(move |shell, _, _, cx| { + shell.remove_note_entry(¬e_id, cx); + })) + .into_any_element() +} + fn markdown_preview(body: &str) -> String { let preview = body .lines() diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index e2b3b34..e4410a0 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -2,6 +2,7 @@ mod bookmarks; mod downloads; mod history; mod internal_pages; +mod notes; mod plugins; mod reading_list; mod render; diff --git a/crates/ely_app/src/shell/notes.rs b/crates/ely_app/src/shell/notes.rs new file mode 100644 index 0000000..8d98a2d --- /dev/null +++ b/crates/ely_app/src/shell/notes.rs @@ -0,0 +1,14 @@ +use ely_domain::NoteId; +use gpui::Context; + +use super::{ElyShell, ShellState}; + +impl ElyShell { + pub(super) fn remove_note_entry(&mut self, note_id: &NoteId, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && core.remove_note_entry(note_id).is_ok() + { + cx.notify(); + } + } +} diff --git a/crates/ely_browser_core/src/error.rs b/crates/ely_browser_core/src/error.rs index 6f5a4d0..dea5975 100644 --- a/crates/ely_browser_core/src/error.rs +++ b/crates/ely_browser_core/src/error.rs @@ -1,6 +1,6 @@ use ely_domain::{ - BookmarkId, DomainError, DownloadId, PluginId, ProfileId, ReadingListId, SpaceId, SplitId, - TabId, + BookmarkId, DomainError, DownloadId, NoteId, PluginId, ProfileId, ReadingListId, SpaceId, + SplitId, TabId, }; use thiserror::Error; @@ -30,6 +30,9 @@ pub enum CoreError { #[error("reading list entry not found: {id}")] ReadingListEntryNotFound { id: ReadingListId }, + #[error("note not found: {id}")] + NoteNotFound { id: NoteId }, + #[error("download target path is unavailable: {id}")] DownloadTargetPathUnavailable { id: DownloadId }, diff --git a/crates/ely_browser_core/src/state/notes.rs b/crates/ely_browser_core/src/state/notes.rs index 228d615..4c6716e 100644 --- a/crates/ely_browser_core/src/state/notes.rs +++ b/crates/ely_browser_core/src/state/notes.rs @@ -87,6 +87,19 @@ impl BrowserCore { .collect() } + pub fn remove_note_entry(&mut self, note_id: &NoteId) -> Result<(), CoreError> { + let index = self.note_entry_index(note_id)?; + self.notes.remove(index); + Ok(()) + } + + fn note_entry_index(&self, note_id: &NoteId) -> Result { + self.notes + .iter() + .position(|entry| entry.id() == note_id) + .ok_or_else(|| CoreError::NoteNotFound { id: note_id.clone() }) + } + fn note_index_for_target( &self, profile_id: &ely_domain::ProfileId, diff --git a/crates/ely_browser_core/tests/notes.rs b/crates/ely_browser_core/tests/notes.rs index 045e82a..f6185ae 100644 --- a/crates/ely_browser_core/tests/notes.rs +++ b/crates/ely_browser_core/tests/notes.rs @@ -1,7 +1,7 @@ use std::error::Error; use ely_browser_core::{BrowserCore, InitialBrowserConfig}; -use ely_domain::{CommandIntent, CommandScope, NoteTarget, ProfileKind, UrlText}; +use ely_domain::{CommandIntent, CommandScope, NoteId, NoteTarget, ProfileKind, UrlText}; #[test] fn url_note_records_active_page_context() -> Result<(), Box> { @@ -101,6 +101,32 @@ fn notes_scoped_search_opens_matching_note_url() -> Result<(), Box> { Ok(()) } +#[test] +fn remove_note_entry_removes_saved_note() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + core.open_tab(UrlText::parse("https://example.com/remove-note")?); + let note_id = core.save_active_url_note("remove me")?; + + core.remove_note_entry(¬e_id)?; + let snapshot = core.snapshot()?; + + assert!(snapshot.notes.is_empty()); + Ok(()) +} + +#[test] +fn missing_note_remove_returns_error() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let missing_id = NoteId::new(); + + let Err(error) = core.remove_note_entry(&missing_id) else { + return Err("expected missing note error".into()); + }; + + assert_eq!(error, ely_browser_core::CoreError::NoteNotFound { id: missing_id }); + Ok(()) +} + #[test] fn notes_stay_with_active_profile() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;