feat: add persistent pull request sort and filter menus
This commit is contained in:
+303
-46
@@ -432,8 +432,11 @@ fn pull_page(closed: bool, rows: &[(u64, &str, &str)], has_more: bool) -> Outcom
|
||||
Category::Open
|
||||
},
|
||||
),
|
||||
date.into(),
|
||||
PullRequestReviewState::None,
|
||||
PullRequestMetadata {
|
||||
created_at: date.into(),
|
||||
updated_at: date.into(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
@@ -464,7 +467,8 @@ fn pull_request_review_refresh_preserves_identity_sorting_and_raw_search_labels(
|
||||
result: Ok(page), ..
|
||||
} = &mut page
|
||||
{
|
||||
page.entries[0].2 = review_state;
|
||||
page.entries[0].1.requested = review_state == PullRequestReviewState::Requested;
|
||||
page.entries[0].1.approved = review_state == PullRequestReviewState::Approved;
|
||||
}
|
||||
state.apply_result(1, Ok(page));
|
||||
let selected = state.entries().nth(state.selected).unwrap();
|
||||
@@ -487,8 +491,8 @@ fn pull_request_review_refresh_preserves_identity_sorting_and_raw_search_labels(
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_request_review_controls_compose_with_search_categories_and_streamed_updates() {
|
||||
use PullRequestReviewState::{Approved, None, Requested};
|
||||
fn pull_request_menus_apply_cancel_and_compose_with_query_categories_and_updates() {
|
||||
use crate::pull_requests::{Direction, Preferences, ReviewFilter, SortField};
|
||||
let mut editor = editor();
|
||||
editor.execute("pull_requests origin");
|
||||
let (id, _) = editor.take_diff_request().unwrap();
|
||||
@@ -509,10 +513,9 @@ fn pull_request_review_controls_compose_with_search_categories_and_streamed_upda
|
||||
result: Ok(page), ..
|
||||
} = &mut page
|
||||
{
|
||||
for (entry, state) in page.entries.iter_mut().zip([
|
||||
Requested, Approved, None, Requested, Approved, None, Requested,
|
||||
]) {
|
||||
entry.2 = state;
|
||||
for (index, entry) in page.entries.iter_mut().enumerate() {
|
||||
entry.1.requested = matches!(index, 0 | 3 | 6);
|
||||
entry.1.approved = matches!(index, 0 | 1 | 4);
|
||||
}
|
||||
page.entries[6].0.category = Category::Drafts;
|
||||
}
|
||||
@@ -524,47 +527,110 @@ fn pull_request_review_controls_compose_with_search_categories_and_streamed_upda
|
||||
.entries()
|
||||
.map(|entry| {
|
||||
let RevisionTarget::PullRequest { number, .. } = entry.target else {
|
||||
panic!("expected pull request");
|
||||
panic!("expected PR")
|
||||
};
|
||||
number
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let selected = |editor: &Editor| {
|
||||
let diff = editor.diff().unwrap();
|
||||
diff.entries().nth(diff.selected).unwrap().target.clone()
|
||||
};
|
||||
assert_eq!(numbers(&editor), [6, 5, 4, 3, 2, 1]);
|
||||
editor.handle(Input::End);
|
||||
let target = selected(&editor);
|
||||
editor.handle(Input::Ctrl('s'));
|
||||
assert_eq!(numbers(&editor), [4, 1, 6, 5, 3, 2]);
|
||||
assert_eq!(selected(&editor), target);
|
||||
editor.handle(Input::Ctrl('s'));
|
||||
assert_eq!(numbers(&editor), [5, 2, 6, 4, 3, 1]);
|
||||
assert_eq!(selected(&editor), target);
|
||||
editor.handle(Input::Ctrl('s'));
|
||||
assert_eq!(numbers(&editor), [6, 5, 4, 3, 2, 1]);
|
||||
for ch in "Fix".chars() {
|
||||
editor.handle(Input::Char(ch));
|
||||
}
|
||||
assert_eq!(numbers(&editor), [5, 4, 3, 2, 1, 6]);
|
||||
assert_eq!(
|
||||
numbers(&editor),
|
||||
[4, 1, 6, 5, 3, 2],
|
||||
"query matches without outranking configured sort"
|
||||
);
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
editor.handle(Input::End);
|
||||
let target = editor
|
||||
.diff()
|
||||
.unwrap()
|
||||
.entries()
|
||||
.last()
|
||||
.unwrap()
|
||||
.target
|
||||
.clone();
|
||||
editor.handle(Input::Ctrl('s'));
|
||||
assert_eq!(numbers(&editor), [4, 1, 5, 3, 2, 6]);
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Char('1'));
|
||||
editor.handle(Input::Right);
|
||||
assert_eq!(editor.diff().unwrap().query, "");
|
||||
assert_eq!(editor.pull_request_preferences, Preferences::default());
|
||||
editor.handle(Input::Escape);
|
||||
assert!(editor.diff().is_some());
|
||||
assert!(editor.diff().unwrap().pull_request_menu.is_none());
|
||||
assert!(!editor.pull_request_preferences_changed);
|
||||
editor.handle(Input::Ctrl('s'));
|
||||
editor.handle(Input::Ctrl('j'));
|
||||
editor.handle(Input::Char('1'));
|
||||
editor.handle(Input::Right);
|
||||
editor.handle(Input::Enter);
|
||||
assert_eq!(numbers(&editor), [1, 2, 3, 4, 5, 6]);
|
||||
let diff = editor.diff().unwrap();
|
||||
assert_eq!(diff.entries().nth(diff.selected).unwrap().target, target);
|
||||
assert!(editor.pull_request_preferences_changed);
|
||||
assert_eq!(
|
||||
editor.pull_request_preferences.sort[&SortField::UpdatedAt].direction,
|
||||
Direction::Asc
|
||||
);
|
||||
for ch in "Fix".chars() {
|
||||
editor.handle(Input::Char(ch));
|
||||
}
|
||||
assert_eq!(
|
||||
numbers(&editor),
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
"settings sort wins over fuzzy scores"
|
||||
);
|
||||
editor.pull_request_preferences_changed = false;
|
||||
editor.handle(Input::Ctrl('s'));
|
||||
editor.handle(Input::Char('4'));
|
||||
editor.handle(Input::Ctrl('f'));
|
||||
assert_eq!(numbers(&editor), [4, 1]);
|
||||
editor.handle(Input::Ctrl('f'));
|
||||
assert_eq!(numbers(&editor), [5, 2]);
|
||||
editor.handle(Input::Ctrl('f'));
|
||||
assert_eq!(numbers(&editor), [4, 1, 5, 3, 2, 6]);
|
||||
editor.handle(Input::Ctrl('f'));
|
||||
editor.handle(Input::Home);
|
||||
let mut update = pull_page(false, &[(4, "Fix", "4")], true);
|
||||
assert!(
|
||||
!editor
|
||||
.diff()
|
||||
.unwrap()
|
||||
.pull_request_menu
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.sort
|
||||
);
|
||||
assert_eq!(
|
||||
editor
|
||||
.diff()
|
||||
.unwrap()
|
||||
.pull_request_menu
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.draft,
|
||||
editor.pull_request_preferences
|
||||
);
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Char(' '));
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Char(' '));
|
||||
assert_eq!(
|
||||
numbers(&editor),
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
"draft filtering is not applied"
|
||||
);
|
||||
editor.handle(Input::Enter);
|
||||
assert_eq!(
|
||||
editor.pull_request_preferences.filters,
|
||||
[ReviewFilter::Requested]
|
||||
);
|
||||
assert_eq!(
|
||||
numbers(&editor),
|
||||
[1, 4],
|
||||
"outstanding request wins even with prior approval"
|
||||
);
|
||||
let mut update = pull_page(false, &[(4, "Fix updated", "0")], true);
|
||||
if let Outcome::PullRequestPage {
|
||||
result: Ok(page), ..
|
||||
} = &mut update
|
||||
{
|
||||
page.entries[0].2 = Approved;
|
||||
page.entries[0].1.approved = true;
|
||||
}
|
||||
editor.apply_diff_result(id, Ok(update));
|
||||
assert_eq!(numbers(&editor), [1]);
|
||||
@@ -576,19 +642,210 @@ fn pull_request_review_controls_compose_with_search_categories_and_streamed_upda
|
||||
assert_eq!(numbers(&editor), [1, 7]);
|
||||
assert_eq!(editor.diff().unwrap().query, "Fix");
|
||||
assert!(editor.diff().unwrap().pull_request_partial());
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
assert_eq!(numbers(&editor), [7, 1]);
|
||||
editor.handle(Input::Ctrl('f'));
|
||||
editor.handle(Input::Char(' '));
|
||||
editor.handle(Input::Enter);
|
||||
assert!(matches!(
|
||||
editor.take_diff_request(),
|
||||
Some((
|
||||
_,
|
||||
Operation::ResolvePullRequest(Revision {
|
||||
target: RevisionTarget::PullRequest { number: 7, .. },
|
||||
..
|
||||
assert!(numbers(&editor).is_empty(), "no checkboxes means no rows");
|
||||
editor.execute("diff.close");
|
||||
editor.execute("pull_requests elsewhere");
|
||||
assert_eq!(
|
||||
editor.diff().unwrap().pull_request_preferences,
|
||||
editor.pull_request_preferences
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_request_sort_ranks_pin_slots_shift_collisions_and_clear_without_changing_direction() {
|
||||
use crate::pull_requests::{Direction, Preferences, SortField::*};
|
||||
let mut state = DiffState::pull_requests("origin".into());
|
||||
state.handle(Input::Ctrl('s'));
|
||||
for input in [
|
||||
Input::Char('2'),
|
||||
Input::Home,
|
||||
Input::Char('2'),
|
||||
Input::Right,
|
||||
] {
|
||||
state.handle(input);
|
||||
}
|
||||
let draft = &state.pull_request_menu.as_ref().unwrap().draft;
|
||||
assert_eq!(draft.sort[&Requested].precedence, Some(3));
|
||||
assert_eq!(draft.sort[&UpdatedAt].precedence, Some(2));
|
||||
assert_eq!(
|
||||
draft.ordered_fields(),
|
||||
[Approved, UpdatedAt, Requested, CreatedAt]
|
||||
);
|
||||
assert_eq!(state.pull_request_menu.as_ref().unwrap().selected, 1);
|
||||
state.handle(Input::Backspace);
|
||||
assert_eq!(state.pull_request_menu.as_ref().unwrap().selected, 0);
|
||||
state.handle(Input::Enter);
|
||||
assert_eq!(
|
||||
state.pull_request_preferences.sort[&UpdatedAt].precedence,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
state.pull_request_preferences.sort[&UpdatedAt].direction,
|
||||
Direction::Asc
|
||||
);
|
||||
assert_eq!(
|
||||
state.pull_request_preferences.ordered_fields(),
|
||||
[UpdatedAt, Approved, Requested, CreatedAt]
|
||||
);
|
||||
let mut preferences = Preferences::default();
|
||||
preferences.set_rank(Requested, Some(4));
|
||||
preferences.set_rank(UpdatedAt, Some(4));
|
||||
assert_eq!(preferences.sort[&Requested].precedence, Some(1));
|
||||
for (field, rank) in [(Approved, 1), (CreatedAt, 2), (UpdatedAt, 1)] {
|
||||
preferences.set_rank(field, Some(rank));
|
||||
preferences.validate().unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
preferences.ordered_fields(),
|
||||
[UpdatedAt, Approved, CreatedAt, Requested]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_request_sort_uses_independent_approval_dates_and_number_ties() {
|
||||
use crate::pull_requests::SortField;
|
||||
let mut state = DiffState::pull_requests("origin".into());
|
||||
state.take_request();
|
||||
let mut page = pull_page(
|
||||
false,
|
||||
&[(1, "A", "1"), (2, "B", "1"), (3, "C", "1"), (4, "D", "2")],
|
||||
false,
|
||||
);
|
||||
if let Outcome::PullRequestPage {
|
||||
result: Ok(page), ..
|
||||
} = &mut page
|
||||
{
|
||||
for entry in &mut page.entries {
|
||||
entry.1.updated_at = "same".into();
|
||||
}
|
||||
page.entries[0].1.requested = true;
|
||||
page.entries[0].1.approved = true;
|
||||
page.entries[1].1.requested = true;
|
||||
}
|
||||
state.apply_result(1, Ok(page));
|
||||
let numbers = |state: &DiffState| {
|
||||
state
|
||||
.entries()
|
||||
.map(|entry| {
|
||||
let RevisionTarget::PullRequest { number, .. } = entry.target else {
|
||||
unreachable!()
|
||||
};
|
||||
number
|
||||
})
|
||||
))
|
||||
));
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(numbers(&state), [1, 2, 4, 3]);
|
||||
assert_eq!(
|
||||
state.pull_request_review_state(state.entries().next().unwrap()),
|
||||
PullRequestReviewState::Requested
|
||||
);
|
||||
let mut preferences = state.pull_request_preferences.clone();
|
||||
preferences.set_rank(SortField::CreatedAt, Some(1));
|
||||
state.set_pull_request_preferences(preferences);
|
||||
assert_eq!(numbers(&state), [4, 1, 2, 3]);
|
||||
state.handle(Input::Ctrl('s'));
|
||||
state.handle(Input::Home);
|
||||
state.handle(Input::Left);
|
||||
state.handle(Input::Enter);
|
||||
assert_eq!(
|
||||
numbers(&state),
|
||||
[1, 2, 3, 4],
|
||||
"creation direction is independent of update direction"
|
||||
);
|
||||
let mut tied = pull_page(false, &[(5, "E", "1")], false);
|
||||
if let Outcome::PullRequestPage {
|
||||
result: Ok(page), ..
|
||||
} = &mut tied
|
||||
{
|
||||
page.entries[0].1.updated_at = "same".into();
|
||||
}
|
||||
state.apply_result(1, Ok(tied));
|
||||
assert_eq!(
|
||||
numbers(&state),
|
||||
[1, 2, 5, 3, 4],
|
||||
"number descending breaks complete ties"
|
||||
);
|
||||
let mut preferences = state.pull_request_preferences.clone();
|
||||
preferences
|
||||
.sort
|
||||
.entry(SortField::Requested)
|
||||
.or_default()
|
||||
.direction = crate::pull_requests::Direction::Asc;
|
||||
state.set_pull_request_preferences(preferences);
|
||||
assert_eq!(
|
||||
numbers(&state),
|
||||
[5, 3, 1, 2, 4],
|
||||
"explicit config directions also apply to booleans"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_request_menu_drafts_survive_streaming_and_checkbox_unions_exclude_requests_from_approved() {
|
||||
let mut state = DiffState::pull_requests("origin".into());
|
||||
state.take_request();
|
||||
state.handle(Input::Ctrl('f'));
|
||||
state.handle(Input::Char(' '));
|
||||
let mut page = pull_page(
|
||||
false,
|
||||
&[(1, "Both", "1"), (2, "Approved", "2"), (3, "Other", "3")],
|
||||
true,
|
||||
);
|
||||
if let Outcome::PullRequestPage {
|
||||
result: Ok(page), ..
|
||||
} = &mut page
|
||||
{
|
||||
page.entries[0].1.requested = true;
|
||||
page.entries[0].1.approved = true;
|
||||
page.entries[1].1.approved = true;
|
||||
}
|
||||
state.apply_result(1, Ok(page));
|
||||
assert_eq!(state.entries().count(), 3);
|
||||
assert_eq!(
|
||||
state.pull_request_menu.as_ref().unwrap().draft.filters,
|
||||
[ReviewFilter::Approved, ReviewFilter::Other]
|
||||
);
|
||||
state.handle(Input::Enter);
|
||||
assert_eq!(
|
||||
state
|
||||
.entries()
|
||||
.map(|entry| entry.label.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["#3 Other", "#2 Approved"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_request_preferences_validate_ranks_filters_and_unknown_fields() {
|
||||
for value in [
|
||||
r#"{"sort":{"requested":{"precedence":0}}}"#,
|
||||
r#"{"sort":{"requested":{"precedence":5}}}"#,
|
||||
r#"{"sort":{"requested":{"precedence":1},"approved":{"precedence":1}}}"#,
|
||||
r#"{"filters":["requested","requested"]}"#,
|
||||
] {
|
||||
let preferences: crate::pull_requests::Preferences = serde_json::from_str(value).unwrap();
|
||||
assert!(preferences.validate().is_err(), "{value}");
|
||||
}
|
||||
for value in [
|
||||
r#"{"extra":true}"#,
|
||||
r#"{"sort":{"requested":{"extra":true}}}"#,
|
||||
r#"{"filters":["unknown"]}"#,
|
||||
] {
|
||||
assert!(
|
||||
serde_json::from_str::<crate::pull_requests::Preferences>(value).is_err(),
|
||||
"{value}"
|
||||
);
|
||||
}
|
||||
let preferences: crate::pull_requests::Preferences =
|
||||
serde_json::from_str(r#"{"sort":{"updated_at":{}}}"#).unwrap();
|
||||
preferences.validate().unwrap();
|
||||
assert_eq!(preferences.filters, ReviewFilter::ALL);
|
||||
assert_eq!(
|
||||
preferences.sort[&SortField::UpdatedAt],
|
||||
crate::pull_requests::SortRule::default()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user