cbd-web: drop the unreachable capture-delete confirmation

delete_needs_confirmation only ever returned true for /captures paths,
and /captures stopped existing when saved queues, bookmarks, and captures
folded into the single /crabidy provider. So the y/N dialog could not
open, the web client already deleted immediately, and its comment
claiming to mirror the TUI described an arrangement neither client had.

Removed rather than re-pointed at /crabidy: a delete there drops the
metadata toml only, never the shared store audio, which survives and
resurfaces under /orphans — so there is little to guard. Both clients now
behave the same, which is what the docs describe.

Gone with it: the Dialog::ConfirmDelete variant, the ConfirmDialog
component and its keyboard handler, the test, and the two CSS rules only
that dialog wore (.danger-dialog and the solid .danger button;
.ghost.danger stays, three row actions still use it).

Verified: clippy clean for wasm32 (where `mod app` actually compiles) and
native, 12 cbd-web tests pass, and the trunk bundle builds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-25 12:58:15 +02:00
parent ba4775b0bb
commit d630c9e550
3 changed files with 8 additions and 80 deletions

View File

@ -15,8 +15,8 @@ use leptos::task::spawn_local;
use crate::keymap::{self, Action}; use crate::keymap::{self, Action};
use crate::rpc::Rpc; use crate::rpc::Rpc;
use crate::state::{ use crate::state::{
delete_needs_confirmation, format_seconds, is_cacheable, track_label, CaptureBoard, Dialog, format_seconds, is_cacheable, track_label, CaptureBoard, Dialog, Focus, LibraryPane,
Focus, LibraryPane, NamePurpose, QueueCursor, UiItemKind, NamePurpose, QueueCursor, UiItemKind,
}; };
const VOLUME_STEP: f32 = 0.1; const VOLUME_STEP: f32 = 0.1;
@ -388,14 +388,14 @@ impl Store {
} }
} }
Action::LibraryDeleteNode => { Action::LibraryDeleteNode => {
if let Some((path, title)) = // Deleting under `/crabidy` drops the metadata toml only; the
// shared store audio survives and resurfaces under
// `/orphans`, so there is nothing to confirm — same as the
// TUI.
if let Some((path, _title)) =
self.library.with_untracked(LibraryPane::selected_deletable) self.library.with_untracked(LibraryPane::selected_deletable)
{ {
if delete_needs_confirmation(&path) { self.delete_node(path);
self.dialog.set(Some(Dialog::ConfirmDelete { path, title }));
} else {
self.delete_node(path);
}
} }
} }
Action::LibraryCaptureNode => { Action::LibraryCaptureNode => {
@ -1122,9 +1122,6 @@ fn Dialogs(store: Store) -> impl IntoView {
Dialog::Name { purpose, buffer } => { Dialog::Name { purpose, buffer } => {
view! { <NameDialog store=store purpose=purpose buffer=buffer /> }.into_any() view! { <NameDialog store=store purpose=purpose buffer=buffer /> }.into_any()
} }
Dialog::ConfirmDelete { path, title } => {
view! { <ConfirmDialog store=store path=path title=title /> }.into_any()
}
Dialog::Login => view! { <LoginDialog store=store /> }.into_any(), Dialog::Login => view! { <LoginDialog store=store /> }.into_any(),
Dialog::Help => view! { <HelpOverlay store=store /> }.into_any(), Dialog::Help => view! { <HelpOverlay store=store /> }.into_any(),
}) })
@ -1166,48 +1163,6 @@ fn NameDialog(store: Store, purpose: NamePurpose, buffer: String) -> impl IntoVi
} }
} }
#[component]
fn ConfirmDialog(store: Store, path: String, title: String) -> impl IntoView {
let confirm_path = path.clone();
let confirm = move |_| {
store.dialog.set(None);
store.delete_node(confirm_path.clone());
};
// y/N without leaving the keyboard, like the TUI: the overlay grabs
// the keys while it is open.
let key_path = path;
let handle = window_event_listener(leptos::ev::keydown, move |ev| {
if !matches!(
store.dialog.get_untracked(),
Some(Dialog::ConfirmDelete { .. })
) {
return;
}
ev.prevent_default();
store.dialog.set(None);
if matches!(ev.key().as_str(), "y" | "Y") {
store.delete_node(key_path.clone());
}
});
on_cleanup(move || handle.remove());
view! {
<div class="overlay" on:click=move |_| store.dialog.set(None)>
<div class="dialog danger-dialog" on:click=|ev| ev.stop_propagation()>
<p>
"Delete "<strong>{title}</strong>
" from disk (downloaded audio included)?"
</p>
<div class="dialog-actions">
<button class="ghost" on:click=move |_| store.dialog.set(None)>
"cancel (N)"
</button>
<button class="danger" on:click=confirm>"delete (y)"</button>
</div>
</div>
</div>
}
}
#[component] #[component]
fn LoginDialog(store: Store) -> impl IntoView { fn LoginDialog(store: Store) -> impl IntoView {
let user = RwSignal::new(load_pref("user").unwrap_or_default()); let user = RwSignal::new(load_pref("user").unwrap_or_default());

View File

@ -52,8 +52,6 @@ pub enum Dialog {
purpose: NamePurpose, purpose: NamePurpose,
buffer: String, buffer: String,
}, },
/// The capture-delete confirmation (architecture/capture-deletion.md).
ConfirmDelete { path: String, title: String },
/// Credentials form. Shown on `UNAUTHENTICATED` responses (creds /// Credentials form. Shown on `UNAUTHENTICATED` responses (creds
/// required), and proactively — but dismissible — on first connect /// required), and proactively — but dismissible — on first connect
/// when the server reports auth is enabled and we hold none. /// when the server reports auth is enabled and we hold none.
@ -62,13 +60,6 @@ pub enum Dialog {
Help, Help,
} }
/// Whether deleting `path` needs the y/N confirmation — same rule as
/// the TUI: captures hold downloaded audio, everything else deletable
/// is cheap to recreate.
pub fn delete_needs_confirmation(path: &str) -> bool {
path == "/captures" || path.starts_with("/captures/")
}
/// Whether a library listing may be cached client-side — same rule as /// Whether a library listing may be cached client-side — same rule as
/// the TUI (`cbd-tui/src/rpc.rs`): server-side folder providers mutate /// the TUI (`cbd-tui/src/rpc.rs`): server-side folder providers mutate
/// behind the client's back and are cheap to re-list. `/crabidy` (saves and /// behind the client's back and are cheap to re-list. `/crabidy` (saves and
@ -467,15 +458,6 @@ mod tests {
); );
} }
#[test]
fn capture_deletes_need_confirmation_cheap_deletes_do_not() {
assert!(delete_needs_confirmation("/captures/mix"));
assert!(delete_needs_confirmation("/captures/mix/a.cbd-track.toml"));
assert!(!delete_needs_confirmation("/queues/roadtrip"));
assert!(!delete_needs_confirmation("/tidal/search/abba"));
assert!(!delete_needs_confirmation("/capturesque"));
}
#[test] #[test]
fn mutable_roots_are_never_cacheable() { fn mutable_roots_are_never_cacheable() {
for path in [ for path in [

View File

@ -76,11 +76,6 @@ button {
} }
} }
&.danger {
background: var(--danger);
color: var(--on-accent);
}
&.ghost.danger { &.ghost.danger {
background: transparent; background: transparent;
color: var(--danger); color: var(--danger);
@ -395,10 +390,6 @@ input {
justify-content: flex-end; justify-content: flex-end;
gap: 0.5rem; gap: 0.5rem;
} }
&.danger-dialog {
border-inline-start: 4px solid var(--danger);
}
} }
.help { .help {