Skip to content

Commit 50f6614

Browse files
ampagenttmm
andcommitted
feat(zbugs-rune): searchable people pickers; filter and create labels in the picker
The assignee picker and the creator and assignee filters are Rune comboboxes searchable by login, as the web app's user picker. The label picker filters the project's labels, lists the issue's first, and creates a label from the filter when none matches, as the web app's, in place of the New Label sheet. Escape closes an open picker. Co-authored-by: tmm <tmm@tmm.dev>
1 parent 8ed3c07 commit 50f6614

3 files changed

Lines changed: 168 additions & 94 deletions

File tree

‎apps/zbugs-rune/e2e.sh‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ out=$(mktemp -d)
2222
issue=$(sql "select id from issue i where open and not exists
2323
(select 1 from comment c where c.\"issueID\" = i.id)
2424
order by length(description), \"shortID\" limit 1")
25-
# The label menu lists "New Label…", a separator, then the issue's project's
26-
# labels by name, bytewise (item 2 is the first).
25+
# The label picker lists the issue's project's labels by name, bytewise.
2726
first_label=$(sql "select l.name from label l join issue i on i.\"projectID\" = l.\"projectID\"
2827
where i.id = '$issue' order by l.name collate \"C\" limit 1")
2928
sql "delete from \"issueLabel\" where \"issueID\" = '$issue'"
@@ -48,16 +47,16 @@ cat > "$out/script.json" <<EOF
4847
{"t": 15.3, "type": "type", "text": "rocket"},
4948
{"t": 15.8, "type": "tap", "key": "emoji-🚀"},
5049
{"t": 16.5, "type": "tap", "key": "labels-trigger"},
51-
{"t": 17.0, "type": "tap", "key": "labels-item2"},
50+
{"t": 17.0, "type": "tap", "key": "label-$first_label"},
5251
{"t": 17.5, "type": "key", "name": "Escape"},
5352
{"t": 18.0, "type": "tap", "key": "toggle-open"},
5453
{"t": 19.0, "type": "tap", "key": "edit-comment"},
5554
{"t": 19.3, "type": "type", "text": " edited"},
5655
{"t": 20.5, "type": "tap", "key": "save-comment-edit"},
5756
{"t": 21.5, "type": "tap", "key": "labels-trigger"},
58-
{"t": 22.0, "type": "tap", "key": "labels-item0"},
59-
{"t": 22.5, "type": "type", "text": "$new_label"},
60-
{"t": 24.0, "type": "tap", "key": "save"},
57+
{"t": 22.0, "type": "type", "text": "$new_label"},
58+
{"t": 23.5, "type": "key", "name": "Enter"},
59+
{"t": 24.0, "type": "key", "name": "Escape"},
6160
{"t": 25.0, "type": "tap", "key": "visibility-trigger"},
6261
{"t": 25.5, "type": "tap", "key": "option1"},
6362
{"t": 26.5, "type": "tap", "key": "notifications-trigger"},

‎apps/zbugs-rune/src/main.rs‎

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod token;
1313
mod view;
1414

1515
use rune::{AppSpec, TextEdit, Tx};
16+
use rune_kit::ComboboxState;
1617
use settings::Settings;
1718
use sync::{Session, Setup, SyncThread};
1819
use zbugs_model::args::{
@@ -84,14 +85,11 @@ pub struct Filter {
8485
/// A dropdown on the issue page.
8586
#[derive(Clone, Debug, PartialEq, Eq)]
8687
pub enum Picker {
87-
Assignee,
8888
Labels,
8989
Visibility,
9090
Notifications,
9191
/// The sidebar's.
9292
Project,
93-
CreatorFilter,
94-
AssigneeFilter,
9593
LabelFilter,
9694
/// Reactions to the issue (its ID) or to one of its comments.
9795
Emoji(String),
@@ -110,7 +108,6 @@ pub enum Confirm {
110108
pub enum Sheet {
111109
NewIssue,
112110
EditIssue,
113-
NewLabel,
114111
Login,
115112
Servers,
116113
}
@@ -172,7 +169,13 @@ pub struct App {
172169
pub title_edit: TextEdit,
173170
pub description_edit: TextEdit,
174171
pub token_edit: TextEdit,
175-
pub label_edit: TextEdit,
172+
/// The label picker's filter, which also names a label to create.
173+
pub label_search: TextEdit,
174+
/// The searchable people pickers: the list's creator and assignee
175+
/// filters, and the issue's assignee.
176+
pub creator_combo: ComboboxState,
177+
pub assignee_filter_combo: ComboboxState,
178+
pub assignee_combo: ComboboxState,
176179
pub cache_edit: TextEdit,
177180
pub api_edit: TextEdit,
178181
pub sheet_error: Option<String>,
@@ -284,7 +287,10 @@ impl App {
284287
title_edit: TextEdit::new(""),
285288
description_edit: TextEdit::new(""),
286289
token_edit: TextEdit::new(""),
287-
label_edit: TextEdit::new(""),
290+
label_search: TextEdit::new(""),
291+
creator_combo: ComboboxState::default(),
292+
assignee_filter_combo: ComboboxState::default(),
293+
assignee_combo: ComboboxState::default(),
288294
cache_edit: TextEdit::new(""),
289295
api_edit: TextEdit::new(""),
290296
sheet_error: None,
@@ -727,15 +733,17 @@ impl App {
727733
}
728734
}
729735

730-
/// Creates a label in the issue's project and adds it to the issue.
731-
pub fn save_label(&mut self) {
732-
let name = self.label_edit.text.trim().to_owned();
733-
if name.is_empty() {
734-
self.sheet_error = Some("A label needs a name.".into());
735-
return;
736-
}
737-
if self.labels.iter().any(|l| l.name == name) {
738-
self.sheet_error = Some(format!("There's already a label named {name}."));
736+
/// Creates a label named as the label picker's filter, in the issue's
737+
/// project, and adds it to the issue, unless a label has that name
738+
/// (whatever its case), as the web picker does.
739+
pub fn create_label(&mut self) {
740+
let name = self.label_search.text.trim().to_owned();
741+
if name.is_empty()
742+
|| self
743+
.labels
744+
.iter()
745+
.any(|l| l.name.to_lowercase() == name.to_lowercase())
746+
{
739747
return;
740748
}
741749
let Some(issue) = &self.detail else { return };
@@ -745,7 +753,8 @@ impl App {
745753
label_name: name,
746754
project_id: None,
747755
}));
748-
self.sheet = None;
756+
self.label_search = TextEdit::new("");
757+
self.label_search.focused = true;
749758
}
750759

751760
pub fn toggle_open(&mut self) {
@@ -845,9 +854,16 @@ impl App {
845854
}
846855

847856
pub fn toggle_picker(&mut self, picker: Picker) {
848-
if matches!(picker, Picker::Emoji(_)) {
849-
self.emoji_search = TextEdit::new("");
850-
self.emoji_search.focused = true;
857+
match picker {
858+
Picker::Emoji(_) => {
859+
self.emoji_search = TextEdit::new("");
860+
self.emoji_search.focused = true;
861+
}
862+
Picker::Labels => {
863+
self.label_search = TextEdit::new("");
864+
self.label_search.focused = true;
865+
}
866+
_ => {}
851867
}
852868
self.picker = (self.picker.as_ref() != Some(&picker)).then_some(picker);
853869
}
@@ -961,11 +977,6 @@ impl App {
961977
self.title_edit = TextEdit::new(issue.title.clone());
962978
self.description_edit = TextEdit::new(issue.description.clone());
963979
}
964-
Sheet::NewLabel => {
965-
self.picker = None;
966-
self.label_edit = TextEdit::new("");
967-
self.label_edit.focused = true;
968-
}
969980
Sheet::Login => self.token_edit = TextEdit::new(""),
970981
Sheet::Servers => {
971982
self.cache_edit = TextEdit::new(self.settings.cache_url.clone());
@@ -1104,8 +1115,9 @@ fn apply_issues(s: &mut App, rows: Vec<Issue>, result_type: ResultType, tx: &mut
11041115
}
11051116
}
11061117

1107-
/// Keys no control took: j and k step through the list, and / starts a
1108-
/// search, as on the web.
1118+
/// Keys no control took: j and k step through the list, / starts a search,
1119+
/// as on the web, and Escape closes an open picker (Rune's popovers leave
1120+
/// Escape to the app).
11091121
fn on_key(s: &mut App, event: &rune::KeyEvent, tx: &mut Tx) -> bool {
11101122
if s.sheet.is_some()
11111123
|| s.confirm.is_some()
@@ -1116,6 +1128,7 @@ fn on_key(s: &mut App, event: &rune::KeyEvent, tx: &mut Tx) -> bool {
11161128
return false;
11171129
}
11181130
match &event.key {
1131+
rune::Key::Escape if s.picker.is_some() => s.picker = None,
11191132
rune::Key::Text(t) if t == "j" => s.step(1, tx),
11201133
rune::Key::Text(t) if t == "k" => s.step(-1, tx),
11211134
rune::Key::Text(t) if t == "/" => s.search.focused = true,

0 commit comments

Comments
 (0)