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
|
"icon_width": 8
|
||||||
},
|
},
|
||||||
"row_height": 28,
|
"row_height": 28,
|
||||||
|
"section_icon_size": 8,
|
||||||
"header_row": {
|
"header_row": {
|
||||||
"family": "Zed Mono",
|
"family": "Zed Mono",
|
||||||
"color": "#8b8792",
|
"color": "#8b8792",
|
||||||
|
|
|
@ -1251,6 +1251,7 @@
|
||||||
"icon_width": 8
|
"icon_width": 8
|
||||||
},
|
},
|
||||||
"row_height": 28,
|
"row_height": 28,
|
||||||
|
"section_icon_size": 8,
|
||||||
"header_row": {
|
"header_row": {
|
||||||
"family": "Zed Mono",
|
"family": "Zed Mono",
|
||||||
"color": "#585260",
|
"color": "#585260",
|
||||||
|
|
|
@ -1251,6 +1251,7 @@
|
||||||
"icon_width": 8
|
"icon_width": 8
|
||||||
},
|
},
|
||||||
"row_height": 28,
|
"row_height": 28,
|
||||||
|
"section_icon_size": 8,
|
||||||
"header_row": {
|
"header_row": {
|
||||||
"family": "Zed Mono",
|
"family": "Zed Mono",
|
||||||
"color": "#9c9c9c",
|
"color": "#9c9c9c",
|
||||||
|
|
|
@ -1251,6 +1251,7 @@
|
||||||
"icon_width": 8
|
"icon_width": 8
|
||||||
},
|
},
|
||||||
"row_height": 28,
|
"row_height": 28,
|
||||||
|
"section_icon_size": 8,
|
||||||
"header_row": {
|
"header_row": {
|
||||||
"family": "Zed Mono",
|
"family": "Zed Mono",
|
||||||
"color": "#474747",
|
"color": "#474747",
|
||||||
|
|
|
@ -1251,6 +1251,7 @@
|
||||||
"icon_width": 8
|
"icon_width": 8
|
||||||
},
|
},
|
||||||
"row_height": 28,
|
"row_height": 28,
|
||||||
|
"section_icon_size": 8,
|
||||||
"header_row": {
|
"header_row": {
|
||||||
"family": "Zed Mono",
|
"family": "Zed Mono",
|
||||||
"color": "#93a1a1",
|
"color": "#93a1a1",
|
||||||
|
|
|
@ -1251,6 +1251,7 @@
|
||||||
"icon_width": 8
|
"icon_width": 8
|
||||||
},
|
},
|
||||||
"row_height": 28,
|
"row_height": 28,
|
||||||
|
"section_icon_size": 8,
|
||||||
"header_row": {
|
"header_row": {
|
||||||
"family": "Zed Mono",
|
"family": "Zed Mono",
|
||||||
"color": "#586e75",
|
"color": "#586e75",
|
||||||
|
|
|
@ -1251,6 +1251,7 @@
|
||||||
"icon_width": 8
|
"icon_width": 8
|
||||||
},
|
},
|
||||||
"row_height": 28,
|
"row_height": 28,
|
||||||
|
"section_icon_size": 8,
|
||||||
"header_row": {
|
"header_row": {
|
||||||
"family": "Zed Mono",
|
"family": "Zed Mono",
|
||||||
"color": "#979db4",
|
"color": "#979db4",
|
||||||
|
|
|
@ -1251,6 +1251,7 @@
|
||||||
"icon_width": 8
|
"icon_width": 8
|
||||||
},
|
},
|
||||||
"row_height": 28,
|
"row_height": 28,
|
||||||
|
"section_icon_size": 8,
|
||||||
"header_row": {
|
"header_row": {
|
||||||
"family": "Zed Mono",
|
"family": "Zed Mono",
|
||||||
"color": "#5e6687",
|
"color": "#5e6687",
|
||||||
|
|
|
@ -15,7 +15,7 @@ use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme::IconButton;
|
use theme::IconButton;
|
||||||
use workspace::menu::{SelectNext, SelectPrev};
|
use workspace::menu::{Confirm, SelectNext, SelectPrev};
|
||||||
use workspace::{AppState, JoinProject};
|
use workspace::{AppState, JoinProject};
|
||||||
|
|
||||||
impl_actions!(
|
impl_actions!(
|
||||||
|
@ -23,9 +23,16 @@ impl_actions!(
|
||||||
[RequestContact, RemoveContact, RespondToContactRequest]
|
[RequestContact, RemoveContact, RespondToContactRequest]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
|
||||||
|
enum Section {
|
||||||
|
Requests,
|
||||||
|
Online,
|
||||||
|
Offline,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum ContactEntry {
|
enum ContactEntry {
|
||||||
Header(&'static str),
|
Header(Section),
|
||||||
IncomingRequest(Arc<User>),
|
IncomingRequest(Arc<User>),
|
||||||
OutgoingRequest(Arc<User>),
|
OutgoingRequest(Arc<User>),
|
||||||
Contact(Arc<Contact>),
|
Contact(Arc<Contact>),
|
||||||
|
@ -38,7 +45,9 @@ pub struct ContactsPanel {
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
user_store: ModelHandle<UserStore>,
|
user_store: ModelHandle<UserStore>,
|
||||||
filter_editor: ViewHandle<Editor>,
|
filter_editor: ViewHandle<Editor>,
|
||||||
|
collapsed_sections: Vec<Section>,
|
||||||
selection: Option<usize>,
|
selection: Option<usize>,
|
||||||
|
app_state: Arc<AppState>,
|
||||||
_maintain_contacts: Subscription,
|
_maintain_contacts: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +71,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(ContactsPanel::clear_filter);
|
cx.add_action(ContactsPanel::clear_filter);
|
||||||
cx.add_action(ContactsPanel::select_next);
|
cx.add_action(ContactsPanel::select_next);
|
||||||
cx.add_action(ContactsPanel::select_prev);
|
cx.add_action(ContactsPanel::select_prev);
|
||||||
|
cx.add_action(ContactsPanel::confirm);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContactsPanel {
|
impl ContactsPanel {
|
||||||
|
@ -97,18 +107,9 @@ impl ContactsPanel {
|
||||||
let is_selected = this.selection == Some(ix);
|
let is_selected = this.selection == Some(ix);
|
||||||
|
|
||||||
match &this.entries[ix] {
|
match &this.entries[ix] {
|
||||||
ContactEntry::Header(text) => {
|
ContactEntry::Header(section) => {
|
||||||
let header_style =
|
let is_collapsed = this.collapsed_sections.contains(§ion);
|
||||||
theme.header_row.style_for(&Default::default(), is_selected);
|
Self::render_header(*section, theme, is_selected, is_collapsed)
|
||||||
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::IncomingRequest(user) => Self::render_contact_request(
|
ContactEntry::IncomingRequest(user) => Self::render_contact_request(
|
||||||
user.clone(),
|
user.clone(),
|
||||||
|
@ -153,17 +154,64 @@ impl ContactsPanel {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
selection: None,
|
selection: None,
|
||||||
|
collapsed_sections: Default::default(),
|
||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
match_candidates: Default::default(),
|
match_candidates: Default::default(),
|
||||||
filter_editor: user_query_editor,
|
filter_editor: user_query_editor,
|
||||||
_maintain_contacts: cx
|
_maintain_contacts: cx
|
||||||
.observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)),
|
.observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)),
|
||||||
user_store: app_state.user_store.clone(),
|
user_store: app_state.user_store.clone(),
|
||||||
|
app_state,
|
||||||
};
|
};
|
||||||
this.update_entries(cx);
|
this.update_entries(cx);
|
||||||
this
|
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(
|
fn render_contact(
|
||||||
contact: Arc<Contact>,
|
contact: Arc<Contact>,
|
||||||
theme: &theme::ContactsPanel,
|
theme: &theme::ContactsPanel,
|
||||||
|
@ -507,8 +555,10 @@ impl ContactsPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !request_entries.is_empty() {
|
if !request_entries.is_empty() {
|
||||||
self.entries.push(ContactEntry::Header("Requests"));
|
self.entries.push(ContactEntry::Header(Section::Requests));
|
||||||
self.entries.append(&mut request_entries);
|
if !self.collapsed_sections.contains(&Section::Requests) {
|
||||||
|
self.entries.append(&mut request_entries);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let contacts = user_store.contacts();
|
let contacts = user_store.contacts();
|
||||||
|
@ -538,22 +588,27 @@ impl ContactsPanel {
|
||||||
.iter()
|
.iter()
|
||||||
.partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
|
.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() {
|
if !matches.is_empty() {
|
||||||
self.entries.push(ContactEntry::Header(name));
|
self.entries.push(ContactEntry::Header(section));
|
||||||
for mat in matches {
|
if !self.collapsed_sections.contains(§ion) {
|
||||||
let contact = &contacts[mat.candidate_id];
|
for mat in matches {
|
||||||
self.entries.push(ContactEntry::Contact(contact.clone()));
|
let contact = &contacts[mat.candidate_id];
|
||||||
self.entries
|
self.entries.push(ContactEntry::Contact(contact.clone()));
|
||||||
.extend(contact.projects.iter().enumerate().filter_map(
|
self.entries
|
||||||
|(ix, project)| {
|
.extend(contact.projects.iter().enumerate().filter_map(
|
||||||
if project.worktree_root_names.is_empty() {
|
|(ix, project)| {
|
||||||
None
|
if project.worktree_root_names.is_empty() {
|
||||||
} else {
|
None
|
||||||
Some(ContactEntry::ContactProject(contact.clone(), ix))
|
} else {
|
||||||
}
|
Some(ContactEntry::ContactProject(contact.clone(), ix))
|
||||||
},
|
}
|
||||||
));
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -624,6 +679,32 @@ impl ContactsPanel {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
self.list_state.reset(self.entries.len());
|
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 {
|
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
|
||||||
|
@ -706,9 +787,9 @@ impl View for ContactsPanel {
|
||||||
impl PartialEq for ContactEntry {
|
impl PartialEq for ContactEntry {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
ContactEntry::Header(name_1) => {
|
ContactEntry::Header(section_1) => {
|
||||||
if let ContactEntry::Header(name_2) = other {
|
if let ContactEntry::Header(section_2) = other {
|
||||||
return name_1 == name_2;
|
return section_1 == section_2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContactEntry::IncomingRequest(user_1) => {
|
ContactEntry::IncomingRequest(user_1) => {
|
||||||
|
@ -816,7 +897,7 @@ mod tests {
|
||||||
&[
|
&[
|
||||||
"+",
|
"+",
|
||||||
"v Requests",
|
"v Requests",
|
||||||
" incoming user_one <=== selected",
|
" incoming user_one",
|
||||||
" outgoing user_two",
|
" outgoing user_two",
|
||||||
"v Online",
|
"v Online",
|
||||||
" user_four",
|
" user_four",
|
||||||
|
@ -838,68 +919,81 @@ mod tests {
|
||||||
render_to_strings(&panel, cx),
|
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",
|
" user_four <=== selected",
|
||||||
" dir2",
|
" dir2",
|
||||||
"Offline",
|
"v Offline",
|
||||||
" user_five",
|
" 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> {
|
fn render_to_strings(panel: &ViewHandle<ContactsPanel>, cx: &TestAppContext) -> Vec<String> {
|
||||||
panel.read_with(cx, |panel, _| {
|
panel.read_with(cx, |panel, _| {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
entries.push("+".to_string());
|
entries.push("+".to_string());
|
||||||
entries.extend(panel.entries.iter().map(|entry| match entry {
|
entries.extend(panel.entries.iter().enumerate().map(|(ix, entry)| {
|
||||||
ContactEntry::Header(name) => {
|
let mut string = match entry {
|
||||||
format!("{}", name)
|
ContactEntry::Header(name) => {
|
||||||
}
|
let icon = if panel.collapsed_sections.contains(name) {
|
||||||
ContactEntry::IncomingRequest(user) => {
|
">"
|
||||||
format!(" incoming {}", user.github_login)
|
} else {
|
||||||
}
|
"v"
|
||||||
ContactEntry::OutgoingRequest(user) => {
|
};
|
||||||
format!(" outgoing {}", user.github_login)
|
format!("{} {:?}", icon, name)
|
||||||
}
|
}
|
||||||
ContactEntry::Contact(contact) => {
|
ContactEntry::IncomingRequest(user) => {
|
||||||
format!(" {}", contact.user.github_login)
|
format!(" incoming {}", user.github_login)
|
||||||
}
|
}
|
||||||
ContactEntry::ContactProject(contact, project_ix) => {
|
ContactEntry::OutgoingRequest(user) => {
|
||||||
format!(
|
format!(" outgoing {}", user.github_login)
|
||||||
" {}",
|
}
|
||||||
contact.projects[*project_ix].worktree_root_names.join(", ")
|
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
|
entries
|
||||||
})
|
})
|
||||||
|
|
|
@ -249,6 +249,7 @@ pub struct ContactsPanel {
|
||||||
pub contact_button_spacing: f32,
|
pub contact_button_spacing: f32,
|
||||||
pub disabled_contact_button: IconButton,
|
pub disabled_contact_button: IconButton,
|
||||||
pub tree_branch: Interactive<TreeBranch>,
|
pub tree_branch: Interactive<TreeBranch>,
|
||||||
|
pub section_icon_size: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default, Clone, Copy)]
|
#[derive(Deserialize, Default, Clone, Copy)]
|
||||||
|
|
|
@ -68,6 +68,7 @@ export default function contactsPanel(theme: Theme) {
|
||||||
iconWidth: 8,
|
iconWidth: 8,
|
||||||
},
|
},
|
||||||
rowHeight: 28,
|
rowHeight: 28,
|
||||||
|
sectionIconSize: 8,
|
||||||
headerRow: {
|
headerRow: {
|
||||||
...text(theme, "mono", "secondary", { size: "sm" }),
|
...text(theme, "mono", "secondary", { size: "sm" }),
|
||||||
margin: { top: 14 },
|
margin: { top: 14 },
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue