Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add description to issue page
Erik Kundt committed 2 years ago
commit 77b6744480ddc2ded1056227b209880d01f42132
parent 244ece0a73557e7a41682517c25bfc1cfa5e70d9
5 files changed +135 -40
modified src/app.rs
@@ -41,7 +41,7 @@ pub enum PatchCid {
pub enum IssueCid {
    Header,
    List,
-
    Discussion,
+
    Details,
    Shortcuts,
}

modified src/app/event.rs
@@ -10,7 +10,7 @@ use radicle_tui::ui::widget::{issue, patch};

use radicle_tui::ui::widget::Widget;

-
use super::{IssueMessage, Message, PatchMessage, PopupMessage};
+
use super::{IssueCid, IssueMessage, Message, PatchMessage, PopupMessage};

/// Since the framework does not know the type of messages that are being
/// passed around in the app, the following handlers need to be implemented for
@@ -73,14 +73,32 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<issue::LargeList> {
                    _ => None,
                }
            }
+
            Event::Keyboard(KeyEvent {
+
                code: Key::Enter, ..
+
            }) => Some(Message::Issue(IssueMessage::Focus(IssueCid::Details))),
            _ => None,
        }
    }
}

-
impl tuirealm::Component<Message, NoUserEvent> for Widget<issue::IssueDiscussion> {
-
    fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
-
        None
+
impl tuirealm::Component<Message, NoUserEvent> for Widget<issue::IssueDetails> {
+
    fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
+
        match event {
+
            Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
+
                self.perform(Cmd::Scroll(MoveDirection::Up));
+
                Some(Message::Tick)
+
            }
+
            Event::Keyboard(KeyEvent {
+
                code: Key::Down, ..
+
            }) => {
+
                self.perform(Cmd::Scroll(MoveDirection::Down));
+
                Some(Message::Tick)
+
            }
+
            Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
+
                Some(Message::Issue(IssueMessage::Focus(IssueCid::List)))
+
            }
+
            _ => None,
+
        }
    }
}

modified src/app/page.rs
@@ -166,9 +166,17 @@ impl ViewPage for IssuePage {
    ) -> Result<()> {
        let (id, issue) = &self.issue;
        let header = widget::common::app_header(context, theme, None).to_boxed();
+
        let comments = issue.comments().collect::<Vec<_>>();
+

        let list = widget::issue::list(context, theme, (*id, issue.clone())).to_boxed();
-
        let discussion =
-
            widget::issue::issue_discussion(context, theme, (*id, issue.clone())).to_boxed();
+
        let details = widget::issue::details(
+
            context,
+
            theme,
+
            (*id, issue.clone()),
+
            comments.first().copied(),
+
        )
+
        .to_boxed();
+

        let shortcuts = widget::common::shortcuts(
            theme,
            vec![
@@ -180,7 +188,7 @@ impl ViewPage for IssuePage {

        app.remount(Cid::Issue(IssueCid::Header), header, vec![])?;
        app.remount(Cid::Issue(IssueCid::List), list, vec![])?;
-
        app.remount(Cid::Issue(IssueCid::Discussion), discussion, vec![])?;
+
        app.remount(Cid::Issue(IssueCid::Details), details, vec![])?;
        app.remount(Cid::Issue(IssueCid::Shortcuts), shortcuts, vec![])?;

        app.active(&Cid::Issue(IssueCid::List))?;
@@ -191,7 +199,7 @@ impl ViewPage for IssuePage {
    fn unmount(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
        app.umount(&Cid::Issue(IssueCid::Header))?;
        app.umount(&Cid::Issue(IssueCid::List))?;
-
        app.umount(&Cid::Issue(IssueCid::Discussion))?;
+
        app.umount(&Cid::Issue(IssueCid::Details))?;
        app.umount(&Cid::Issue(IssueCid::Shortcuts))?;
        Ok(())
    }
@@ -207,9 +215,15 @@ impl ViewPage for IssuePage {
            Message::Issue(IssueMessage::Changed(id)) => {
                let repo = context.repository();
                if let Some(issue) = cob::issue::find(repo, &id)? {
-
                    let discussion =
-
                        widget::issue::issue_discussion(context, theme, (id, issue)).to_boxed();
-
                    app.remount(Cid::Issue(IssueCid::Discussion), discussion, vec![])?;
+
                    let comments = issue.comments().collect::<Vec<_>>();
+
                    let details = widget::issue::details(
+
                        context,
+
                        theme,
+
                        (id, issue.clone()),
+
                        comments.first().copied(),
+
                    )
+
                    .to_boxed();
+
                    app.remount(Cid::Issue(IssueCid::Details), details, vec![])?;
                }
            }
            Message::Issue(IssueMessage::Focus(cid)) => {
@@ -224,11 +238,11 @@ impl ViewPage for IssuePage {
    fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame) {
        let area = frame.size();
        let shortcuts_h = 1u16;
-
        let layout = layout::issue_preview(area, shortcuts_h);
+
        let layout = layout::issue_page(area, shortcuts_h);

        app.view(&Cid::Issue(IssueCid::Header), frame, layout.header);
        app.view(&Cid::Issue(IssueCid::List), frame, layout.left);
-
        app.view(&Cid::Issue(IssueCid::Discussion), frame, layout.right);
+
        app.view(&Cid::Issue(IssueCid::Details), frame, layout.right);
        app.view(&Cid::Issue(IssueCid::Shortcuts), frame, layout.shortcuts);
    }

modified src/ui/layout.rs
@@ -8,7 +8,7 @@ pub struct AppHeader {
    pub line: Rect,
}

-
pub struct IssuePreview {
+
pub struct IssuePage {
    pub header: Rect,
    pub left: Rect,
    pub right: Rect,
@@ -195,12 +195,9 @@ pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
        .split(popup_layout[1])[1]
}

-
pub fn issue_preview(area: Rect, shortcuts_h: u16) -> IssuePreview {
+
pub fn issue_page(area: Rect, shortcuts_h: u16) -> IssuePage {
+
    let content_h = area.height.saturating_sub(shortcuts_h);
    let header_h = 3u16;
-
    let content_h = area
-
        .height
-
        .saturating_sub(header_h)
-
        .saturating_sub(shortcuts_h);

    let root = Layout::default()
        .direction(Direction::Vertical)
@@ -220,7 +217,7 @@ pub fn issue_preview(area: Rect, shortcuts_h: u16) -> IssuePreview {
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
        .split(root[1]);

-
    IssuePreview {
+
    IssuePage {
        header: root[0],
        left: split[0],
        right: split[1],
modified src/ui/widget/issue.rs
@@ -1,3 +1,5 @@
+
use radicle::cob::thread::Comment;
+
use radicle::cob::thread::CommentId;
use radicle_cli::terminal::format;

use radicle::cob::issue::Issue;
@@ -10,6 +12,7 @@ use tuirealm::tui::layout::Layout;

use super::common::container::Container;
use super::common::container::LabeledContainer;
+
use super::common::label::Textarea;
use super::common::list::List;
use super::common::list::Property;
use super::Widget;
@@ -77,11 +80,11 @@ impl WidgetComponent for LargeList {
    }
}

-
pub struct Details {
+
pub struct IssueHeader {
    container: Widget<Container>,
}

-
impl Details {
+
impl IssueHeader {
    pub fn new(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Self {
        let repo = context.repository();

@@ -132,7 +135,7 @@ impl Details {
    }
}

-
impl WidgetComponent for Details {
+
impl WidgetComponent for IssueHeader {
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
        self.container.view(frame, area);
    }
@@ -146,34 +149,87 @@ impl WidgetComponent for Details {
    }
}

-
pub struct IssueDiscussion {
-
    details: Widget<Details>,
+
pub struct IssueDetails {
+
    header: Widget<IssueHeader>,
+
    description: Widget<CommentBody>,
}

-
impl IssueDiscussion {
-
    pub fn new(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Self {
+
impl IssueDetails {
+
    pub fn new(
+
        context: &Context,
+
        theme: &Theme,
+
        issue: (IssueId, Issue),
+
        description: Option<(&CommentId, &Comment)>,
+
    ) -> Self {
        Self {
-
            details: details(context, theme, issue),
+
            header: header(context, theme, issue),
+
            description: issue::description(context, theme, description),
        }
    }
}

-
impl WidgetComponent for IssueDiscussion {
-
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
+
impl WidgetComponent for IssueDetails {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let focus = properties
+
            .get_or(Attribute::Focus, AttrValue::Flag(false))
+
            .unwrap_flag();
        let layout = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(6), Constraint::Min(1)])
            .split(area);

-
        self.details.view(frame, layout[0]);
+
        self.header.view(frame, layout[0]);
+

+
        self.description
+
            .attr(Attribute::Focus, AttrValue::Flag(focus));
+
        self.description.view(frame, layout[1]);
    }

    fn state(&self) -> State {
        State::None
    }

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
+
    fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
+
        self.description.perform(cmd)
+
    }
+
}
+

+
pub struct CommentBody {
+
    textarea: Widget<Container>,
+
}
+

+
impl CommentBody {
+
    pub fn new(_context: &Context, theme: &Theme, comment: Option<(&CommentId, &Comment)>) -> Self {
+
        let content = match comment {
+
            Some((_, comment)) => comment.body().to_string(),
+
            None => String::new(),
+
        };
+
        let textarea = Widget::new(Textarea::new(theme.clone()))
+
            .content(AttrValue::String(content))
+
            .foreground(theme.colors.default_fg);
+

+
        let textarea = common::container(theme, textarea.to_boxed());
+

+
        Self { textarea }
+
    }
+
}
+

+
impl WidgetComponent for CommentBody {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let focus = properties
+
            .get_or(Attribute::Focus, AttrValue::Flag(false))
+
            .unwrap_flag();
+

+
        self.textarea.attr(Attribute::Focus, AttrValue::Flag(focus));
+
        self.textarea.view(frame, area);
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
+
        self.textarea.perform(cmd)
    }
}

@@ -183,17 +239,27 @@ pub fn list(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Widget
    Widget::new(list)
}

-
pub fn details(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Widget<Details> {
-
    let details = Details::new(context, theme, issue);
-
    Widget::new(details)
+
pub fn header(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Widget<IssueHeader> {
+
    let header = IssueHeader::new(context, theme, issue);
+
    Widget::new(header)
+
}
+

+
pub fn description(
+
    context: &Context,
+
    theme: &Theme,
+
    comment: Option<(&CommentId, &Comment)>,
+
) -> Widget<CommentBody> {
+
    let body = CommentBody::new(context, theme, comment);
+
    Widget::new(body)
}

-
pub fn issue_discussion(
+
pub fn details(
    context: &Context,
    theme: &Theme,
    issue: (IssueId, Issue),
-
) -> Widget<IssueDiscussion> {
-
    let discussion = IssueDiscussion::new(context, theme, issue);
+
    comment: Option<(&CommentId, &Comment)>,
+
) -> Widget<IssueDetails> {
+
    let discussion = IssueDetails::new(context, theme, issue, comment);
    Widget::new(discussion)
}