Files
novem/src/screen/dashboard/pull_request_view.rs

343 lines
11 KiB
Rust
Raw Normal View History

2026-05-23 18:45:44 +01:00
use std::sync::Arc;
2026-05-11 02:14:05 +08:00
use gpui::{
AppContext, InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled,
div, img, prelude::FluentBuilder,
2026-05-11 02:14:05 +08:00
};
2026-05-11 00:32:12 +08:00
use crate::{
2026-05-11 02:14:05 +08:00
api::{self},
2026-05-11 00:32:12 +08:00
app,
component::{
2026-05-13 02:23:20 +08:00
button::{self, Button, button},
2026-05-11 00:32:12 +08:00
font_icon::{FontIcon, font_icon},
markdown::{self, MarkdownText},
text::text,
},
query::{self, QueryStatus, read_query, use_query},
2026-05-18 22:30:46 +08:00
screen::dashboard::pull_request_diff_view::PullRequestDiffView,
2026-05-11 00:32:12 +08:00
};
pub(crate) struct PullRequestView {
markdown_viewer: Option<gpui::Entity<MarkdownText>>,
2026-05-18 22:30:46 +08:00
diff_view: Option<gpui::Entity<PullRequestDiffView>>,
2026-05-11 00:32:12 +08:00
pull_request_query: Option<query::Entity<api::issues::FetchPullRequest>>,
}
2026-05-13 02:23:20 +08:00
#[derive(gpui::IntoElement)]
struct Toolbar {}
pub fn new(_cx: &mut gpui::Context<PullRequestView>) -> PullRequestView {
2026-05-11 00:32:12 +08:00
PullRequestView {
markdown_viewer: None,
2026-05-18 22:30:46 +08:00
diff_view: None,
2026-05-11 00:32:12 +08:00
pull_request_query: None,
}
}
impl PullRequestView {
pub(crate) fn change_displayed_pull_request(
&mut self,
id: api::issues::Id,
cx: &mut gpui::Context<Self>,
) {
let query = use_query(api::issues::FetchPullRequest { id }, cx);
self.pull_request_query = Some(query.clone());
_ = cx
.observe(&query.clone(), move |this, _, cx| {
this.load_markdown_content(cx);
2026-05-11 00:32:12 +08:00
})
.detach();
// cached query will not trigger observe callback
// this is required so that content is loaded immediately for cached query
self.load_markdown_content(cx);
cx.notify();
}
fn load_markdown_content(&mut self, cx: &mut gpui::Context<Self>) {
let Some(query) = &self.pull_request_query else {
return;
};
let maybe_content = {
let data = read_query(&query, cx);
if let QueryStatus::Loaded(pr) = data {
2026-05-23 18:45:44 +01:00
Some(Arc::clone(&pr.body))
} else {
None
}
};
self.markdown_viewer = maybe_content.map(|content| cx.new(|cx| markdown::new(content, cx)));
2026-05-11 00:32:12 +08:00
cx.notify();
}
fn pr_content(
&self,
pr: &api::issues::DetailedPullRequest,
cx: &gpui::Context<Self>,
2026-05-11 02:14:05 +08:00
) -> gpui::AnyElement {
2026-05-11 00:32:12 +08:00
let theme = app::current_theme(cx);
let mut status_pill = div()
.flex()
.flex_row()
.items_center()
.gap_1()
.px_2()
.rounded_full();
match pr.state {
2026-05-14 00:05:31 +08:00
| api::issues::PullRequestState::Open => {
status_pill = status_pill
.bg(theme.colors.success_solid)
.child(
font_icon(FontIcon::PullRequestArrow)
.size_3()
.text_color(theme.colors.success_on_solid),
)
.child(
text("Open")
.text_color(theme.colors.success_on_solid)
2026-05-11 00:32:12 +08:00
.text_xs(),
);
2026-05-14 00:05:31 +08:00
}
| api::issues::PullRequestState::Closed => {
status_pill = status_pill
.bg(theme.colors.danger_solid)
.child(
font_icon(FontIcon::PullRequestClosed)
.size_3()
.text_color(theme.colors.danger_on_solid),
)
.child(
text("Closed")
.text_color(theme.colors.danger_on_solid)
.text_xs(),
);
}
| api::issues::PullRequestState::Merged => {
status_pill = status_pill.bg(theme.colors.accent_solid).child(
text("Merged")
.text_color(theme.colors.accent_on_solid)
.text_xs(),
);
}
2026-05-11 00:32:12 +08:00
}
2026-05-18 22:30:46 +08:00
let merge_text = pr.author.as_ref().map(|author| {
2026-05-23 18:45:44 +01:00
let base_branch = &pr.base_branch_name;
let head_branch = &pr.head_branch_name;
2026-05-14 00:05:31 +08:00
let str = format!(
"{} requested to merge {} into {}",
author.login, head_branch, base_branch
);
2026-05-11 00:32:12 +08:00
2026-05-14 00:05:31 +08:00
let head_branch_text_offset = author.login.len() + 20;
let base_branch_text_offset = head_branch_text_offset + head_branch.len() + 6;
2026-05-12 01:34:33 +08:00
2026-05-14 00:05:31 +08:00
let highlights = [
(
0..author.login.len(),
gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
..Default::default()
},
),
(
head_branch_text_offset..head_branch_text_offset + head_branch.len(),
gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
color: Some(theme.colors.accent_fg.into()),
..Default::default()
},
),
(
base_branch_text_offset..base_branch_text_offset + base_branch.len(),
gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
color: Some(theme.colors.accent_fg.into()),
..Default::default()
},
),
];
2026-05-12 01:34:33 +08:00
2026-05-18 22:30:46 +08:00
(
2026-05-14 00:05:31 +08:00
author,
gpui::StyledText::new(str).with_highlights(highlights),
2026-05-18 22:30:46 +08:00
)
});
2026-05-12 01:34:33 +08:00
2026-05-23 18:45:44 +01:00
let pr_title = gpui::SharedString::new(Arc::clone(&pr.title));
2026-05-12 01:34:33 +08:00
let metadata_line =
div()
.flex()
.flex_row()
.gap_2()
.when_some(merge_text, |it, (author, t)| {
it.child(
div()
.flex()
.flex_row()
.items_center()
.gap_1p5()
2026-05-23 18:45:44 +01:00
.child(img(author.avatar_url.as_ref()).size_4().rounded_full())
2026-05-12 01:34:33 +08:00
.child(
div()
.min_w_0()
.w_full()
.text_color(theme.colors.text)
.text_xs()
.font_weight(gpui::FontWeight::LIGHT)
.opacity(0.8)
.child(t),
),
)
});
2026-05-11 00:32:12 +08:00
div()
.size_full()
.flex()
.flex_col()
2026-05-14 00:05:31 +08:00
.bg(theme.colors.surface)
2026-05-11 02:14:05 +08:00
.overflow_hidden()
2026-05-13 02:23:20 +08:00
.child(Toolbar {})
2026-05-11 00:32:12 +08:00
.child(
div()
.flex()
.flex_row()
.justify_between()
.items_center()
2026-05-11 00:32:12 +08:00
.w_full()
.px_3p5()
.py_3()
.border_b_1()
2026-05-14 00:05:31 +08:00
.border_color(theme.colors.border_muted)
.child(
div()
.w_full()
.flex()
.flex_col()
.items_start()
2026-05-23 18:45:44 +01:00
.child(text(pr_title).w_full().text_xl().mb_1())
.child(metadata_line),
)
.child(div().flex().flex_col().items_end().gap_1().when_some(
pr.created_at,
|it, created_at| {
it.child(
text(created_at.format("%Y-%m-%d %H:%M").to_string())
.opacity(0.5)
.text_xs(),
)
.child(status_pill)
},
)),
2026-05-11 00:32:12 +08:00
)
2026-05-11 02:14:05 +08:00
.child(
div().flex_1().min_h_0().w_full().child(
div()
.id("pr-body-content")
.size_full()
.overflow_y_scroll()
.when_some(self.markdown_viewer.as_ref(), |it, viewer| {
it.child(div().w_full().p_3p5().child(viewer.clone()))
}),
),
)
.into_any_element()
2026-05-11 00:32:12 +08:00
}
}
impl gpui::Render for PullRequestView {
fn render(
&mut self,
2026-05-11 02:14:05 +08:00
_window: &mut gpui::Window,
2026-05-11 00:32:12 +08:00
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
2026-05-11 02:14:05 +08:00
div().size_full().child(match &self.pull_request_query {
2026-05-14 00:05:31 +08:00
| Some(q) => match read_query(q, cx) {
| QueryStatus::Loaded(pr) => self.pr_content(pr, cx),
| QueryStatus::Err(e) => div()
.size_full()
.child(format!("{:?}", e))
.into_any_element(),
| QueryStatus::Loading => div()
.size_full()
.child("loading pr content")
.into_any_element(),
},
| None => div().size_full().child("no pr selected").into_any_element(),
2026-05-11 02:14:05 +08:00
})
2026-05-11 00:32:12 +08:00
}
}
2026-05-13 02:23:20 +08:00
impl gpui::RenderOnce for Toolbar {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
2026-05-13 02:23:20 +08:00
fn toolbar_button(id: impl Into<gpui::ElementId>) -> Button {
button(id)
.px_2p5()
.py_1()
.variant(button::Variant::Secondary)
2026-05-14 00:05:31 +08:00
.border_0()
2026-05-13 02:23:20 +08:00
}
fn divider() -> gpui::Div {
div().h_full().w_px()
}
let theme = app::current_theme(cx);
2026-05-14 00:05:31 +08:00
2026-05-13 02:23:20 +08:00
div()
.w_full()
.flex()
.flex_row()
.items_center()
.justify_start()
2026-05-13 17:35:07 +08:00
.p_1()
2026-05-14 00:05:31 +08:00
.bg(theme.colors.surface)
2026-05-13 02:23:20 +08:00
.border_b_1()
2026-05-14 00:05:31 +08:00
.border_color(theme.colors.border_muted)
2026-05-13 02:23:20 +08:00
.child(
toolbar_button("pr-review-btn")
.leading(font_icon(FontIcon::Eye))
2026-05-13 17:35:07 +08:00
.mr_1(),
2026-05-13 02:23:20 +08:00
)
.child(
toolbar_button("pr-review-btn")
.leading(font_icon(FontIcon::RefreshCw))
2026-05-14 00:05:31 +08:00
.mr_1(),
2026-05-13 02:23:20 +08:00
)
2026-05-14 00:05:31 +08:00
.child(divider().bg(theme.colors.border).mr_1())
2026-05-13 17:35:07 +08:00
.child(toolbar_button("pr-review-btn").leading(font_icon(FontIcon::Star)))
2026-05-13 02:23:20 +08:00
.child(div().flex_1())
.child(
toolbar_button("pr-close-btn")
.leading(font_icon(FontIcon::PullRequestClosed))
2026-05-13 17:35:07 +08:00
.mr_1(),
2026-05-13 02:23:20 +08:00
)
.child(
toolbar_button("pr-merge-btn")
.variant(button::Variant::Primary)
.leading(font_icon(FontIcon::GitMerge))
.rounded_r_none(),
)
.child(divider())
.child(
toolbar_button("chevron")
.py_1()
.px_0p5()
.variant(button::Variant::Primary)
.leading(font_icon(FontIcon::ChevronDown))
.rounded_l_none(),
)
}
}