feat(readarr): add the delete author UI

This commit is contained in:
Dark-Alex-17 committed 2026-09-03 21:51:21 -06:00
1 parent 7fa091cb44
commit 2b07ad96f1
9 files changed
+454 -2

No files matched your search

@@ -0,0 +1,85 @@
use ratatui::Frame;
use ratatui::layout::Rect;
use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::readarr::readarr_data::{
ActiveReadarrBlock, DELETE_AUTHOR_BLOCKS,
};
use crate::ui::DrawUi;
use crate::ui::widgets::checkbox::Checkbox;
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::popup::{Popup, Size};
#[cfg(test)]
#[path = "delete_author_ui_tests.rs"]
mod delete_author_ui_tests;
pub(super) struct DeleteAuthorUi;
impl DrawUi for DeleteAuthorUi {
fn accepts(route: Route) -> bool {
let Route::Readarr(active_readarr_block, _) = route else {
return false;
};
DELETE_AUTHOR_BLOCKS.contains(&active_readarr_block)
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
if matches!(
app.get_current_route(),
Route::Readarr(ActiveReadarrBlock::DeleteAuthorPrompt, _)
) {
let selected_block = app.data.readarr_data.selected_block.get_active_block();
let highlights = delete_author_prompt_highlights(selected_block);
let prompt = format!(
"Do you really want to delete the author: \n{}?",
app
.data
.readarr_data
.authors
.current_selection()
.author_name
.text
);
let checkboxes = vec![
Checkbox::new("Delete Author Files")
.checked(app.data.readarr_data.delete_files)
.highlighted(highlights.delete_files),
Checkbox::new("Add List Exclusion")
.checked(app.data.readarr_data.add_import_list_exclusion)
.highlighted(highlights.add_import_list_exclusion),
];
let confirmation_prompt = ConfirmationPrompt::new()
.title("Delete Author")
.prompt(&prompt)
.checkboxes(checkboxes)
.yes_no_highlighted(highlights.confirm)
.yes_no_value(app.data.readarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
}
}
#[derive(PartialEq, Eq)]
#[cfg_attr(test, derive(Debug))]
struct DeleteAuthorPromptHighlights {
delete_files: bool,
add_import_list_exclusion: bool,
confirm: bool,
}
fn delete_author_prompt_highlights(
selected_block: ActiveReadarrBlock,
) -> DeleteAuthorPromptHighlights {
DeleteAuthorPromptHighlights {
delete_files: selected_block == ActiveReadarrBlock::DeleteAuthorToggleDeleteFile,
add_import_list_exclusion: selected_block
== ActiveReadarrBlock::DeleteAuthorToggleAddListExclusion,
confirm: selected_block == ActiveReadarrBlock::DeleteAuthorConfirmPrompt,
}
}
@@ -0,0 +1,155 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::models::servarr_data::readarr::readarr_data::{
ActiveReadarrBlock, DELETE_AUTHOR_BLOCKS, DELETE_AUTHOR_SELECTION_BLOCKS,
};
use crate::ui::DrawUi;
use crate::ui::readarr_ui::library::delete_author_ui::{
DeleteAuthorPromptHighlights, DeleteAuthorUi, delete_author_prompt_highlights,
};
#[test]
fn test_delete_author_ui_accepts() {
ActiveReadarrBlock::iter().for_each(|active_readarr_block| {
if DELETE_AUTHOR_BLOCKS.contains(&active_readarr_block) {
assert!(DeleteAuthorUi::accepts(active_readarr_block.into()));
} else {
assert!(!DeleteAuthorUi::accepts(active_readarr_block.into()));
}
});
}
#[rstest]
#[case(
ActiveReadarrBlock::DeleteAuthorToggleDeleteFile,
DeleteAuthorPromptHighlights {
delete_files: true,
add_import_list_exclusion: false,
confirm: false,
}
)]
#[case(
ActiveReadarrBlock::DeleteAuthorToggleAddListExclusion,
DeleteAuthorPromptHighlights {
delete_files: false,
add_import_list_exclusion: true,
confirm: false,
}
)]
#[case(
ActiveReadarrBlock::DeleteAuthorConfirmPrompt,
DeleteAuthorPromptHighlights {
delete_files: false,
add_import_list_exclusion: false,
confirm: true,
}
)]
#[case(
ActiveReadarrBlock::Authors,
DeleteAuthorPromptHighlights {
delete_files: false,
add_import_list_exclusion: false,
confirm: false,
}
)]
fn test_delete_author_prompt_highlights(
#[case] selected_block: ActiveReadarrBlock,
#[case] expected_highlights: DeleteAuthorPromptHighlights,
) {
let highlights = delete_author_prompt_highlights(selected_block);
assert_eq!(highlights, expected_highlights);
}
#[test]
fn test_delete_author_prompt_highlights_covers_every_selection_step() {
for step in DELETE_AUTHOR_SELECTION_BLOCKS {
let highlights = delete_author_prompt_highlights(step[0]);
assert_ne!(
highlights,
delete_author_prompt_highlights(ActiveReadarrBlock::Authors)
);
}
}
mod snapshot_tests {
use rstest::rstest;
use super::*;
use crate::app::App;
use crate::models::BlockSelectionState;
use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app};
fn seed_distinct_delete_preferences(app: &mut App<'_>) {
app.data.readarr_data.delete_files = true;
app.data.readarr_data.add_import_list_exclusion = false;
}
#[rstest]
#[case(0, ActiveReadarrBlock::DeleteAuthorToggleDeleteFile)]
#[case(1, ActiveReadarrBlock::DeleteAuthorToggleAddListExclusion)]
#[case(2, ActiveReadarrBlock::DeleteAuthorConfirmPrompt)]
fn test_delete_author_ui_renders_step(
#[case] step: usize,
#[case] selected_readarr_block: ActiveReadarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveReadarrBlock::DeleteAuthorPrompt.into());
seed_distinct_delete_preferences(&mut app);
let mut selected_block = BlockSelectionState::new(DELETE_AUTHOR_SELECTION_BLOCKS);
selected_block.set_index(0, step);
app.data.readarr_data.selected_block = selected_block;
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
DeleteAuthorUi::draw(f, app, f.area());
});
insta::assert_snapshot!(
format!("delete_author_step_{step}_{selected_readarr_block}"),
output
);
}
#[test]
fn test_delete_author_ui_renders_inverted_delete_preferences() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveReadarrBlock::DeleteAuthorPrompt.into());
app.data.readarr_data.delete_files = false;
app.data.readarr_data.add_import_list_exclusion = true;
app.data.readarr_data.selected_block =
BlockSelectionState::new(DELETE_AUTHOR_SELECTION_BLOCKS);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
DeleteAuthorUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[rstest]
#[case(ActiveReadarrBlock::DeleteAuthorConfirmPrompt)]
#[case(ActiveReadarrBlock::DeleteAuthorToggleDeleteFile)]
#[case(ActiveReadarrBlock::DeleteAuthorToggleAddListExclusion)]
fn test_delete_author_ui_renders_nothing_when_route_is_not_the_prompt(
#[case] active_readarr_block: ActiveReadarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_readarr_block.into());
seed_distinct_delete_preferences(&mut app);
app.data.readarr_data.selected_block =
BlockSelectionState::new(DELETE_AUTHOR_SELECTION_BLOCKS);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
DeleteAuthorUi::draw(f, app, f.area());
});
assert!(!output.contains("Delete Author"));
assert!(!output.contains("Add List Exclusion"));
}
}
}
+20 -2
View File
@@ -5,7 +5,7 @@ mod tests {
use crate::models::readarr_models::{Author, AuthorStatistics, AuthorStatus};
use crate::models::servarr_data::readarr::readarr_data::{
ADD_AUTHOR_BLOCKS, AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, BOOK_DETAILS_BLOCKS,
EDIT_AUTHOR_BLOCKS, EDITION_DETAILS_BLOCKS, LIBRARY_BLOCKS,
DELETE_AUTHOR_BLOCKS, EDIT_AUTHOR_BLOCKS, EDITION_DETAILS_BLOCKS, LIBRARY_BLOCKS,
};
use crate::ui::DrawUi;
use crate::ui::readarr_ui::library::{LibraryUi, decorate_author_row_with_style};
@@ -20,6 +20,7 @@ mod tests {
blocks.extend(BOOK_DETAILS_BLOCKS);
blocks.extend(EDITION_DETAILS_BLOCKS);
blocks.extend(ADD_AUTHOR_BLOCKS);
blocks.extend(DELETE_AUTHOR_BLOCKS);
blocks.extend(EDIT_AUTHOR_BLOCKS);
for active_readarr_block in ActiveReadarrBlock::iter() {
@@ -190,7 +191,7 @@ mod tests {
use crate::app::App;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::readarr::readarr_data::{
ActiveReadarrBlock, EDIT_AUTHOR_SELECTION_BLOCKS,
ActiveReadarrBlock, DELETE_AUTHOR_SELECTION_BLOCKS, EDIT_AUTHOR_SELECTION_BLOCKS,
};
use rstest::rstest;
@@ -297,5 +298,22 @@ mod tests {
insta::assert_snapshot!(output);
}
#[test]
fn test_library_ui_renders_delete_author_over_library() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveReadarrBlock::Authors.into());
app.data.readarr_data.delete_files = true;
app.data.readarr_data.add_import_list_exclusion = false;
app.data.readarr_data.selected_block =
BlockSelectionState::new(DELETE_AUTHOR_SELECTION_BLOCKS);
app.push_navigation_stack(ActiveReadarrBlock::DeleteAuthorPrompt.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
LibraryUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
}
}
+4
View File
@@ -1,5 +1,6 @@
use add_author_ui::AddAuthorUi;
use author_details_ui::AuthorDetailsUi;
use delete_author_ui::DeleteAuthorUi;
use edit_author_ui::EditAuthorUi;
use ratatui::{
Frame,
@@ -30,6 +31,7 @@ use crate::{
mod add_author_ui;
mod author_details_ui;
mod book_details_ui;
mod delete_author_ui;
mod edit_author_ui;
mod edition_details_ui;
@@ -43,6 +45,7 @@ impl DrawUi for LibraryUi {
fn accepts(route: Route) -> bool {
if let Route::Readarr(active_readarr_block, _) = route {
return AddAuthorUi::accepts(route)
|| DeleteAuthorUi::accepts(route)
|| EditAuthorUi::accepts(route)
|| AuthorDetailsUi::accepts(route)
|| LIBRARY_BLOCKS.contains(&active_readarr_block);
@@ -57,6 +60,7 @@ impl DrawUi for LibraryUi {
match route {
_ if AddAuthorUi::accepts(route) => AddAuthorUi::draw(f, app, area),
_ if DeleteAuthorUi::accepts(route) => DeleteAuthorUi::draw(f, app, area),
_ if AuthorDetailsUi::accepts(route) => AuthorDetailsUi::draw(f, app, area),
_ if EditAuthorUi::accepts(route) => EditAuthorUi::draw(f, app, area),
Route::Readarr(ActiveReadarrBlock::UpdateAllAuthorsPrompt, _) => {
@@ -0,0 +1,38 @@
---
source: src/ui/readarr_ui/library/delete_author_ui_tests.rs
expression: output
---
╭───────────────────── Delete Author ─────────────────────╮
│ Do you really want to delete the author: │
│ Test Author? │
│ │
│ │
│ ╭───╮ │
│ Delete Author Files: │ ✔ │ │
│ ╰───╯ │
│ ╭───╮ │
│ Add List Exclusion: │ │ │
│ ╰───╯ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -0,0 +1,38 @@
---
source: src/ui/readarr_ui/library/delete_author_ui_tests.rs
expression: output
---
╭───────────────────── Delete Author ─────────────────────╮
│ Do you really want to delete the author: │
│ Test Author? │
│ │
│ │
│ ╭───╮ │
│ Delete Author Files: │ ✔ │ │
│ ╰───╯ │
│ ╭───╮ │
│ Add List Exclusion: │ │ │
│ ╰───╯ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -0,0 +1,38 @@
---
source: src/ui/readarr_ui/library/delete_author_ui_tests.rs
expression: output
---
╭───────────────────── Delete Author ─────────────────────╮
│ Do you really want to delete the author: │
│ Test Author? │
│ │
│ │
│ ╭───╮ │
│ Delete Author Files: │ ✔ │ │
│ ╰───╯ │
│ ╭───╮ │
│ Add List Exclusion: │ │ │
│ ╰───╯ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -0,0 +1,38 @@
---
source: src/ui/readarr_ui/library/delete_author_ui_tests.rs
expression: output
---
╭───────────────────── Delete Author ─────────────────────╮
│ Do you really want to delete the author: │
│ Test Author? │
│ │
│ │
│ ╭───╮ │
│ Delete Author Files: │ │ │
│ ╰───╯ │
│ ╭───╮ │
│ Add List Exclusion: │ ✔ │ │
│ ╰───╯ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -0,0 +1,38 @@
---
source: src/ui/readarr_ui/library/library_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name ▼ Status Quality Profile Metadata Profile Books Size Monitored Tags
=> Test Author Continuing Standard Standard 0 0.00 GB 🏷 alex
╭───────────────────── Delete Author ─────────────────────╮
│ Do you really want to delete the author: │
│ Test Author? │
│ │
│ │
│ ╭───╮ │
│ Delete Author Files: │ ✔ │ │
│ ╰───╯ │
│ ╭───╮ │
│ Add List Exclusion: │ │ │
│ ╰───╯ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯