diff --git a/radicle-tui/src/app/event.rs b/radicle-tui/src/app/event.rs index 3b0e8e57..a6719b35 100644 --- a/radicle-tui/src/app/event.rs +++ b/radicle-tui/src/app/event.rs @@ -68,13 +68,28 @@ impl tuirealm::Component for Widget { } } -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None +impl tuirealm::Component for Widget { + fn on(&mut self, event: Event) -> Option { + match event { + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(MoveDirection::Up)); + Some(Message::Tick) + } + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(MoveDirection::Down)); + Some(Message::Tick) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Message::Tick), + _ => None, + } } } -impl tuirealm::Component for Widget { +impl tuirealm::Component for Widget { fn on(&mut self, _event: Event) -> Option { None } diff --git a/radicle-tui/src/app/page.rs b/radicle-tui/src/app/page.rs index 3ced38dc..c2d481fc 100644 --- a/radicle-tui/src/app/page.rs +++ b/radicle-tui/src/app/page.rs @@ -65,7 +65,7 @@ impl ViewPage for HomeView { let navigation = widget::home::navigation(theme).to_boxed(); let dashboard = widget::home::dashboard(theme, &context.id, &context.project).to_boxed(); - let issue_browser = widget::home::issues(theme).to_boxed(); + let issue_browser = widget::home::issues(theme, &context.id, &context.profile).to_boxed(); let patch_browser = widget::home::patches(theme, &context.id, &context.profile).to_boxed(); app.remount( diff --git a/radicle-tui/src/ui/cob.rs b/radicle-tui/src/ui/cob.rs index 0351b3e0..e5ae1729 100644 --- a/radicle-tui/src/ui/cob.rs +++ b/radicle-tui/src/ui/cob.rs @@ -8,8 +8,9 @@ use radicle::storage::git::Repository; use radicle::storage::{Oid, ReadRepository}; use radicle::Profile; -use radicle::cob::patch::{Patch, PatchId, State}; -use radicle::cob::Timestamp; +use radicle::cob::issue::{Issue, IssueId, State as IssueState}; +use radicle::cob::patch::{Patch, PatchId, State as PatchState}; +use radicle::cob::{Tag, Timestamp}; use tuirealm::props::{Color, Style}; use tuirealm::tui::widgets::Cell; @@ -38,7 +39,7 @@ pub struct PatchItem { /// Patch OID. id: PatchId, /// Patch state. - state: State, + state: PatchState, /// Patch title. title: String, /// Author of the latest revision. @@ -88,17 +89,17 @@ impl TryFrom<(&Profile, &Repository, PatchId, Patch)> for PatchItem { impl TableItem<8> for PatchItem { fn row(&self, theme: &Theme) -> [Cell; 8] { - let (icon, color) = format_state(&self.state); + let (icon, color) = format_patch_state(&self.state); let state = Cell::from(icon).style(Style::default().fg(color)); let id = Cell::from(format::cob(&self.id)) - .style(Style::default().fg(theme.colors.browser_patch_list_id)); + .style(Style::default().fg(theme.colors.browser_list_id)); let title = Cell::from(self.title.clone()) - .style(Style::default().fg(theme.colors.browser_patch_list_title)); + .style(Style::default().fg(theme.colors.browser_list_title)); let author = Cell::from(format_author(&self.author.did, self.author.is_you)) - .style(Style::default().fg(theme.colors.browser_patch_list_author)); + .style(Style::default().fg(theme.colors.browser_list_author)); let head = Cell::from(format::oid(self.head)) .style(Style::default().fg(theme.colors.browser_patch_list_head)); @@ -110,24 +111,105 @@ impl TableItem<8> for PatchItem { .style(Style::default().fg(theme.colors.browser_patch_list_removed)); let updated = Cell::from(format::timestamp(&self.timestamp).to_string()) - .style(Style::default().fg(theme.colors.browser_patch_list_timestamp)); + .style(Style::default().fg(theme.colors.browser_list_timestamp)); [state, id, title, author, head, added, removed, updated] } } +/// An issue item that can be used in tables, list or trees. +/// +/// Breaks up dependencies to [`Profile`] and [`Repository`] that +/// would be needed if [`Issue`] would be used directly. +#[derive(Clone)] +pub struct IssueItem { + /// Issue OID. + id: IssueId, + /// Issue state. + state: IssueState, + /// Issue title. + title: String, + /// Issue author. + author: AuthorItem, + /// Issue tags. + tags: Vec, + /// Issue assignees. + assignees: Vec, + /// Time when issue was opened. + timestamp: Timestamp, +} + +impl TryFrom<(&Profile, &Repository, IssueId, Issue)> for IssueItem { + type Error = anyhow::Error; + + fn try_from(value: (&Profile, &Repository, IssueId, Issue)) -> Result { + let (profile, _, id, issue) = value; + + Ok(IssueItem { + id, + state: *issue.state(), + title: issue.title().into(), + author: AuthorItem { + did: issue.author().id, + is_you: *issue.author().id == *profile.did(), + }, + tags: issue.tags().cloned().collect(), + assignees: issue + .assigned() + .map(|did| AuthorItem { + did, + is_you: did == profile.did(), + }) + .collect::>(), + timestamp: issue.timestamp(), + }) + } +} + +impl TableItem<7> for IssueItem { + fn row(&self, theme: &Theme) -> [Cell; 7] { + let (icon, color) = format_issue_state(&self.state); + let state = Cell::from(icon).style(Style::default().fg(color)); + + let id = Cell::from(format::cob(&self.id)) + .style(Style::default().fg(theme.colors.browser_list_id)); + + let title = Cell::from(self.title.clone()) + .style(Style::default().fg(theme.colors.browser_list_title)); + + let author = Cell::from(format_author(&self.author.did, self.author.is_you)) + .style(Style::default().fg(theme.colors.browser_list_author)); + + let tags = Cell::from(format_tags(&self.tags)) + .style(Style::default().fg(theme.colors.browser_list_tags)); + + let assignees = self + .assignees + .iter() + .map(|author| (author.did, author.is_you)) + .collect::>(); + let assignees = Cell::from(format_assignees(&assignees)) + .style(Style::default().fg(theme.colors.browser_list_author)); + + let opened = Cell::from(format::timestamp(&self.timestamp).to_string()) + .style(Style::default().fg(theme.colors.browser_list_timestamp)); + + [state, id, title, author, tags, assignees, opened] + } +} + impl TableItem<1> for () { fn row(&self, _theme: &Theme) -> [Cell; 1] { [Cell::default()] } } -pub fn format_state(state: &State) -> (String, Color) { +pub fn format_patch_state(state: &PatchState) -> (String, Color) { match state { - State::Open { conflicts: _ } => (" ● ".into(), Color::Green), - State::Archived => (" ● ".into(), Color::Yellow), - State::Draft => (" ● ".into(), Color::Gray), - State::Merged { + PatchState::Open { conflicts: _ } => (" ● ".into(), Color::Green), + PatchState::Archived => (" ● ".into(), Color::Yellow), + PatchState::Draft => (" ● ".into(), Color::Gray), + PatchState::Merged { revision: _, commit: _, } => (" ✔ ".into(), Color::Blue), @@ -141,3 +223,38 @@ pub fn format_author(did: &Did, is_you: bool) -> String { format!("{}", format::did(did)) } } + +pub fn format_issue_state(state: &IssueState) -> (String, Color) { + match state { + IssueState::Open => (" ● ".into(), Color::Green), + IssueState::Closed { reason: _ } => (" ● ".into(), Color::Red), + } +} + +pub fn format_tags(tags: &[Tag]) -> String { + let mut output = String::new(); + let mut tags = tags.iter().peekable(); + + while let Some(tag) = tags.next() { + output.push_str(&tag.to_string()); + + if tags.peek().is_some() { + output.push(','); + } + } + output +} + +pub fn format_assignees(assignees: &[(Did, bool)]) -> String { + let mut output = String::new(); + let mut assignees = assignees.iter().peekable(); + + while let Some((assignee, is_you)) = assignees.next() { + output.push_str(&format_author(assignee, *is_you)); + + if assignees.peek().is_some() { + output.push(','); + } + } + output +} diff --git a/radicle-tui/src/ui/theme.rs b/radicle-tui/src/ui/theme.rs index aa30b711..7e8a31c1 100644 --- a/radicle-tui/src/ui/theme.rs +++ b/radicle-tui/src/ui/theme.rs @@ -19,15 +19,15 @@ pub struct Colors { pub shortcut_short_fg: Color, pub shortcut_long_fg: Color, pub shortcutbar_divider_fg: Color, - pub browser_patch_list_id: Color, - pub browser_patch_list_title: Color, - pub browser_patch_list_author: Color, + pub browser_list_id: Color, + pub browser_list_title: Color, + pub browser_list_author: Color, + pub browser_list_tags: Color, + pub browser_list_comments: Color, + pub browser_list_timestamp: Color, pub browser_patch_list_head: Color, pub browser_patch_list_added: Color, pub browser_patch_list_removed: Color, - pub browser_patch_list_tags: Color, - pub browser_patch_list_comments: Color, - pub browser_patch_list_timestamp: Color, pub context_bg: Color, pub context_light_bg: Color, pub context_badge_bg: Color, @@ -86,15 +86,15 @@ pub fn default_dark() -> Theme { shortcut_short_fg: COLOR_DEFAULT_DARK, shortcut_long_fg: COLOR_DEFAULT_DARKER, shortcutbar_divider_fg: COLOR_DEFAULT_DARKER, - browser_patch_list_id: Color::Cyan, - browser_patch_list_title: COLOR_DEFAULT_FG, - browser_patch_list_author: Color::Gray, + browser_list_id: Color::Cyan, + browser_list_title: COLOR_DEFAULT_FG, + browser_list_author: Color::Gray, + browser_list_tags: Color::LightBlue, + browser_list_comments: COLOR_DEFAULT_DARK_FG, + browser_list_timestamp: COLOR_DEFAULT_DARK, browser_patch_list_head: Color::LightBlue, browser_patch_list_added: Color::Green, browser_patch_list_removed: Color::Red, - browser_patch_list_tags: Color::Yellow, - browser_patch_list_comments: COLOR_DEFAULT_DARK_FG, - browser_patch_list_timestamp: COLOR_DEFAULT_DARK, context_bg: COLOR_DEFAULT_DARKEST, context_light_bg: Color::Gray, context_badge_bg: Color::LightRed, diff --git a/radicle-tui/src/ui/widget/home.rs b/radicle-tui/src/ui/widget/home.rs index fbaf7a15..b8a2c5c2 100644 --- a/radicle-tui/src/ui/widget/home.rs +++ b/radicle-tui/src/ui/widget/home.rs @@ -1,3 +1,4 @@ +use radicle::cob::issue::Issues; use radicle::prelude::{Id, Project}; use radicle::storage::ReadStorage; use radicle::Profile; @@ -11,12 +12,11 @@ use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State}; use super::common; use super::common::container::{LabeledContainer, Tabs}; use super::common::context::Shortcuts; -use super::common::label::Label; use super::common::list::{ColumnWidth, Table}; use super::{Widget, WidgetComponent}; -use crate::ui::cob::PatchItem; +use crate::ui::cob::{IssueItem, PatchItem}; use crate::ui::layout; use crate::ui::theme::Theme; @@ -54,23 +54,61 @@ impl WidgetComponent for Dashboard { } pub struct IssueBrowser { - label: Widget