Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bb-imager-gui/assets/icons/search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions bb-imager-gui/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub(crate) const ARROW_FORWARD_IOS_ICON_BYTES: &[u8] =
pub(crate) const FILE_SAVE_ICON_BYTES: &[u8] = include_bytes!("../assets/icons/file-save.svg");
pub(crate) const INFO_ICON_BYTES: &[u8] = include_bytes!("../assets/icons/info.svg");
pub(crate) const COPY_ICON_BYTES: &[u8] = include_bytes!("../assets/icons/content-copy.svg");
pub(crate) const SEARCH_ICON_BYTES: &[u8] = include_bytes!("../assets/icons/search.svg");

// Font
pub(crate) const FONT_REGULAR: iced::Font = iced::Font::with_name("Nunito");
Expand Down
5 changes: 3 additions & 2 deletions bb-imager-gui/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,9 @@ impl Db {
}

/// Get board list data. (ID, Icon, Name)
pub(crate) async fn board_list(&self) -> sqlx::Result<Vec<BoardListItem>> {
sqlx::query_as("SELECT id, icon, name FROM boards")
pub(crate) async fn board_list(&self, search: &str) -> sqlx::Result<Vec<BoardListItem>> {
sqlx::query_as("SELECT id, icon, name FROM boards WHERE name LIKE $1 COLLATE NOCASE")
.bind(format!("%{}%", search))
.fetch_all(&self.db)
.await
}
Expand Down
107 changes: 96 additions & 11 deletions bb-imager-gui/src/db/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ async fn add_config_inserts_device_into_board_list() {
db.init().await.expect("DB initialization should succeed");

let initial_boards = db
.board_list()
.board_list("")
.await
.expect("Fetching board list should succeed");

Expand Down Expand Up @@ -357,7 +357,7 @@ async fn add_config_inserts_device_into_board_list() {
.expect("add_config should succeed");

let updated_boards = db
.board_list()
.board_list("")
.await
.expect("Fetching board list should succeed");

Expand Down Expand Up @@ -419,7 +419,7 @@ async fn add_config_updates_existing_device_with_same_name() {

// Get inserted board id
let boards = db
.board_list()
.board_list("")
.await
.expect("Fetching board list should succeed");

Expand Down Expand Up @@ -456,7 +456,7 @@ async fn add_config_updates_existing_device_with_same_name() {

// Ensure board count unchanged
let updated_boards = db
.board_list()
.board_list("")
.await
.expect("Fetching board list should succeed");

Expand Down Expand Up @@ -544,7 +544,7 @@ async fn add_config_inserts_os_image_for_board() {
.await
.expect("add_config should succeed");

let boards = db.board_list().await.unwrap();
let boards = db.board_list("").await.unwrap();
let board_id = boards.iter().find(|b| b.name == board.name).unwrap().id;

let items = db
Expand Down Expand Up @@ -623,7 +623,7 @@ async fn os_image_by_id_returns_correct_data() {
.await
.expect("add_config should succeed");

let boards = db.board_list().await.unwrap();
let boards = db.board_list("").await.unwrap();
let board_id = boards.iter().find(|b| b.name == "Test Board").unwrap().id;

let items = db
Expand Down Expand Up @@ -730,7 +730,7 @@ async fn add_config_inserts_os_sublist_for_board() {
.await
.expect("add_config should succeed");

let boards = db.board_list().await.unwrap();
let boards = db.board_list("").await.unwrap();
let board_id = boards.iter().find(|b| b.name == "Test Board").unwrap().id;

let items = db
Expand Down Expand Up @@ -827,7 +827,7 @@ async fn nested_os_sublists_propagate_board_support() {
.expect("add_config should succeed");

let board_id = db
.board_list()
.board_list("")
.await
.unwrap()
.into_iter()
Expand Down Expand Up @@ -906,7 +906,7 @@ async fn remote_os_sublist_is_returned_for_board() {
.expect("add_config should succeed");

let board_id = db
.board_list()
.board_list("")
.await
.unwrap()
.into_iter()
Expand Down Expand Up @@ -994,7 +994,7 @@ async fn remote_os_sublist_resolve_inserts_child_items_and_clears_url() {
.expect("add_config should succeed");

let board_id = db
.board_list()
.board_list("")
.await
.unwrap()
.into_iter()
Expand Down Expand Up @@ -1102,7 +1102,7 @@ async fn duplicate_remote_sublist_resolve_does_not_duplicate_os_items() {
.expect("add_config should succeed");

let board_id = db
.board_list()
.board_list("")
.await
.unwrap()
.into_iter()
Expand Down Expand Up @@ -1158,3 +1158,88 @@ async fn duplicate_remote_sublist_resolve_does_not_duplicate_os_items() {

assert!(remote_lists_after.is_empty(),);
}

/// This test verifies that board_list(search) correctly filters boards
/// using a case-insensitive LIKE query.
///
/// What this test checks:
/// 1. Multiple boards are inserted into the database.
/// 2. board_list(Some("beagle")) is called.
/// 3. Only boards whose names contain "beagle" (case-insensitive) are returned.
/// 4. Non-matching boards are excluded.
///
/// Why this test is needed:
/// - board_list() has two execution paths (search and non-search).
/// - Ensures the LIKE query is applied correctly.
/// - Ensures COLLATE NOCASE works as expected.
/// - Prevents regressions where search returns all boards or none.
///
/// Without this test:
/// - Search filtering could silently break.
/// - Case-insensitive matching might stop working.
/// - UI board search would behave incorrectly.
#[tokio::test]
async fn board_list_search_filters_boards_case_insensitive() {
let db = Db::new().expect("Failed to create DB");
db.init().await.expect("DB init should succeed");

let board1 = bb_config::config::Device {
name: "Test Board 1".to_string(),
description: "Board 1".to_string(),
icon: None,
flasher: bb_config::config::Flasher::SdCard,
instructions: None,
oshw: None,
specification: vec![],
documentation: None,
tags: HashSet::from(["bbb".to_string()]),
};

let board2 = bb_config::config::Device {
name: "Test Board 2".to_string(),
description: "Board 2".to_string(),
icon: None,
flasher: bb_config::config::Flasher::SdCard,
instructions: None,
oshw: None,
specification: vec![],
documentation: None,
tags: HashSet::from(["beagleplay".to_string()]),
};

let board3 = bb_config::config::Device {
name: "Test Board 3".to_string(),
description: "Board 3".to_string(),
icon: None,
flasher: bb_config::config::Flasher::SdCard,
instructions: None,
oshw: None,
specification: vec![],
documentation: None,
tags: HashSet::from(["rpi".to_string()]),
};

let config = Config {
imager: bb_config::config::Imager {
remote_configs: Default::default(),
devices: vec![board1, board2, board3],
},
os_list: vec![],
};

db.add_config(config)
.await
.expect("add_config should succeed");

let results = db.board_list("test").await.expect("search should succeed");

assert_eq!(
results.len(),
3,
"Only boards containing 'test' should be returned"
);

assert!(results.iter().any(|b| b.name == "Test Board 1"));
assert!(results.iter().any(|b| b.name == "Test Board 2"));
assert!(results.iter().any(|b| b.name == "Test Board 3"));
}
24 changes: 8 additions & 16 deletions bb-imager-gui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ impl BBImager {
common,
boards: Vec::new(),
selected_board: None,
search_text: String::new(),
})
}

Expand Down Expand Up @@ -163,10 +164,6 @@ impl BBImager {
)
}

fn fetch_board_images(&self) -> Task<BBImagerMessage> {
self.common().fetch_board_images()
}

fn common_mut(&mut self) -> &mut BBImagerCommon {
match self {
BBImager::ChooseBoard(x) => &mut x.common,
Expand Down Expand Up @@ -222,7 +219,11 @@ impl BBImager {
}
};

self.refresh_board_list()
if let BBImager::ChooseBoard(x) = self {
return x.refresh_board_list();
}

Task::none()
}

fn subscription(&self) -> Subscription<BBImagerMessage> {
Expand Down Expand Up @@ -315,15 +316,6 @@ impl BBImager {
t
}

fn refresh_board_list(&self) -> Task<BBImagerMessage> {
let db = self.common().db.clone();

Task::perform(
async move { db.board_list().await.unwrap() },
BBImagerMessage::UpdateBoardList,
)
}

fn resolve_remote_sublists(&self, board_id: i64, pos: Option<i64>) -> Task<BBImagerMessage> {
let db = self.common().db.clone();
let downloader = self.common().downloader.clone();
Expand Down Expand Up @@ -415,8 +407,8 @@ impl BBImager {
};

match self {
BBImager::ChooseBoard(_) => {
Task::batch([self.refresh_board_list(), self.scroll_reset()])
BBImager::ChooseBoard(inner) => {
Task::batch([inner.refresh_board_list(), self.scroll_reset()])
}
BBImager::ChooseOs(inner) => {
let board_id = inner.selected_board.id;
Expand Down
30 changes: 22 additions & 8 deletions bb-imager-gui/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ pub(crate) enum BBImagerMessage {

/// DB Ops
DbInitSuccess,

/// Search
UpdateSearchText(String),
}

pub(crate) fn update(state: &mut BBImager, message: BBImagerMessage) -> Task<BBImagerMessage> {
Expand Down Expand Up @@ -233,11 +236,14 @@ pub(crate) fn update(state: &mut BBImager, message: BBImagerMessage) -> Task<BBI
},
);

let tail_tasks = if let BBImager::ChooseBoard(_) = state {
let tail_tasks = if let BBImager::ChooseBoard(inner) = state {
// If we are in ChooseBoard page, update the board list
Task::batch([state.fetch_board_images(), state.refresh_board_list()])
Task::batch([
inner.common.fetch_board_images(),
inner.refresh_board_list(),
])
} else {
state.fetch_board_images()
state.common().fetch_board_images()
};

// We want fetch board images to run after the config has been added
Expand Down Expand Up @@ -502,12 +508,20 @@ pub(crate) fn update(state: &mut BBImager, message: BBImagerMessage) -> Task<BBI
BBImagerMessage::ResolveImages,
);

return Task::batch([
board_icon_cache_task,
config_fetch_task,
state.refresh_board_list(),
]);
let board_refresh_task = if let BBImager::ChooseBoard(x) = state {
x.refresh_board_list()
} else {
Task::none()
};

return Task::batch([board_icon_cache_task, config_fetch_task, board_refresh_task]);
}
BBImagerMessage::UpdateSearchText(x) => match state {
BBImager::ChooseBoard(inner) => {
return inner.update_search(x);
}
_ => {}
},
BBImagerMessage::Null => {}
}

Expand Down
17 changes: 17 additions & 0 deletions bb-imager-gui/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub(crate) struct ChooseBoardState {
pub(crate) common: BBImagerCommon,
pub(crate) boards: Vec<db::BoardListItem>,
pub(crate) selected_board: Option<Board>,
pub(crate) search_text: String,
}

impl ChooseBoardState {
Expand All @@ -70,6 +71,21 @@ impl ChooseBoardState {
pub(crate) fn image_handle_cache(&self) -> &helpers::ImageHandleCache {
&self.common.img_handle_cache
}

pub(crate) fn refresh_board_list(&self) -> Task<BBImagerMessage> {
let db = self.common.db.clone();
let search = self.search_text.clone();

Task::perform(
async move { db.board_list(&search).await.unwrap() },
BBImagerMessage::UpdateBoardList,
)
}

pub(crate) fn update_search(&mut self, search: String) -> Task<BBImagerMessage> {
self.search_text = search;
self.refresh_board_list()
}
}

impl From<ChooseOsState> for ChooseBoardState {
Expand All @@ -78,6 +94,7 @@ impl From<ChooseOsState> for ChooseBoardState {
common: value.common,
boards: Vec::new(),
selected_board: Some(value.selected_board),
search_text: String::new(),
}
}
}
Expand Down
21 changes: 18 additions & 3 deletions bb-imager-gui/src/ui/board_selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,24 @@ fn board_list_pane<'a>(state: &'a ChooseBoardState) -> Element<'a, BBImagerMessa
})
.map(Into::into);

widget::scrollable(column(items).padding(LIST_COL_PADDING))
.id(state.common.scroll_id.clone())
.into()
widget::scrollable(
column(
[
helpers::search_box(&state.search_text).into(),
widget::center(widget::rule::horizontal(2))
.padding(iced::Padding {
left: 16.0,
..Default::default()
})
.into(),
]
.into_iter()
.chain(items),
)
.padding(LIST_COL_PADDING),
)
.id(state.common.scroll_id.clone())
.into()
}

fn board_view_pane<'a>(state: &'a ChooseBoardState) -> Element<'a, BBImagerMessage> {
Expand Down
Loading
Loading