tui: List issues
This commit is contained in:
parent
5f731fad0e
commit
56a61b61c9
|
|
@ -68,13 +68,28 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<PatchBrowser> {
|
|||
}
|
||||
}
|
||||
|
||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<Dashboard> {
|
||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
||||
None
|
||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<IssueBrowser> {
|
||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||
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<Message, NoUserEvent> for Widget<IssueBrowser> {
|
||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<Dashboard> {
|
||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Tag>,
|
||||
/// Issue assignees.
|
||||
assignees: Vec<AuthorItem>,
|
||||
/// 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<Self, Self::Error> {
|
||||
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::<Vec<_>>(),
|
||||
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::<Vec<_>>();
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Label>,
|
||||
table: Widget<Table<IssueItem, 7>>,
|
||||
shortcuts: Widget<Shortcuts>,
|
||||
}
|
||||
|
||||
impl IssueBrowser {
|
||||
pub fn new(label: Widget<Label>, shortcuts: Widget<Shortcuts>) -> Self {
|
||||
Self { label, shortcuts }
|
||||
pub fn new(theme: &Theme, profile: &Profile, id: &Id, shortcuts: Widget<Shortcuts>) -> Self {
|
||||
let repo = profile.storage.repository(*id).unwrap();
|
||||
let issues = Issues::open(&repo)
|
||||
.and_then(|issues| issues.all().map(|iter| iter.flatten().collect::<Vec<_>>()));
|
||||
|
||||
let header = [
|
||||
common::label(" ● "),
|
||||
common::label("ID"),
|
||||
common::label("Title"),
|
||||
common::label("Author"),
|
||||
common::label("Tags"),
|
||||
common::label("Assignees"),
|
||||
common::label("Opened"),
|
||||
];
|
||||
|
||||
let widths = [
|
||||
ColumnWidth::Fixed(3),
|
||||
ColumnWidth::Fixed(7),
|
||||
ColumnWidth::Grow,
|
||||
ColumnWidth::Fixed(21),
|
||||
ColumnWidth::Fixed(25),
|
||||
ColumnWidth::Fixed(21),
|
||||
ColumnWidth::Fixed(18),
|
||||
];
|
||||
|
||||
let mut items = vec![];
|
||||
if let Ok(mut issues) = issues {
|
||||
issues.sort_by(|(_, a, _), (_, b, _)| b.timestamp().cmp(&a.timestamp()));
|
||||
issues.sort_by(|(_, a, _), (_, b, _)| a.state().cmp(b.state()));
|
||||
|
||||
for (id, patch, _) in issues {
|
||||
if let Ok(item) = IssueItem::try_from((profile, &repo, id, patch)) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let table = Widget::new(Table::new(&items, header, widths, theme.clone()))
|
||||
.highlight(theme.colors.item_list_highlighted_bg);
|
||||
|
||||
Self { table, shortcuts }
|
||||
}
|
||||
|
||||
pub fn selected_item(&self) -> Option<&IssueItem> {
|
||||
self.table.selection()
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetComponent for IssueBrowser {
|
||||
fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
|
||||
let label_w = self
|
||||
.label
|
||||
.query(Attribute::Width)
|
||||
.unwrap_or(AttrValue::Size(1))
|
||||
.unwrap_size();
|
||||
let shortcuts_h = self
|
||||
.shortcuts
|
||||
.query(Attribute::Height)
|
||||
|
|
@ -78,17 +116,16 @@ impl WidgetComponent for IssueBrowser {
|
|||
.unwrap_size();
|
||||
let layout = layout::root_component(area, shortcuts_h);
|
||||
|
||||
self.label
|
||||
.view(frame, layout::centered_label(label_w, layout[0]));
|
||||
self.table.view(frame, layout[0]);
|
||||
self.shortcuts.view(frame, layout[1])
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::None
|
||||
self.table.state()
|
||||
}
|
||||
|
||||
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
||||
CmdResult::None
|
||||
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
||||
self.table.perform(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -221,17 +258,16 @@ pub fn patches(theme: &Theme, id: &Id, profile: &Profile) -> Widget<PatchBrowser
|
|||
Widget::new(PatchBrowser::new(profile, id, shortcuts, theme.clone()))
|
||||
}
|
||||
|
||||
pub fn issues(theme: &Theme) -> Widget<IssueBrowser> {
|
||||
pub fn issues(theme: &Theme, id: &Id, profile: &Profile) -> Widget<IssueBrowser> {
|
||||
let shortcuts = common::shortcuts(
|
||||
theme,
|
||||
vec![
|
||||
common::shortcut(theme, "tab", "section"),
|
||||
common::shortcut(theme, "↑/↓", "navigate"),
|
||||
common::shortcut(theme, "enter", "show"),
|
||||
common::shortcut(theme, "q", "quit"),
|
||||
],
|
||||
);
|
||||
|
||||
let not_implemented = common::label("not implemented").foreground(theme.colors.default_fg);
|
||||
let browser = IssueBrowser::new(not_implemented, shortcuts);
|
||||
|
||||
Widget::new(browser)
|
||||
Widget::new(IssueBrowser::new(theme, profile, id, shortcuts))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue