sidebar: Improve behavior of "view more" button (#51105)
This PR adjusts the "View More" button in the sidebar to expose threads in batches of 5. Once you've expanded the whole available set, a button to collapse the list back to the default number appears at the bottom. Similarly, as soon as you expand the list even once, a button in the group header shows up that does the same thing. No release notes because this is still under feature flag. Release Notes: - N/A
This commit is contained in:
1 parent
6810f23634
commit
171e7cb4a7
2 files changed
+169
-30
No files matched your search
@@ -1 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.857 6.857 4.286 5.43 2.857 4M2.857 12l1.429-1.429-1.429-1.428M6.857 4.571h6.286M6.857 8h6.286M6.857 11.428h6.286"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 4H7.33333" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 8H7.33333" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 12H7.33333" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 4L12 6L14 4" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 12L12 10L14 12" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 289 B After Width: | Height: | Size: 653 B |
+162
-29
@@ -89,6 +89,7 @@ enum ListEntry {
|
||||
ViewMore {
|
||||
path_list: PathList,
|
||||
remaining_count: usize,
|
||||
is_fully_expanded: bool,
|
||||
},
|
||||
NewThread {
|
||||
path_list: PathList,
|
||||
@@ -174,7 +175,7 @@ pub struct Sidebar {
|
||||
focused_thread: Option<acp::SessionId>,
|
||||
active_entry_index: Option<usize>,
|
||||
collapsed_groups: HashSet<PathList>,
|
||||
expanded_groups: HashSet<PathList>,
|
||||
expanded_groups: HashMap<PathList, usize>,
|
||||
}
|
||||
|
||||
impl EventEmitter<SidebarEvent> for Sidebar {}
|
||||
@@ -269,7 +270,7 @@ impl Sidebar {
|
||||
focused_thread: None,
|
||||
active_entry_index: None,
|
||||
collapsed_groups: HashSet::new(),
|
||||
expanded_groups: HashSet::new(),
|
||||
expanded_groups: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,21 +580,20 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
let total = threads.len();
|
||||
let show_view_more =
|
||||
total > DEFAULT_THREADS_SHOWN && !self.expanded_groups.contains(&path_list);
|
||||
|
||||
let count = if show_view_more {
|
||||
DEFAULT_THREADS_SHOWN
|
||||
} else {
|
||||
total
|
||||
};
|
||||
let extra_batches = self.expanded_groups.get(&path_list).copied().unwrap_or(0);
|
||||
let threads_to_show =
|
||||
DEFAULT_THREADS_SHOWN + (extra_batches * DEFAULT_THREADS_SHOWN);
|
||||
let count = threads_to_show.min(total);
|
||||
let is_fully_expanded = count >= total;
|
||||
|
||||
entries.extend(threads.into_iter().take(count));
|
||||
|
||||
if show_view_more {
|
||||
if total > DEFAULT_THREADS_SHOWN {
|
||||
entries.push(ListEntry::ViewMore {
|
||||
path_list: path_list.clone(),
|
||||
remaining_count: total - DEFAULT_THREADS_SHOWN,
|
||||
remaining_count: total.saturating_sub(count),
|
||||
is_fully_expanded,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -632,10 +632,13 @@ impl Sidebar {
|
||||
|
||||
let had_notifications = self.has_notifications(cx);
|
||||
|
||||
let scroll_position = self.list_state.logical_scroll_top();
|
||||
|
||||
self.rebuild_contents(cx);
|
||||
self.recompute_active_entry_index(cx);
|
||||
|
||||
self.list_state.reset(self.contents.entries.len());
|
||||
self.list_state.scroll_to(scroll_position);
|
||||
|
||||
if had_notifications != self.has_notifications(cx) {
|
||||
multi_workspace.update(cx, |_, cx| {
|
||||
@@ -720,7 +723,15 @@ impl Sidebar {
|
||||
ListEntry::ViewMore {
|
||||
path_list,
|
||||
remaining_count,
|
||||
} => self.render_view_more(ix, path_list, *remaining_count, is_selected, cx),
|
||||
is_fully_expanded,
|
||||
} => self.render_view_more(
|
||||
ix,
|
||||
path_list,
|
||||
*remaining_count,
|
||||
*is_fully_expanded,
|
||||
is_selected,
|
||||
cx,
|
||||
),
|
||||
ListEntry::NewThread {
|
||||
path_list,
|
||||
workspace,
|
||||
@@ -765,7 +776,11 @@ impl Sidebar {
|
||||
let workspace_for_new_thread = workspace.clone();
|
||||
let workspace_for_remove = workspace.clone();
|
||||
// let workspace_for_activate = workspace.clone();
|
||||
|
||||
let path_list_for_toggle = path_list.clone();
|
||||
let path_list_for_collapse = path_list.clone();
|
||||
let view_more_expanded = self.expanded_groups.contains_key(path_list);
|
||||
|
||||
let multi_workspace = self.multi_workspace.upgrade();
|
||||
let workspace_count = multi_workspace
|
||||
.as_ref()
|
||||
@@ -853,6 +868,25 @@ impl Sidebar {
|
||||
)),
|
||||
)
|
||||
})
|
||||
.when(view_more_expanded && !is_collapsed, |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("project-header-collapse-{}", ix)),
|
||||
IconName::ListCollapse,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Collapse Displayed Threads"))
|
||||
.on_click(cx.listener({
|
||||
let path_list_for_collapse = path_list_for_collapse.clone();
|
||||
move |this, _, _window, cx| {
|
||||
this.selection = None;
|
||||
this.expanded_groups.remove(&path_list_for_collapse);
|
||||
this.update_entries(cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(has_threads, |this| {
|
||||
this.child(
|
||||
IconButton::new(ib_id, IconName::NewThread)
|
||||
@@ -1031,9 +1065,18 @@ impl Sidebar {
|
||||
let workspace = workspace.clone();
|
||||
self.activate_thread(session_info, &workspace, window, cx);
|
||||
}
|
||||
ListEntry::ViewMore { path_list, .. } => {
|
||||
ListEntry::ViewMore {
|
||||
path_list,
|
||||
is_fully_expanded,
|
||||
..
|
||||
} => {
|
||||
let path_list = path_list.clone();
|
||||
self.expanded_groups.insert(path_list);
|
||||
if *is_fully_expanded {
|
||||
self.expanded_groups.remove(&path_list);
|
||||
} else {
|
||||
let current = self.expanded_groups.get(&path_list).copied().unwrap_or(0);
|
||||
self.expanded_groups.insert(path_list, current + 1);
|
||||
}
|
||||
self.update_entries(cx);
|
||||
}
|
||||
ListEntry::NewThread { workspace, .. } => {
|
||||
@@ -1202,32 +1245,42 @@ impl Sidebar {
|
||||
ix: usize,
|
||||
path_list: &PathList,
|
||||
remaining_count: usize,
|
||||
is_fully_expanded: bool,
|
||||
is_selected: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let path_list = path_list.clone();
|
||||
let id = SharedString::from(format!("view-more-{}", ix));
|
||||
|
||||
let count = format!("({})", remaining_count);
|
||||
let (icon, label) = if is_fully_expanded {
|
||||
(IconName::ListCollapse, "Collapse List")
|
||||
} else {
|
||||
(IconName::Plus, "View More")
|
||||
};
|
||||
|
||||
ListItem::new(id)
|
||||
.focused(is_selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.px_1()
|
||||
.py_1p5()
|
||||
.p_1()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Plus)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("View More").color(Color::Muted))
|
||||
.child(Label::new(count).color(Color::Muted).size(LabelSize::Small)),
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||
.child(Label::new(label).color(Color::Muted))
|
||||
.when(!is_fully_expanded, |this| {
|
||||
this.child(
|
||||
Label::new(format!("({})", remaining_count))
|
||||
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.5))),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.selection = None;
|
||||
this.expanded_groups.insert(path_list.clone());
|
||||
if is_fully_expanded {
|
||||
this.expanded_groups.remove(&path_list);
|
||||
} else {
|
||||
let current = this.expanded_groups.get(&path_list).copied().unwrap_or(0);
|
||||
this.expanded_groups.insert(path_list.clone(), current + 1);
|
||||
}
|
||||
this.update_entries(cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
@@ -1660,9 +1713,15 @@ mod tests {
|
||||
)
|
||||
}
|
||||
ListEntry::ViewMore {
|
||||
remaining_count, ..
|
||||
remaining_count,
|
||||
is_fully_expanded,
|
||||
..
|
||||
} => {
|
||||
format!(" + View More ({}){}", remaining_count, selected)
|
||||
if *is_fully_expanded {
|
||||
format!(" - Collapse{}", selected)
|
||||
} else {
|
||||
format!(" + View More ({}){}", remaining_count, selected)
|
||||
}
|
||||
}
|
||||
ListEntry::NewThread { .. } => {
|
||||
format!(" [+ New Thread]{}", selected)
|
||||
@@ -1824,6 +1883,78 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_view_more_batched_expansion(cx: &mut TestAppContext) {
|
||||
let project = init_test_project("/my-project", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
|
||||
let sidebar = setup_sidebar(&multi_workspace, cx);
|
||||
|
||||
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
|
||||
// Create 17 threads: initially shows 5, then 10, then 15, then all 17 with Collapse
|
||||
save_n_test_threads(17, &path_list, cx).await;
|
||||
|
||||
multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
|
||||
cx.run_until_parked();
|
||||
|
||||
// Initially shows 5 threads + View More (12 remaining)
|
||||
let entries = visible_entries_as_strings(&sidebar, cx);
|
||||
assert_eq!(entries.len(), 7); // header + 5 threads + View More
|
||||
assert!(entries.iter().any(|e| e.contains("View More (12)")));
|
||||
|
||||
// Focus and navigate to View More, then confirm to expand by one batch
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
for _ in 0..7 {
|
||||
cx.dispatch_action(SelectNext);
|
||||
}
|
||||
cx.dispatch_action(Confirm);
|
||||
cx.run_until_parked();
|
||||
|
||||
// Now shows 10 threads + View More (7 remaining)
|
||||
let entries = visible_entries_as_strings(&sidebar, cx);
|
||||
assert_eq!(entries.len(), 12); // header + 10 threads + View More
|
||||
assert!(entries.iter().any(|e| e.contains("View More (7)")));
|
||||
|
||||
// Expand again by one batch
|
||||
sidebar.update_in(cx, |s, _window, cx| {
|
||||
let current = s.expanded_groups.get(&path_list).copied().unwrap_or(0);
|
||||
s.expanded_groups.insert(path_list.clone(), current + 1);
|
||||
s.update_entries(cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Now shows 15 threads + View More (2 remaining)
|
||||
let entries = visible_entries_as_strings(&sidebar, cx);
|
||||
assert_eq!(entries.len(), 17); // header + 15 threads + View More
|
||||
assert!(entries.iter().any(|e| e.contains("View More (2)")));
|
||||
|
||||
// Expand one more time - should show all 17 threads with Collapse button
|
||||
sidebar.update_in(cx, |s, _window, cx| {
|
||||
let current = s.expanded_groups.get(&path_list).copied().unwrap_or(0);
|
||||
s.expanded_groups.insert(path_list.clone(), current + 1);
|
||||
s.update_entries(cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// All 17 threads shown with Collapse button
|
||||
let entries = visible_entries_as_strings(&sidebar, cx);
|
||||
assert_eq!(entries.len(), 19); // header + 17 threads + Collapse
|
||||
assert!(!entries.iter().any(|e| e.contains("View More")));
|
||||
assert!(entries.iter().any(|e| e.contains("Collapse")));
|
||||
|
||||
// Click collapse - should go back to showing 5 threads
|
||||
sidebar.update_in(cx, |s, _window, cx| {
|
||||
s.expanded_groups.remove(&path_list);
|
||||
s.update_entries(cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Back to initial state: 5 threads + View More (12 remaining)
|
||||
let entries = visible_entries_as_strings(&sidebar, cx);
|
||||
assert_eq!(entries.len(), 7); // header + 5 threads + View More
|
||||
assert!(entries.iter().any(|e| e.contains("View More (12)")));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_collapse_and_expand_group(cx: &mut TestAppContext) {
|
||||
let project = init_test_project("/my-project", cx).await;
|
||||
@@ -1984,6 +2115,7 @@ mod tests {
|
||||
ListEntry::ViewMore {
|
||||
path_list: expanded_path.clone(),
|
||||
remaining_count: 42,
|
||||
is_fully_expanded: false,
|
||||
},
|
||||
// Collapsed project header
|
||||
ListEntry::ProjectHeader {
|
||||
@@ -2237,10 +2369,11 @@ mod tests {
|
||||
cx.dispatch_action(Confirm);
|
||||
cx.run_until_parked();
|
||||
|
||||
// All 8 threads should now be visible, no "View More"
|
||||
// All 8 threads should now be visible with a "Collapse" button
|
||||
let entries = visible_entries_as_strings(&sidebar, cx);
|
||||
assert_eq!(entries.len(), 9); // header + 8 threads
|
||||
assert_eq!(entries.len(), 10); // header + 8 threads + Collapse button
|
||||
assert!(!entries.iter().any(|e| e.contains("View More")));
|
||||
assert!(entries.iter().any(|e| e.contains("Collapse")));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
Reference in new issue
Block a user