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:
parent
ba4775b0bb
commit
d630c9e550
|
|
@ -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,16 +388,16 @@ 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.dialog.set(Some(Dialog::ConfirmDelete { path, title }));
|
|
||||||
} else {
|
|
||||||
self.delete_node(path);
|
self.delete_node(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Action::LibraryCaptureNode => {
|
Action::LibraryCaptureNode => {
|
||||||
if let Some((path, title)) =
|
if let Some((path, title)) =
|
||||||
self.library.with_untracked(LibraryPane::selected_queueable)
|
self.library.with_untracked(LibraryPane::selected_queueable)
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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 [
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue