Add the ability to expand and collapse sections of the contacts panel
Also, allow joining projects using the keyboard.
This commit is contained in:
parent
615319b2ab
commit
72e7079005
11 changed files with 189 additions and 85 deletions
|
@ -1251,6 +1251,7 @@
|
|||
"icon_width": 8
|
||||
},
|
||||
"row_height": 28,
|
||||
"section_icon_size": 8,
|
||||
"header_row": {
|
||||
"family": "Zed Mono",
|
||||
"color": "#8b8792",
|
||||
|
|
|
@ -1251,6 +1251,7 @@
|
|||
"icon_width": 8
|
||||
},
|
||||
"row_height": 28,
|
||||
"section_icon_size": 8,
|
||||
"header_row": {
|
||||
"family": "Zed Mono",
|
||||
"color": "#585260",
|
||||
|
|
|
@ -1251,6 +1251,7 @@
|
|||
"icon_width": 8
|
||||
},
|
||||
"row_height": 28,
|
||||
"section_icon_size": 8,
|
||||
"header_row": {
|
||||
"family": "Zed Mono",
|
||||
"color": "#9c9c9c",
|
||||
|
|
|
@ -1251,6 +1251,7 @@
|
|||
"icon_width": 8
|
||||
},
|
||||
"row_height": 28,
|
||||
"section_icon_size": 8,
|
||||
"header_row": {
|
||||
"family": "Zed Mono",
|
||||
"color": "#474747",
|
||||
|
|
|
@ -1251,6 +1251,7 @@
|
|||
"icon_width": 8
|
||||
},
|
||||
"row_height": 28,
|
||||
"section_icon_size": 8,
|
||||
"header_row": {
|
||||
"family": "Zed Mono",
|
||||
"color": "#93a1a1",
|
||||
|
|
|
@ -1251,6 +1251,7 @@
|
|||
"icon_width": 8
|
||||
},
|
||||
"row_height": 28,
|
||||
"section_icon_size": 8,
|
||||
"header_row": {
|
||||
"family": "Zed Mono",
|
||||
"color": "#586e75",
|
||||
|
|
|
@ -1251,6 +1251,7 @@
|
|||
"icon_width": 8
|
||||
},
|
||||
"row_height": 28,
|
||||
"section_icon_size": 8,
|
||||
"header_row": {
|
||||
"family": "Zed Mono",
|
||||
"color": "#979db4",
|
||||
|
|
|
@ -1251,6 +1251,7 @@
|
|||
"icon_width": 8
|
||||
},
|
||||
"row_height": 28,
|
||||
"section_icon_size": 8,
|
||||
"header_row": {
|
||||
"family": "Zed Mono",
|
||||
"color": "#5e6687",
|
||||
|
|
|
@ -15,7 +15,7 @@ use serde::Deserialize;
|
|||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use theme::IconButton;
|
||||
use workspace::menu::{SelectNext, SelectPrev};
|
||||
use workspace::menu::{Confirm, SelectNext, SelectPrev};
|
||||
use workspace::{AppState, JoinProject};
|
||||
|
||||
impl_actions!(
|
||||
|
@ -23,9 +23,16 @@ impl_actions!(
|
|||
[RequestContact, RemoveContact, RespondToContactRequest]
|
||||
);
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
|
||||
enum Section {
|
||||
Requests,
|
||||
Online,
|
||||
Offline,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum ContactEntry {
|
||||
Header(&'static str),
|
||||
Header(Section),
|
||||
IncomingRequest(Arc<User>),
|
||||
OutgoingRequest(Arc<User>),
|
||||
Contact(Arc<Contact>),
|
||||
|
@ -38,7 +45,9 @@ pub struct ContactsPanel {
|
|||
list_state: ListState,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
filter_editor: ViewHandle<Editor>,
|
||||
collapsed_sections: Vec<Section>,
|
||||
selection: Option<usize>,
|
||||
app_state: Arc<AppState>,
|
||||
_maintain_contacts: Subscription,
|
||||
}
|
||||
|
||||
|
@ -62,6 +71,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
cx.add_action(ContactsPanel::clear_filter);
|
||||
cx.add_action(ContactsPanel::select_next);
|
||||
cx.add_action(ContactsPanel::select_prev);
|
||||
cx.add_action(ContactsPanel::confirm);
|
||||
}
|
||||
|
||||
impl ContactsPanel {
|
||||
|
@ -97,18 +107,9 @@ impl ContactsPanel {
|
|||
let is_selected = this.selection == Some(ix);
|
||||
|
||||
match &this.entries[ix] {
|
||||
ContactEntry::Header(text) => {
|
||||
let header_style =
|
||||
theme.header_row.style_for(&Default::default(), is_selected);
|
||||
Label::new(text.to_string(), header_style.text.clone())
|
||||
.contained()
|
||||
.aligned()
|
||||
.left()
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(header_style.container)
|
||||
.boxed()
|
||||
ContactEntry::Header(section) => {
|
||||
let is_collapsed = this.collapsed_sections.contains(§ion);
|
||||
Self::render_header(*section, theme, is_selected, is_collapsed)
|
||||
}
|
||||
ContactEntry::IncomingRequest(user) => Self::render_contact_request(
|
||||
user.clone(),
|
||||
|
@ -153,17 +154,64 @@ impl ContactsPanel {
|
|||
}
|
||||
}),
|
||||
selection: None,
|
||||
collapsed_sections: Default::default(),
|
||||
entries: Default::default(),
|
||||
match_candidates: Default::default(),
|
||||
filter_editor: user_query_editor,
|
||||
_maintain_contacts: cx
|
||||
.observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)),
|
||||
user_store: app_state.user_store.clone(),
|
||||
app_state,
|
||||
};
|
||||
this.update_entries(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn render_header(
|
||||
section: Section,
|
||||
theme: &theme::ContactsPanel,
|
||||
is_selected: bool,
|
||||
is_collapsed: bool,
|
||||
) -> ElementBox {
|
||||
let header_style = theme.header_row.style_for(&Default::default(), is_selected);
|
||||
let text = match section {
|
||||
Section::Requests => "Requests",
|
||||
Section::Online => "Online",
|
||||
Section::Offline => "Offline",
|
||||
};
|
||||
let icon_size = theme.section_icon_size;
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new(if is_collapsed {
|
||||
"icons/disclosure-closed.svg"
|
||||
} else {
|
||||
"icons/disclosure-open.svg"
|
||||
})
|
||||
.with_color(header_style.text.color)
|
||||
.constrained()
|
||||
.with_max_width(icon_size)
|
||||
.with_max_height(icon_size)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(icon_size)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(text.to_string(), header_style.text.clone())
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
.with_margin_left(theme.contact_username.container.margin.left)
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(header_style.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_contact(
|
||||
contact: Arc<Contact>,
|
||||
theme: &theme::ContactsPanel,
|
||||
|
@ -507,8 +555,10 @@ impl ContactsPanel {
|
|||
}
|
||||
|
||||
if !request_entries.is_empty() {
|
||||
self.entries.push(ContactEntry::Header("Requests"));
|
||||
self.entries.append(&mut request_entries);
|
||||
self.entries.push(ContactEntry::Header(Section::Requests));
|
||||
if !self.collapsed_sections.contains(&Section::Requests) {
|
||||
self.entries.append(&mut request_entries);
|
||||
}
|
||||
}
|
||||
|
||||
let contacts = user_store.contacts();
|
||||
|
@ -538,22 +588,27 @@ impl ContactsPanel {
|
|||
.iter()
|
||||
.partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
|
||||
|
||||
for (matches, name) in [(online_contacts, "Online"), (offline_contacts, "Offline")] {
|
||||
for (matches, section) in [
|
||||
(online_contacts, Section::Online),
|
||||
(offline_contacts, Section::Offline),
|
||||
] {
|
||||
if !matches.is_empty() {
|
||||
self.entries.push(ContactEntry::Header(name));
|
||||
for mat in matches {
|
||||
let contact = &contacts[mat.candidate_id];
|
||||
self.entries.push(ContactEntry::Contact(contact.clone()));
|
||||
self.entries
|
||||
.extend(contact.projects.iter().enumerate().filter_map(
|
||||
|(ix, project)| {
|
||||
if project.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ContactEntry::ContactProject(contact.clone(), ix))
|
||||
}
|
||||
},
|
||||
));
|
||||
self.entries.push(ContactEntry::Header(section));
|
||||
if !self.collapsed_sections.contains(§ion) {
|
||||
for mat in matches {
|
||||
let contact = &contacts[mat.candidate_id];
|
||||
self.entries.push(ContactEntry::Contact(contact.clone()));
|
||||
self.entries
|
||||
.extend(contact.projects.iter().enumerate().filter_map(
|
||||
|(ix, project)| {
|
||||
if project.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ContactEntry::ContactProject(contact.clone(), ix))
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -624,6 +679,32 @@ impl ContactsPanel {
|
|||
cx.notify();
|
||||
self.list_state.reset(self.entries.len());
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some(selection) = self.selection {
|
||||
if let Some(entry) = self.entries.get(selection) {
|
||||
match entry {
|
||||
ContactEntry::Header(section) => {
|
||||
if let Some(ix) = self.collapsed_sections.iter().position(|s| s == section)
|
||||
{
|
||||
self.collapsed_sections.remove(ix);
|
||||
} else {
|
||||
self.collapsed_sections.push(*section);
|
||||
}
|
||||
self.update_entries(cx);
|
||||
}
|
||||
ContactEntry::ContactProject(contact, project_ix) => {
|
||||
cx.dispatch_global_action(JoinProject {
|
||||
project_id: contact.projects[*project_ix].id,
|
||||
app_state: self.app_state.clone(),
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
|
||||
|
@ -706,9 +787,9 @@ impl View for ContactsPanel {
|
|||
impl PartialEq for ContactEntry {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match self {
|
||||
ContactEntry::Header(name_1) => {
|
||||
if let ContactEntry::Header(name_2) = other {
|
||||
return name_1 == name_2;
|
||||
ContactEntry::Header(section_1) => {
|
||||
if let ContactEntry::Header(section_2) = other {
|
||||
return section_1 == section_2;
|
||||
}
|
||||
}
|
||||
ContactEntry::IncomingRequest(user_1) => {
|
||||
|
@ -816,7 +897,7 @@ mod tests {
|
|||
&[
|
||||
"+",
|
||||
"v Requests",
|
||||
" incoming user_one <=== selected",
|
||||
" incoming user_one",
|
||||
" outgoing user_two",
|
||||
"v Online",
|
||||
" user_four",
|
||||
|
@ -838,68 +919,81 @@ mod tests {
|
|||
render_to_strings(&panel, cx),
|
||||
&[
|
||||
"+",
|
||||
"Online",
|
||||
"v Online",
|
||||
" user_four",
|
||||
" dir2",
|
||||
"v Offline",
|
||||
" user_five",
|
||||
]
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.select_next(&Default::default(), cx);
|
||||
});
|
||||
assert_eq!(
|
||||
render_to_strings(&panel, cx),
|
||||
&[
|
||||
"+",
|
||||
"v Online <=== selected",
|
||||
" user_four",
|
||||
" dir2",
|
||||
"v Offline",
|
||||
" user_five",
|
||||
]
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.select_next(&Default::default(), cx);
|
||||
});
|
||||
assert_eq!(
|
||||
render_to_strings(&panel, cx),
|
||||
&[
|
||||
"+",
|
||||
"v Online",
|
||||
" user_four <=== selected",
|
||||
" dir2",
|
||||
"Offline",
|
||||
"v Offline",
|
||||
" user_five",
|
||||
]
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.select_next(&Default::default(), cx);
|
||||
});
|
||||
assert_eq!(
|
||||
render_to_strings(&panel, cx),
|
||||
&[
|
||||
"+",
|
||||
"Online",
|
||||
" user_four",
|
||||
" dir2 <=== selected",
|
||||
"Offline",
|
||||
" user_five",
|
||||
]
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.select_next(&Default::default(), cx);
|
||||
});
|
||||
assert_eq!(
|
||||
render_to_strings(&panel, cx),
|
||||
&[
|
||||
"+",
|
||||
"Online",
|
||||
" user_four",
|
||||
" dir2",
|
||||
"Offline",
|
||||
" user_five <=== selected",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn render_to_strings(panel: &ViewHandle<ContactsPanel>, cx: &TestAppContext) -> Vec<String> {
|
||||
panel.read_with(cx, |panel, _| {
|
||||
let mut entries = Vec::new();
|
||||
entries.push("+".to_string());
|
||||
entries.extend(panel.entries.iter().map(|entry| match entry {
|
||||
ContactEntry::Header(name) => {
|
||||
format!("{}", name)
|
||||
}
|
||||
ContactEntry::IncomingRequest(user) => {
|
||||
format!(" incoming {}", user.github_login)
|
||||
}
|
||||
ContactEntry::OutgoingRequest(user) => {
|
||||
format!(" outgoing {}", user.github_login)
|
||||
}
|
||||
ContactEntry::Contact(contact) => {
|
||||
format!(" {}", contact.user.github_login)
|
||||
}
|
||||
ContactEntry::ContactProject(contact, project_ix) => {
|
||||
format!(
|
||||
" {}",
|
||||
contact.projects[*project_ix].worktree_root_names.join(", ")
|
||||
)
|
||||
entries.extend(panel.entries.iter().enumerate().map(|(ix, entry)| {
|
||||
let mut string = match entry {
|
||||
ContactEntry::Header(name) => {
|
||||
let icon = if panel.collapsed_sections.contains(name) {
|
||||
">"
|
||||
} else {
|
||||
"v"
|
||||
};
|
||||
format!("{} {:?}", icon, name)
|
||||
}
|
||||
ContactEntry::IncomingRequest(user) => {
|
||||
format!(" incoming {}", user.github_login)
|
||||
}
|
||||
ContactEntry::OutgoingRequest(user) => {
|
||||
format!(" outgoing {}", user.github_login)
|
||||
}
|
||||
ContactEntry::Contact(contact) => {
|
||||
format!(" {}", contact.user.github_login)
|
||||
}
|
||||
ContactEntry::ContactProject(contact, project_ix) => {
|
||||
format!(
|
||||
" {}",
|
||||
contact.projects[*project_ix].worktree_root_names.join(", ")
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if panel.selection == Some(ix) {
|
||||
string.push_str(" <=== selected");
|
||||
}
|
||||
|
||||
string
|
||||
}));
|
||||
entries
|
||||
})
|
||||
|
|
|
@ -249,6 +249,7 @@ pub struct ContactsPanel {
|
|||
pub contact_button_spacing: f32,
|
||||
pub disabled_contact_button: IconButton,
|
||||
pub tree_branch: Interactive<TreeBranch>,
|
||||
pub section_icon_size: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone, Copy)]
|
||||
|
|
|
@ -68,6 +68,7 @@ export default function contactsPanel(theme: Theme) {
|
|||
iconWidth: 8,
|
||||
},
|
||||
rowHeight: 28,
|
||||
sectionIconSize: 8,
|
||||
headerRow: {
|
||||
...text(theme, "mono", "secondary", { size: "sm" }),
|
||||
margin: { top: 14 },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue