use super::*;
use crate::fuzzy;
impl App {
fn show_popup(&mut self, popup: Popup, index: usize) {
self.view.popup = popup;
self.view.popup_index = index;
self.view.popup_query.clear();
}
pub fn open_team_select(&mut self) {
self.show_popup(Popup::TeamSelect, self.nav.team);
}
pub fn open_workspace_select(&mut self) {
if self.workspaces.len() < 2 {
self.set_status(
"Only one workspace is signed in \u{2014} `linear-tui auth login` adds another",
);
return;
}
let current = self.workspaces.iter().position(|w| w.current).unwrap_or(0);
self.show_popup(Popup::WorkspaceSelect, current);
}
pub fn select_workspace(&mut self) {
let Some(entry) = self.popup_choice().and_then(|i| self.workspaces.get(i)) else {
return;
};
let (id, current, name) = (entry.id.clone(), entry.current, entry.name.clone());
self.close_popup();
if !current {
self.set_status(format!("Switching to {name}\u{2026}"));
self.switch_to = Some(id);
}
}
pub fn select_team(&mut self) {
let Some(index) = self.popup_choice() else {
return;
};
self.close_popup();
let section = match self.nav.dest {
Nav::Team(_, section) => section,
_ => TeamSection::Issues,
};
self.activate(Nav::Team(index, section));
}
pub fn open_filter(&mut self) {
for team_id in self.list_team_ids() {
self.ensure_team_context(team_id);
}
self.show_popup(Popup::Filter(FilterKind::Status), 0);
}
pub fn open_group_by(&mut self) {
let index = GroupBy::ALL
.iter()
.position(|g| *g == self.view.group_by)
.unwrap_or(0);
self.show_popup(Popup::GroupBy, index);
}
pub fn open_status_change(&mut self) {
if let Some(issue) = self.focused_issue() {
self.open_change_popup(Popup::StatusChange(issue.id.clone()));
}
}
pub fn open_priority_change(&mut self) {
if let Some(issue) = self.focused_issue() {
let index = issue.priority.as_index();
self.show_popup(Popup::PriorityChange(issue.id.clone()), index);
}
}
pub fn open_assignee_change(&mut self) {
if let Some(issue) = self.focused_issue() {
self.open_change_popup(Popup::AssigneeChange(issue.id.clone()));
}
}
fn open_change_popup(&mut self, popup: Popup) {
self.show_popup(popup, 0);
if let Some(team_id) = self.popup_team_id().cloned() {
self.ensure_team_context(team_id);
}
self.view.popup_index = self.popup_initial_index();
}
pub(super) fn popup_initial_index(&self) -> usize {
let Some(issue) = self.popup_issue() else {
return 0;
};
if !self.view.popup_query.is_empty() {
return 0;
}
match self.view.popup {
Popup::StatusChange(_) => issue
.state
.as_ref()
.and_then(|state| self.popup_states().iter().position(|s| s.id == state.id))
.unwrap_or(0),
Popup::AssigneeChange(_) => issue
.assignee
.as_ref()
.and_then(|user| self.popup_members().iter().position(|u| u.id == user.id))
.map_or(0, |i| i + 1),
_ => 0,
}
}
pub fn issue_team_id<'a>(&'a self, issue: &'a Issue) -> Option<&'a TeamId> {
self.store
.issue_team_id(issue, self.current_team().map(|t| &t.id))
}
pub(super) fn ensure_team_context(&mut self, team_id: TeamId) {
if self.store.team_contexts.contains_key(&team_id)
|| !self.outbox.team_contexts.insert(team_id.clone())
{
return;
}
self.request(Request::TeamContext { team_id });
}
pub(super) fn popup_team_id(&self) -> Option<&TeamId> {
match self.view.popup {
Popup::StatusChange(_) | Popup::AssigneeChange(_) => {
self.issue_team_id(self.popup_issue()?)
}
_ => None,
}
}
fn popup_context(&self) -> Option<&TeamContext> {
self.store.team_contexts.get(self.popup_team_id()?)
}
pub fn popup_loading(&self) -> bool {
match self.view.popup {
Popup::StatusChange(_) | Popup::AssigneeChange(_) => self.popup_context().is_none(),
Popup::Filter(FilterKind::Status) => self
.list_team_ids()
.iter()
.any(|id| !self.store.team_contexts.contains_key(id)),
_ => false,
}
}
pub fn popup_states(&self) -> &[WorkflowState] {
self.popup_context().map_or(&[], |c| &c.states)
}
pub fn popup_members(&self) -> &[User] {
self.popup_context().map_or(&[], |c| &c.members)
}
fn list_team_ids(&self) -> Vec<TeamId> {
let mut ids: Vec<TeamId> = Vec::new();
for issue in &self.store.issues[self.issue_source()].items {
if let Some(id) = self.issue_team_id(issue)
&& !ids.contains(id)
{
ids.push(id.clone());
}
}
if ids.is_empty() {
ids.extend(self.team_id());
}
ids
}
pub fn filter_states(&self) -> Vec<&WorkflowState> {
let mut states: Vec<&WorkflowState> = Vec::new();
for id in self.list_team_ids() {
let Some(context) = self.store.team_contexts.get(&id) else {
continue;
};
for state in &context.states {
if !states.iter().any(|s| s.name == state.name) {
states.push(state);
}
}
}
states
}
pub fn apply_popup(&mut self) {
match self.view.popup {
Popup::TeamSelect => self.select_team(),
Popup::WorkspaceSelect => self.select_workspace(),
Popup::Filter(_) => self.apply_filter_selection(),
Popup::StatusChange(_) => self.apply_status_selection(),
Popup::PriorityChange(_) => self.apply_priority_selection(),
Popup::AssigneeChange(_) => self.apply_assignee_selection(),
Popup::GroupBy => self.apply_group_by_selection(),
Popup::Palette | Popup::None => {}
}
}
pub fn apply_status_selection(&mut self) {
let Some(state) = self
.popup_choice()
.and_then(|i| self.popup_states().get(i))
.cloned()
else {
return;
};
if let Popup::StatusChange(issue_id) = self.take_popup() {
let request = usecase::issue::set_status(&mut self.store, &issue_id, state);
self.request(request);
}
}
pub fn apply_priority_selection(&mut self) {
let Some(index) = self.popup_choice() else {
return;
};
if let Popup::PriorityChange(issue_id) = self.take_popup() {
let priority = Priority::from_index(index);
let request = usecase::issue::set_priority(&mut self.store, &issue_id, priority);
self.request(request);
}
}
pub fn apply_assignee_selection(&mut self) {
let assignee = match self.popup_choice() {
None => return,
Some(0) => None, Some(i) => match self.popup_members().get(i - 1) {
Some(member) => Some(member.clone()),
None => return,
},
};
if let Popup::AssigneeChange(issue_id) = self.take_popup() {
let request = usecase::issue::set_assignee(&mut self.store, &issue_id, assignee);
self.request(request);
}
}
pub fn apply_filter_selection(&mut self) {
let Popup::Filter(kind) = self.view.popup else {
return;
};
let Some(choice) = self.popup_choice() else {
return;
};
match kind {
FilterKind::Status => {
if choice == 0 {
self.list_mut().filters.status = None;
} else if let Some(state) = self.filter_states().get(choice - 1) {
let name = state.name.clone();
self.list_mut().filters.status = Some(name);
}
self.view.popup = Popup::Filter(FilterKind::Priority);
self.view.popup_index = 0;
self.view.popup_query.clear();
}
FilterKind::Priority => {
let priority = match choice {
0 => None,
n => Some(Priority::from_index(n)),
};
self.list_mut().filters.priority = priority;
self.close_popup();
*self.selected_index_mut() = 0;
}
}
}
pub fn apply_group_by_selection(&mut self) {
let Some(group_by) = self.popup_choice().and_then(|i| GroupBy::ALL.get(i)) else {
return;
};
let group_by = *group_by;
self.close_popup();
self.set_group_by(group_by);
}
pub fn clear_filters(&mut self) {
self.list_mut().filters.clear();
*self.selected_index_mut() = 0;
}
pub fn close_popup(&mut self) {
self.view.popup = Popup::None;
self.view.popup_from_palette = false;
}
pub fn popup_escape(&mut self) {
if self.view.popup_from_palette {
self.open_palette();
} else {
self.close_popup();
}
}
pub fn popup_type(&mut self, c: char) {
self.edit_popup_query(|q| q.insert(c));
}
pub fn popup_erase(&mut self) {
if self.view.popup_query.is_empty() {
if self.view.popup_from_palette {
self.open_palette();
}
return;
}
self.edit_popup_query(Input::backspace);
}
pub fn edit_popup_query(&mut self, change: impl FnOnce(&mut Input)) {
change(&mut self.view.popup_query);
self.view.popup_index = 0;
}
fn popup_labels(&self) -> Vec<String> {
let priorities = |any: bool| {
let any = any.then(|| "Any priority".to_string());
let levels = (1..=5).map(|i| Priority::from_index(i).label().to_string());
any.into_iter().chain(levels)
};
match &self.view.popup {
Popup::TeamSelect => self
.store
.teams
.iter()
.map(|t| format!("{} {}", t.name, t.key))
.collect(),
Popup::WorkspaceSelect => self
.workspaces
.iter()
.map(|w| format!("{} {}", w.name, w.url_key))
.collect(),
Popup::Filter(FilterKind::Status) => std::iter::once("Any status".to_string())
.chain(self.filter_states().iter().map(|s| s.name.clone()))
.collect(),
Popup::Filter(FilterKind::Priority) => priorities(true).collect(),
Popup::StatusChange(_) => self.popup_states().iter().map(|s| s.name.clone()).collect(),
Popup::PriorityChange(_) => Priority::ALL
.iter()
.map(|p| p.label().to_string())
.collect(),
Popup::AssigneeChange(_) => std::iter::once("No assignee unassign".to_string())
.chain(self.popup_members().iter().map(|u| {
let display = u.display_name.as_deref().unwrap_or_default();
format!("{} {display}", u.name)
}))
.collect(),
Popup::GroupBy => GroupBy::ALL.iter().map(|g| g.label().to_string()).collect(),
Popup::Palette | Popup::None => Vec::new(),
}
}
pub fn popup_rows(&self) -> Vec<usize> {
let labels = self.popup_labels();
let query = self.view.popup_query.value.trim();
if query.is_empty() {
return (0..labels.len()).collect();
}
let mut scored: Vec<(i32, usize)> = labels
.iter()
.enumerate()
.filter_map(|(i, label)| fuzzy::score(query, label).map(|m| (m.score, i)))
.collect();
scored.sort_by_key(|(score, _)| -score);
scored.into_iter().map(|(_, i)| i).collect()
}
fn popup_choice(&self) -> Option<usize> {
self.popup_rows().get(self.view.popup_index).copied()
}
pub fn popup_issue(&self) -> Option<&Issue> {
let id = match &self.view.popup {
Popup::StatusChange(id) | Popup::PriorityChange(id) | Popup::AssigneeChange(id) => id,
_ => return None,
};
self.store.issue(id)
}
fn take_popup(&mut self) -> Popup {
self.view.popup_from_palette = false;
std::mem::replace(&mut self.view.popup, Popup::None)
}
pub fn popup_next(&mut self) {
let max = self.popup_list_len();
if max > 0 && self.view.popup_index < max - 1 {
self.view.popup_index += 1;
}
}
pub fn popup_prev(&mut self) {
if self.view.popup_index > 0 {
self.view.popup_index -= 1;
}
}
pub fn popup_first(&mut self) {
self.view.popup_index = 0;
}
pub fn popup_last(&mut self) {
self.view.popup_index = self.popup_list_len().saturating_sub(1);
}
pub fn popup_pick(&mut self, index: usize) {
if index < self.popup_list_len() {
self.view.popup_index = index;
self.apply_popup();
}
}
pub fn popup_list_len(&self) -> usize {
self.popup_rows().len()
}
}